diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 568b4f35e..b0d4633a8 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -33,4 +33,4 @@ jobs: git lfs pull - name: Build run: | - .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -arch=ARM64 -platform=Android -configuration=Release -buildtargets=FlaxGame + .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=7 -arch=ARM64 -platform=Android -configuration=Release -buildtargets=FlaxGame diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml index f4b5d8147..2aec46320 100644 --- a/.github/workflows/build_ios.yml +++ b/.github/workflows/build_ios.yml @@ -33,4 +33,4 @@ jobs: git lfs pull - name: Build run: | - ./Development/Scripts/Mac/CallBuildTool.sh -build -log -arch=ARM64 -platform=iOS -configuration=Release -buildtargets=FlaxGame + ./Development/Scripts/Mac/CallBuildTool.sh -build -log -dotnet=7 -arch=ARM64 -platform=iOS -configuration=Release -buildtargets=FlaxGame diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index bd1726737..56accba84 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -36,7 +36,7 @@ jobs: git lfs pull - name: Build run: | - ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxEditor + ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxEditor # Game game-linux: @@ -64,4 +64,4 @@ jobs: git lfs pull - name: Build run: | - ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Linux -configuration=Release -buildtargets=FlaxGame + ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Linux -configuration=Release -buildtargets=FlaxGame diff --git a/.github/workflows/build_mac.yml b/.github/workflows/build_mac.yml index 88cb9b7a8..54bdb77b5 100644 --- a/.github/workflows/build_mac.yml +++ b/.github/workflows/build_mac.yml @@ -30,7 +30,7 @@ jobs: git lfs pull - name: Build run: | - ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Mac -configuration=Development -buildtargets=FlaxEditor + ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Mac -configuration=Development -buildtargets=FlaxEditor # Game game-mac: @@ -55,4 +55,4 @@ jobs: git lfs pull - name: Build run: | - ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Mac -configuration=Release -buildtargets=FlaxGame + ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Mac -configuration=Release -buildtargets=FlaxGame diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index 513cbfd08..b6131fb53 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -30,7 +30,7 @@ jobs: git lfs pull - name: Build run: | - .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxEditor + .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxEditor # Game game-windows: @@ -55,4 +55,4 @@ jobs: git lfs pull - name: Build run: | - .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -arch=x64 -platform=Windows -configuration=Release -buildtargets=FlaxGame + .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Windows -configuration=Release -buildtargets=FlaxGame diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f9d18bcfc..a524bdca2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,8 +34,8 @@ jobs: sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev - name: Build run: | - ./GenerateProjectFiles.sh -vs2022 -log -verbose -printSDKs - ./Development/Scripts/Linux/CallBuildTool.sh -build -log -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget + ./GenerateProjectFiles.sh -vs2022 -log -verbose -printSDKs -dotnet=7 + ./Development/Scripts/Linux/CallBuildTool.sh -build -log -dotnet=7 -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget dotnet msbuild Source/Tools/Flax.Build.Tests/Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo dotnet msbuild Source/Tools/Flax.Build.Tests/Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo - name: Test @@ -48,7 +48,7 @@ jobs: dotnet test -f net7.0 Binaries/Tests/FlaxEngine.CSharp.dll - name: Test UseLargeWorlds run: | - ./Development/Scripts/Linux/CallBuildTool.sh -build -log -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget -UseLargeWorlds=true + ./Development/Scripts/Linux/CallBuildTool.sh -build -log -dotnet=7 -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget -UseLargeWorlds=true ${GITHUB_WORKSPACE}/Binaries/Editor/Linux/Development/FlaxTests # Tests on Windows @@ -72,8 +72,8 @@ jobs: git lfs pull - name: Build run: | - .\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs - .\Development\Scripts\Windows\CallBuildTool.bat -build -log -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxTestsTarget + .\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs -dotnet=7 + .\Development\Scripts\Windows\CallBuildTool.bat -build -log -dotnet=7 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxTestsTarget dotnet msbuild Source\Tools\Flax.Build.Tests\Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo - name: Test run: | diff --git a/.gitignore b/.gitignore index 54907892f..b7e11e554 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ Source/*.csproj /Package_*/ !Source/Engine/Debug /Source/Platforms/Editor/Linux/Mono/etc/mono/registry +PackageEditor_Cert.command PackageEditor_Cert.bat PackagePlatforms_Cert.bat diff --git a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax index 8e810f7d4..cc4a16fe3 100644 --- a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax +++ b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57066ba805fd3f21a1d48048c7d3a0ec4e4c66c5cdd1ca97553605987f43b460 -size 41028 +oid sha256:5d0dc041c6b8712c2f892ac3185c1f9e0b3608b774db5590bcaad3ad0775dc93 +size 41042 diff --git a/Content/Editor/DebugMaterials/SingleColor/Particle.flax b/Content/Editor/DebugMaterials/SingleColor/Particle.flax index 72e956e7a..e22b6c1fa 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Particle.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Particle.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e737c911cffc1e1ad5cad3d7d4e13e24cdba7d08da4b488bf7bb41f098cb1638 -size 31713 +oid sha256:a54c7442f97baa1ab835485f906911372847808e2865790109b0524614663210 +size 31722 diff --git a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax index 2fffdd0eb..790690cd8 100644 --- a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax +++ b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7933e14f937b148d6b0c4a7fff10aa9b931f01e1bfe42ce3e5ff4575fbeeb463 -size 31873 +oid sha256:4cd7963263033f8f6e09de86da1ce14aa9baee3f1811986d56441cc62164e231 +size 31882 diff --git a/Content/Editor/Gizmo/FoliageBrushMaterial.flax b/Content/Editor/Gizmo/FoliageBrushMaterial.flax index 8eb64e89d..f1fa6e3f5 100644 --- a/Content/Editor/Gizmo/FoliageBrushMaterial.flax +++ b/Content/Editor/Gizmo/FoliageBrushMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67e5aba4c1eeb15231aef4001c7b0bb25db4243b871b1493774b1d1348310b22 -size 37868 +oid sha256:f87fe1172cc96b6aaef5653b9d7c5ef336b7f5934c620ae13661fd89b636cfcc +size 37909 diff --git a/Content/Editor/Particles/Constant Burst.flax b/Content/Editor/Particles/Constant Burst.flax index 16ca82be3..bb90199fe 100644 --- a/Content/Editor/Particles/Constant Burst.flax +++ b/Content/Editor/Particles/Constant Burst.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f6a7d30653808828c9f3ece113eb55684a330253366ff34de953085de4f9766 -size 2671 +oid sha256:fc1e46708152005dc92532e940b3ce19ec527b595fb18365b41ebdcd338ad2c4 +size 2705 diff --git a/Content/Editor/Particles/Periodic Burst.flax b/Content/Editor/Particles/Periodic Burst.flax index 80a4c306d..784d456c5 100644 --- a/Content/Editor/Particles/Periodic Burst.flax +++ b/Content/Editor/Particles/Periodic Burst.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeedc5055dc5a8218fd229a7a433ede79ce036eaf07326a9cee5f988b9b67a06 -size 3621 +oid sha256:94bf88983e3cf9fe555141a867af3c20e5bd58d13a527a9b4e02d4d1be157be9 +size 3664 diff --git a/Content/Editor/Particles/Smoke.flax b/Content/Editor/Particles/Smoke.flax index 1335a84f4..a682db6ee 100644 --- a/Content/Editor/Particles/Smoke.flax +++ b/Content/Editor/Particles/Smoke.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3dc51e7805056006ca6cbb481ba202583a9b2287c152fc04e28e1d07747d6ce -size 14706 +oid sha256:dc72b3152b85137a22b7dc72209ece02e8c2fff6674ddd395eaa4a4b089a51da +size 14662 diff --git a/Content/Editor/Particles/Sparks.flax b/Content/Editor/Particles/Sparks.flax index 7977e231b..227d9e381 100644 --- a/Content/Editor/Particles/Sparks.flax +++ b/Content/Editor/Particles/Sparks.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d902ab5f79426cc66dc5f19a3b8280136a58aa3c6fd317554d1a032357c65a -size 15275 +oid sha256:d38b4ed6a68e0c327e7d6dda2f47c6665611c4ae9c7f8b1ba6148eb26abb205f +size 13650 diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs index 663acf05f..30fbe9d86 100644 --- a/Content/Editor/Scripting/ScriptTemplate.cs +++ b/Content/Editor/Scripting/ScriptTemplate.cs @@ -33,4 +33,3 @@ public class %class% : Script // Here you can add code that needs to be called every frame } } - diff --git a/Content/Editor/Terrain/Circle Brush Material.flax b/Content/Editor/Terrain/Circle Brush Material.flax index b014ffb1f..9b21e6c82 100644 --- a/Content/Editor/Terrain/Circle Brush Material.flax +++ b/Content/Editor/Terrain/Circle Brush Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e73a50fe4296e58eff5942855dd655ba4c2c776ee3acc0451844645dda14247 -size 27150 +oid sha256:a20f7283220500bb17c8c5afbab5a5c1bfdc03703868b8ced7da462657806fd7 +size 27409 diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax index 18f653f86..0e94c5c06 100644 --- a/Content/Shaders/GlobalSignDistanceField.flax +++ b/Content/Shaders/GlobalSignDistanceField.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c8aa181a814d69b15ffec6493a71a6f42ae816ce04f7803cff2d5073b4b3c4f -size 11790 +oid sha256:e075583620e62407503c73f52487c204ddcad421e80fa621aebd55af1cfb08d5 +size 11798 diff --git a/Flax.flaxproj b/Flax.flaxproj index ebcdc2830..81a63a1fc 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -3,7 +3,8 @@ "Version": { "Major": 1, "Minor": 7, - "Build": 6401 + "Revision": 1, + "Build": 6406 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", diff --git a/GenerateProjectFiles.bat b/GenerateProjectFiles.bat index 622939c34..28970a203 100644 --- a/GenerateProjectFiles.bat +++ b/GenerateProjectFiles.bat @@ -15,7 +15,7 @@ if errorlevel 1 goto BuildToolFailed :: Build bindings for all editor configurations echo Building C# bindings... -Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor,FlaxGame +Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor popd echo Done! diff --git a/GenerateProjectFiles.command b/GenerateProjectFiles.command index 5ee5c0783..a42121252 100755 --- a/GenerateProjectFiles.command +++ b/GenerateProjectFiles.command @@ -14,4 +14,4 @@ bash ./Development/Scripts/Mac/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor diff --git a/GenerateProjectFiles.sh b/GenerateProjectFiles.sh index dceb8abe8..76d96c7ef 100755 --- a/GenerateProjectFiles.sh +++ b/GenerateProjectFiles.sh @@ -14,4 +14,4 @@ bash ./Development/Scripts/Linux/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor diff --git a/PackageAll.bat b/PackageAll.bat index 02ab69f4d..0325f2244 100644 --- a/PackageAll.bat +++ b/PackageAll.bat @@ -7,7 +7,7 @@ pushd echo Performing the full package... rem Run the build tool. -call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -deployPlatforms -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %* +call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -deployPlatforms -dotnet=7 -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %* if errorlevel 1 goto BuildToolFailed popd diff --git a/PackageEditor.bat b/PackageEditor.bat index 2dc90f7f4..515b81871 100644 --- a/PackageEditor.bat +++ b/PackageEditor.bat @@ -7,7 +7,7 @@ pushd echo Building and packaging Flax Editor... rem Run the build tool. -call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %* +call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -dotnet=7 -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %* if errorlevel 1 goto BuildToolFailed popd diff --git a/PackageEditor.command b/PackageEditor.command index d7cc909a0..eb62ae1a4 100755 --- a/PackageEditor.command +++ b/PackageEditor.command @@ -9,4 +9,4 @@ echo Building and packaging Flax Editor... cd "`dirname "$0"`" # Run Flax.Build (also pass the arguments) -bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployEditor --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" +bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployEditor --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" diff --git a/PackageEditor.sh b/PackageEditor.sh index 0584ab4e7..151147b6a 100755 --- a/PackageEditor.sh +++ b/PackageEditor.sh @@ -9,4 +9,4 @@ echo Building and packaging Flax Editor... cd "`dirname "$0"`" # Run Flax.Build (also pass the arguments) -bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployEditor --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" +bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployEditor --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" diff --git a/PackagePlatforms.bat b/PackagePlatforms.bat index d8ebd0980..eb9c42d34 100644 --- a/PackagePlatforms.bat +++ b/PackagePlatforms.bat @@ -7,7 +7,7 @@ pushd echo Building and packaging platforms data... rem Run the build tool. -call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployPlatforms -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %* +call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployPlatforms -dotnet=7 -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %* if errorlevel 1 goto BuildToolFailed popd diff --git a/PackagePlatforms.command b/PackagePlatforms.command index 20847f232..e9182a627 100755 --- a/PackagePlatforms.command +++ b/PackagePlatforms.command @@ -9,4 +9,4 @@ echo Building and packaging platforms data... cd "`dirname "$0"`" # Run Flax.Build (also pass the arguments) -bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployPlatforms --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" +bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployPlatforms --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" diff --git a/PackagePlatforms.sh b/PackagePlatforms.sh index 3f9a9c550..f7e43cc42 100755 --- a/PackagePlatforms.sh +++ b/PackagePlatforms.sh @@ -9,4 +9,4 @@ echo Building and packaging platforms data... cd "`dirname "$0"`" # Run Flax.Build (also pass the arguments) -bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployPlatforms --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" +bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployPlatforms --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@" diff --git a/README.md b/README.md index fac631a6a..d6688bd03 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Flax Engine is a high quality modern 3D game engine written in C++ and C#. -From stunning graphics to powerful scripts - Flax can give everything for your games. Designed for fast workflow with many ready to use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). +From stunning graphics to powerful scripts, it's designed for fast workflow with many ready-to-use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). This repository contains full source code of the Flax Engine (excluding NDA-protected platforms support). Anyone is welcome to contribute or use the modified source in Flax-based games. diff --git a/Source/Editor/Content/AssetPickerValidator.cs b/Source/Editor/Content/AssetPickerValidator.cs new file mode 100644 index 000000000..f43ab6a29 --- /dev/null +++ b/Source/Editor/Content/AssetPickerValidator.cs @@ -0,0 +1,292 @@ +using System; +using System.IO; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.Utilities; + +namespace FlaxEditor.Content; + +/// +/// Manages and converts the selected content item to the appropriate types. Useful for drag operations. +/// +public class AssetPickerValidator : IContentItemOwner +{ + private Asset _selected; + private ContentItem _selectedItem; + private ScriptType _type; + private string _fileExtension; + + /// + /// Gets or sets the selected item. + /// + public ContentItem SelectedItem + { + get => _selectedItem; + set + { + if (_selectedItem == value) + return; + if (value == null) + { + if (_selected == null && _selectedItem is SceneItem) + { + // Deselect scene reference + _selectedItem.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + return; + } + + // Deselect + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + } + else if (value is SceneItem item) + { + if (_selectedItem == item) + return; + if (!IsValid(item)) + item = null; + + // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = null; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + else if (value is AssetItem assetItem) + { + SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); + } + else + { + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = value; + _selected = null; + OnSelectedItemChanged(); + } + } + } + + /// + /// Gets or sets the selected asset identifier. + /// + public Guid SelectedID + { + get + { + if (_selected != null) + return _selected.ID; + if (_selectedItem is AssetItem assetItem) + return assetItem.ID; + return Guid.Empty; + } + set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); + } + + /// + /// Gets or sets the selected content item path. + /// + public string SelectedPath + { + get + { + string path = _selectedItem?.Path ?? _selected?.Path; + if (path != null) + { + // Convert into path relative to the project (cross-platform) + var projectFolder = Globals.ProjectFolder; + if (path.StartsWith(projectFolder)) + path = path.Substring(projectFolder.Length + 1); + } + return path; + } + set + { + if (string.IsNullOrEmpty(value)) + { + SelectedItem = null; + } + else + { + var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value; + SelectedItem = Editor.Instance.ContentDatabase.Find(path); + } + } + } + + /// + /// Gets or sets the selected asset object. + /// + public Asset SelectedAsset + { + get => _selected; + set + { + // Check if value won't change + if (value == _selected) + return; + + // Find item from content database and check it + var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; + if (item != null && !IsValid(item)) + item = null; + + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = value; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + } + + /// + /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. + /// + public ScriptType AssetType + { + get => _type; + set + { + if (_type != value) + { + _type = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Gets or sets the content items extensions filter. Null if unused. + /// + public string FileExtension + { + get => _fileExtension; + set + { + if (_fileExtension != value) + { + _fileExtension = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Occurs when selected item gets changed. + /// + public event Action SelectedItemChanged; + + /// + /// The custom callback for assets validation. Cane be used to implement a rule for assets to pick. + /// + public Func CheckValid; + + /// + /// Returns whether item is valid. + /// + /// + /// + public bool IsValid(ContentItem item) + { + if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) + return false; + if (CheckValid != null && !CheckValid(item)) + return false; + if (_type == ScriptType.Null) + return true; + + if (item is AssetItem assetItem) + { + // Faster path for binary items (in-built) + if (assetItem is BinaryAssetItem binaryItem) + return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); + + // Type filter + var type = TypeUtils.GetType(assetItem.TypeName); + if (_type.IsAssignableFrom(type)) + return true; + + // Json assets can contain any type of the object defined by the C# type (data oriented design) + if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) + return true; + + // Special case for scene asset references + if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) + return true; + } + + return false; + } + + /// + /// Initializes a new instance of the class. + /// + public AssetPickerValidator() + : this(new ScriptType(typeof(Asset))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The assets types that this picker accepts. + public AssetPickerValidator(ScriptType assetType) + { + _type = assetType; + } + + /// + /// Called when selected item gets changed. + /// + protected virtual void OnSelectedItemChanged() + { + SelectedItemChanged?.Invoke(); + } + + /// + public void OnItemDeleted(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + public void OnItemRenamed(ContentItem item) + { + } + + /// + public void OnItemReimported(ContentItem item) + { + } + + /// + public void OnItemDispose(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + /// Call to remove reference from the selected item. + /// + public void OnDestroy() + { + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + } +} diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs index 2f6651b14..e2cd1e771 100644 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentTreeNode.cs @@ -24,6 +24,16 @@ namespace FlaxEditor.Content /// protected ContentFolder _folder; + /// + /// Whether this node can be deleted. + /// + public virtual bool CanDelete => true; + + /// + /// Whether this node can be duplicated. + /// + public virtual bool CanDuplicate => true; + /// /// Gets the content folder item. /// @@ -86,6 +96,7 @@ namespace FlaxEditor.Content Folder.ParentFolder = parent.Folder; Parent = parent; } + IconColor = Style.Current.Foreground; } /// @@ -300,7 +311,7 @@ namespace FlaxEditor.Content StartRenaming(); return true; case KeyboardKeys.Delete: - if (Folder.Exists) + if (Folder.Exists && CanDelete) Editor.Instance.Windows.ContentWin.Delete(Folder); return true; } @@ -309,7 +320,7 @@ namespace FlaxEditor.Content switch (key) { case KeyboardKeys.D: - if (Folder.Exists) + if (Folder.Exists && CanDuplicate) Editor.Instance.Windows.ContentWin.Duplicate(Folder); return true; } diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index def873622..a4f24f4ab 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -12,6 +12,12 @@ namespace FlaxEditor.Content { private FileSystemWatcher _watcher; + /// + public override bool CanDelete => false; + + /// + public override bool CanDuplicate => false; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/Cooker/CookingData.h b/Source/Editor/Cooker/CookingData.h index 51a3fcd79..1924b0c56 100644 --- a/Source/Editor/Cooker/CookingData.h +++ b/Source/Editor/Cooker/CookingData.h @@ -12,6 +12,13 @@ class GameCooker; class PlatformTools; +#if OFFICIAL_BUILD +// Use the fixed .NET SDK version in packaged builds for compatibility (FlaxGame is precompiled with it) +#define GAME_BUILD_DOTNET_VER TEXT("-dotnet=7") +#else +#define GAME_BUILD_DOTNET_VER TEXT("") +#endif + /// /// Game building options. Used as flags. /// diff --git a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp index 7d05409e5..995f19fce 100644 --- a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp @@ -280,17 +280,25 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) const Char* gradlew = TEXT("gradlew"); #endif #if PLATFORM_LINUX - Platform::RunProcess(String::Format(TEXT("chmod +x \"{0}/gradlew\""), data.OriginalOutputPath), data.OriginalOutputPath, Dictionary(), true); + { + CreateProcessSettings procSettings; + procSettings.FileName = String::Format(TEXT("chmod +x \"{0}/gradlew\""), data.OriginalOutputPath); + procSettings.WorkingDirectory = data.OriginalOutputPath; + procSettings.HiddenWindow = true; + Platform::CreateProcess(procSettings); + } #endif const bool distributionPackage = buildSettings->ForDistribution; - CreateProcessSettings procSettings; - procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT("assemble") : TEXT("assembleDebug")); - procSettings.WorkingDirectory = data.OriginalOutputPath; - const int32 result = Platform::CreateProcess(procSettings); - if (result != 0) { - data.Error(String::Format(TEXT("Failed to build Gradle project into package (result code: {0}). See log for more info."), result)); - return true; + CreateProcessSettings procSettings; + procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT("assemble") : TEXT("assembleDebug")); + procSettings.WorkingDirectory = data.OriginalOutputPath; + const int32 result = Platform::CreateProcess(procSettings); + if (result != 0) + { + data.Error(String::Format(TEXT("Failed to build Gradle project into package (result code: {0}). See log for more info."), result)); + return true; + } } // Copy result package diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp index ec7b8e7e1..42ef6fcdf 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp @@ -104,4 +104,19 @@ bool LinuxPlatformTools::OnDeployBinaries(CookingData& data) return false; } +void LinuxPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h index 562b38962..432240d00 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h @@ -20,6 +20,7 @@ public: ArchitectureType GetArchitecture() const override; bool UseSystemDotnet() const override; bool OnDeployBinaries(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp index a1db61dbb..aa56a6b95 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp @@ -249,4 +249,19 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) return false; } +void MacPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h index 21d9141e3..efdd0b733 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h @@ -27,6 +27,7 @@ public: bool IsNativeCodeFile(CookingData& data, const String& file) override; void OnBuildStarted(CookingData& data) override; bool OnPostProcess(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif diff --git a/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp b/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp index 361e5f937..befc41640 100644 --- a/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp +++ b/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp @@ -188,8 +188,8 @@ bool CompileScriptsStep::Perform(CookingData& data) LOG(Info, "Starting scripts compilation for game..."); const String logFile = data.CacheDirectory / TEXT("CompileLog.txt"); auto args = String::Format( - TEXT("-log -logfile=\"{4}\" -build -mutex -buildtargets={0} -platform={1} -arch={2} -configuration={3} -aotMode={5}"), - target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT())); + TEXT("-log -logfile=\"{4}\" -build -mutex -buildtargets={0} -platform={1} -arch={2} -configuration={3} -aotMode={5} {6}"), + target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT()), GAME_BUILD_DOTNET_VER); #if PLATFORM_WINDOWS if (data.Platform == BuildPlatform::LinuxX64) #elif PLATFORM_LINUX diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index 8306475d1..c1179c77a 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -87,7 +87,7 @@ bool DeployDataStep::Perform(CookingData& data) { // Ask Flax.Build to provide .Net SDK location for the current platform String sdks; - bool failed = ScriptsBuilder::RunBuildTool(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs"), data.CacheDirectory); + bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), GAME_BUILD_DOTNET_VER), data.CacheDirectory); failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks); int32 idx = sdks.Find(TEXT("DotNetSdk, "), StringSearchCase::CaseSensitive); if (idx != -1) @@ -168,7 +168,7 @@ bool DeployDataStep::Perform(CookingData& data) String sdks; const Char *platformName, *archName; data.GetBuildPlatformName(platformName, archName); - String args = String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printDotNetRuntime -platform={} -arch={}"), platformName, archName); + String args = String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printDotNetRuntime -platform={} -arch={} {}"), platformName, archName, GAME_BUILD_DOTNET_VER); bool failed = ScriptsBuilder::RunBuildTool(args, data.CacheDirectory); failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks); Array parts; @@ -269,8 +269,8 @@ bool DeployDataStep::Perform(CookingData& data) LOG(Info, "Optimizing .NET class library size to include only used assemblies"); const String logFile = data.CacheDirectory / TEXT("StripDotnetLibs.txt"); String args = String::Format( - TEXT("-log -logfile=\"{}\" -runDotNetClassLibStripping -mutex -binaries=\"{}\""), - logFile, data.DataOutputPath); + TEXT("-log -logfile=\"{}\" -runDotNetClassLibStripping -mutex -binaries=\"{}\" {}"), + logFile, data.DataOutputPath, GAME_BUILD_DOTNET_VER); for (const String& define : data.CustomDefines) { args += TEXT(" -D"); diff --git a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp index deb8f5022..5d566efb5 100644 --- a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp +++ b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp @@ -67,8 +67,8 @@ bool PrecompileAssembliesStep::Perform(CookingData& data) data.GetBuildPlatformName(platform, architecture); const String logFile = data.CacheDirectory / TEXT("AOTLog.txt"); String args = String::Format( - TEXT("-log -logfile=\"{}\" -runDotNetAOT -mutex -platform={} -arch={} -configuration={} -aotMode={} -binaries=\"{}\" -intermediate=\"{}\""), - logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath); + TEXT("-log -logfile=\"{}\" -runDotNetAOT -mutex -platform={} -arch={} -configuration={} -aotMode={} -binaries=\"{}\" -intermediate=\"{}\" {}"), + logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath, GAME_BUILD_DOTNET_VER); if (!buildSettings.SkipUnusedDotnetLibsPackaging) args += TEXT(" -skipUnusedDotnetLibs=false"); // Run AOT on whole class library (not just used libs) for (const String& define : data.CustomDefines) diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 03b21e12b..9de330213 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -157,6 +157,12 @@ namespace FlaxEditor.CustomEditors var values = _values; var presenter = _presenter; var layout = _layout; + if (layout.Editors.Count > 1) + { + // There are more editors using the same layout so rebuild parent editor to prevent removing others editors + _parent?.RebuildLayout(); + return; + } var control = layout.ContainerControl; var parent = _parent; var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; diff --git a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs index 71c5f6daf..4099e5aee 100644 --- a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs @@ -225,8 +225,15 @@ namespace FlaxEditor.CustomEditors.Dedicated } _actor = actor; - var showActorPicker = actor == null || ParentEditor.Values.All(x => x is not Cloth); - if (showActorPicker) + if (ParentEditor.Values.Any(x => x is Cloth)) + { + // Cloth always picks the parent model mesh + if (actor == null) + { + layout.Label("Cloth needs to be added as a child to model actor."); + } + } + else { // Actor reference picker _actorPicker = layout.Custom(); @@ -242,7 +249,10 @@ namespace FlaxEditor.CustomEditors.Dedicated { var model = staticModel.Model; if (model == null || model.WaitForLoaded()) + { + layout.Label("No model."); return; + } var materials = model.MaterialSlots; var lods = model.LODs; meshNames = new string[lods.Length][]; @@ -267,7 +277,10 @@ namespace FlaxEditor.CustomEditors.Dedicated { var skinnedModel = animatedModel.SkinnedModel; if (skinnedModel == null || skinnedModel.WaitForLoaded()) + { + layout.Label("No model."); return; + } var materials = skinnedModel.MaterialSlots; var lods = skinnedModel.LODs; meshNames = new string[lods.Length][]; diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 8fb742b5e..7e0c6f38c 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -37,41 +37,32 @@ public class MissingScriptEditor : GenericEditor Parent = _dropPanel, Height = 64, }; - _replaceScriptButton = new Button { Text = "Replace Script", TooltipText = "Replaces the missing script with a given script type", AnchorPreset = AnchorPresets.TopCenter, - Width = 240, - Height = 24, - X = -120, - Y = 0, + Bounds = new Rectangle(-120, 0, 240, 24), Parent = replaceScriptPanel, }; _replaceScriptButton.Clicked += OnReplaceScriptButtonClicked; - var replaceAllLabel = new Label { Text = "Replace all matching missing scripts", TooltipText = "Whether or not to apply this script change to all scripts missing the same type.", AnchorPreset = AnchorPresets.BottomCenter, - Y = -34, + Y = -38, Parent = replaceScriptPanel, }; - replaceAllLabel.X -= FlaxEngine.GUI.Style.Current.FontSmall.MeasureText(replaceAllLabel.Text).X; - _shouldReplaceAllCheckbox = new CheckBox { TooltipText = replaceAllLabel.TooltipText, AnchorPreset = AnchorPresets.BottomCenter, - Y = -34, + Y = -38, Parent = replaceScriptPanel, }; - - float centerDifference = (_shouldReplaceAllCheckbox.Right - replaceAllLabel.Left) / 2; - replaceAllLabel.X += centerDifference; - _shouldReplaceAllCheckbox.X += centerDifference; + _shouldReplaceAllCheckbox.X -= _replaceScriptButton.Width * 0.5f + 0.5f; + replaceAllLabel.X -= 52; base.Initialize(layout); } diff --git a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs index 5215e23a4..296560507 100644 --- a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs @@ -695,7 +695,41 @@ namespace FlaxEditor.CustomEditors.Dedicated private void SetType(ref ScriptType controlType, UIControl uiControl) { string previousName = uiControl.Control?.GetType().Name ?? nameof(UIControl); - uiControl.Control = (Control)controlType.CreateInstance(); + + var oldControlType = (Control)uiControl.Control; + var newControlType = (Control)controlType.CreateInstance(); + + // copy old control data to new control + if (oldControlType != null) + { + newControlType.Visible = oldControlType.Visible; + newControlType.Enabled = oldControlType.Enabled; + newControlType.AutoFocus = oldControlType.AutoFocus; + + newControlType.AnchorMin = oldControlType.AnchorMin; + newControlType.AnchorMax = oldControlType.AnchorMax; + newControlType.Offsets = oldControlType.Offsets; + + newControlType.LocalLocation = oldControlType.LocalLocation; + newControlType.Scale = oldControlType.Scale; + newControlType.Bounds = oldControlType.Bounds; + newControlType.Width = oldControlType.Width; + newControlType.Height = oldControlType.Height; + newControlType.Center = oldControlType.Center; + newControlType.PivotRelative = oldControlType.PivotRelative; + + newControlType.Pivot = oldControlType.Pivot; + newControlType.Shear = oldControlType.Shear; + newControlType.Rotation = oldControlType.Rotation; + } + if (oldControlType is ContainerControl oldContainer && newControlType is ContainerControl newContainer) + { + newContainer.CullChildren = oldContainer.CullChildren; + newContainer.ClipChildren = oldContainer.ClipChildren; + } + + uiControl.Control = newControlType; + if (uiControl.Name.StartsWith(previousName)) { string newName = controlType.Name + uiControl.Name.Substring(previousName.Length); diff --git a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs index bf087feda..4829bd91a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs @@ -34,7 +34,7 @@ namespace FlaxEditor.CustomEditors.Editors value = 0; // If selected is single actor that has children, ask if apply layer to the sub objects as well - if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren) + if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren && !Editor.IsPlayMode) { var valueText = comboBox.SelectedItem; diff --git a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs index cfba940c2..1f3359fd5 100644 --- a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs @@ -71,7 +71,7 @@ namespace FlaxEditor.CustomEditors.Editors { // Generic file picker assetType = ScriptType.Null; - Picker.FileExtension = assetReference.TypeName; + Picker.Validator.FileExtension = assetReference.TypeName; } else { @@ -85,7 +85,7 @@ namespace FlaxEditor.CustomEditors.Editors } } - Picker.AssetType = assetType; + Picker.Validator.AssetType = assetType; Picker.Height = height; Picker.SelectedItemChanged += OnSelectedItemChanged; } @@ -95,15 +95,15 @@ namespace FlaxEditor.CustomEditors.Editors if (_isRefreshing) return; if (typeof(AssetItem).IsAssignableFrom(_valueType.Type)) - SetValue(Picker.SelectedItem); + SetValue(Picker.Validator.SelectedItem); else if (_valueType.Type == typeof(Guid)) - SetValue(Picker.SelectedID); + SetValue(Picker.Validator.SelectedID); else if (_valueType.Type == typeof(SceneReference)) - SetValue(new SceneReference(Picker.SelectedID)); + SetValue(new SceneReference(Picker.Validator.SelectedID)); else if (_valueType.Type == typeof(string)) - SetValue(Picker.SelectedPath); + SetValue(Picker.Validator.SelectedPath); else - SetValue(Picker.SelectedAsset); + SetValue(Picker.Validator.SelectedAsset); } /// @@ -115,15 +115,15 @@ namespace FlaxEditor.CustomEditors.Editors { _isRefreshing = true; if (Values[0] is AssetItem assetItem) - Picker.SelectedItem = assetItem; + Picker.Validator.SelectedItem = assetItem; else if (Values[0] is Guid guid) - Picker.SelectedID = guid; + Picker.Validator.SelectedID = guid; else if (Values[0] is SceneReference sceneAsset) - Picker.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); + Picker.Validator.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); else if (Values[0] is string path) - Picker.SelectedPath = path; + Picker.Validator.SelectedPath = path; else - Picker.SelectedAsset = Values[0] as Asset; + Picker.Validator.SelectedAsset = Values[0] as Asset; _isRefreshing = false; } } diff --git a/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs index 8ac6a51cb..b3c5792ac 100644 --- a/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs +++ b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs @@ -171,11 +171,13 @@ namespace FlaxEditor.CustomEditors.Editors tree.Select(typeNode); if (addItems) { - var items = GenericEditor.GetItemsForType(type, type.IsClass, true); + var items = GenericEditor.GetItemsForType(type, type.IsClass, true, true); foreach (var item in items) { if (typed && !typed.IsAssignableFrom(item.Info.ValueType)) continue; + if (item.Info.DeclaringType.Type == typeof(FlaxEngine.Object)) + continue; // Skip engine internals var itemPath = typePath + item.Info.Name; var node = new TreeNode { diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 8922e2d25..6f623fb23 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -3,9 +3,12 @@ using System; using System.Collections; using System.Linq; +using FlaxEditor.Content; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Drag; +using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; @@ -110,7 +113,7 @@ namespace FlaxEditor.CustomEditors.Editors public override void Initialize(LayoutElementsContainer layout) { // No support for different collections for now - if (HasDifferentValues || HasDifferentTypes) + if (HasDifferentTypes) return; var size = Count; @@ -135,14 +138,43 @@ namespace FlaxEditor.CustomEditors.Editors spacing = collection.Spacing; } + var dragArea = layout.CustomContainer(); + dragArea.CustomControl.Editor = this; + dragArea.CustomControl.ElementType = ElementType; + + // Check for the AssetReferenceAttribute. In JSON assets, it can be used to filter + // which scripts can be dragged over and dropped on this collection editor. + var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute); + if (assetReference != null) + { + if (string.IsNullOrEmpty(assetReference.TypeName)) + { + } + else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.') + { + dragArea.CustomControl.ElementType = ScriptType.Null; + dragArea.CustomControl.FileExtension = assetReference.TypeName; + } + else + { + var customType = TypeUtils.GetType(assetReference.TypeName); + if (customType != ScriptType.Null) + dragArea.CustomControl.ElementType = customType; + else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName)) + Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for drag and drop filter.", assetReference.TypeName)); + else + dragArea.CustomControl.ElementType = ScriptType.Void; + } + } + // Size if (_readOnly || (NotNullItems && size == 0)) { - layout.Label("Size", size.ToString()); + dragArea.Label("Size", size.ToString()); } else { - _size = layout.IntegerValue("Size"); + _size = dragArea.IntegerValue("Size"); _size.IntValue.MinValue = 0; _size.IntValue.MaxValue = ushort.MaxValue; _size.IntValue.Value = size; @@ -152,7 +184,7 @@ namespace FlaxEditor.CustomEditors.Editors // Elements if (size > 0) { - var panel = layout.VerticalPanel(); + var panel = dragArea.VerticalPanel(); panel.Panel.BackgroundColor = _background; var elementType = ElementType; @@ -212,37 +244,33 @@ namespace FlaxEditor.CustomEditors.Editors // Add/Remove buttons if (!_readOnly) { - var area = layout.Space(20); - var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16) - { - Text = "+", - TooltipText = "Add new item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = !NotNullItems || size > 0, - }; - addButton.Clicked += () => - { - if (IsSetBlocked) - return; + var panel = dragArea.HorizontalPanel(); + panel.Panel.Size = new Float2(0, 20); + panel.Panel.Margin = new Margin(2); - Resize(Count + 1); - }; - var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16) - { - Text = "-", - TooltipText = "Remove last item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = size > 0, - }; - removeButton.Clicked += () => + var removeButton = panel.Button("-", "Remove last item"); + removeButton.Button.Size = new Float2(16, 16); + removeButton.Button.Enabled = size > 0; + removeButton.Button.AnchorPreset = AnchorPresets.TopRight; + removeButton.Button.Clicked += () => { if (IsSetBlocked) return; Resize(Count - 1); }; + + var addButton = panel.Button("+", "Add new item"); + addButton.Button.Size = new Float2(16, 16); + addButton.Button.Enabled = !NotNullItems || size > 0; + addButton.Button.AnchorPreset = AnchorPresets.TopRight; + addButton.Button.Clicked += () => + { + if (IsSetBlocked) + return; + + Resize(Count + 1); + }; } } @@ -369,5 +397,232 @@ namespace FlaxEditor.CustomEditors.Editors } return base.OnDirty(editor, value, token); } + + private class DragAreaControl : VerticalPanel + { + private DragItems _dragItems; + private DragActors _dragActors; + private DragHandlers _dragHandlers; + private AssetPickerValidator _pickerValidator; + + public ScriptType ElementType + { + get => _pickerValidator?.AssetType ?? ScriptType.Null; + set => _pickerValidator = new AssetPickerValidator(value); + } + + public CollectionEditor Editor { get; set; } + + public string FileExtension + { + set => _pickerValidator.FileExtension = value; + } + + /// + public override void Draw() + { + if (_dragHandlers is { HasValidDrag: true }) + { + var area = new Rectangle(Float2.Zero, Size); + Render2D.FillRectangle(area, Color.Orange * 0.5f); + Render2D.DrawRectangle(area, Color.Black); + } + + base.Draw(); + } + + public override void OnDestroy() + { + _pickerValidator.OnDestroy(); + } + + private bool ValidateActors(ActorNode node) + { + return node.Actor.GetScript(ElementType.Type) || ElementType.Type.IsAssignableTo(typeof(Actor)); + } + + /// + public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) + { + var result = base.OnDragEnter(ref location, data); + if (result != DragDropEffect.None) + return result; + + if (_dragHandlers == null) + { + _dragItems = new DragItems(_pickerValidator.IsValid); + _dragActors = new DragActors(ValidateActors); + _dragHandlers = new DragHandlers + { + _dragActors, + _dragItems + }; + } + return _dragHandlers.OnDragEnter(data); + } + + /// + public override DragDropEffect OnDragMove(ref Float2 location, DragData data) + { + var result = base.OnDragMove(ref location, data); + if (result != DragDropEffect.None) + return result; + + return _dragHandlers.Effect; + } + + /// + public override void OnDragLeave() + { + _dragHandlers.OnDragLeave(); + + base.OnDragLeave(); + } + + /// + public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) + { + var result = base.OnDragDrop(ref location, data); + if (result != DragDropEffect.None) + { + _dragHandlers.OnDragDrop(null); + return result; + } + + if (_dragHandlers.HasValidDrag) + { + if (_dragItems.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragItems.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = _dragItems.Objects[i - oldSize], + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + array.SetValue(validator.SelectedItem, i); + else if (ElementType.Type == typeof(Guid)) + array.SetValue(validator.SelectedID, i); + else if (ElementType.Type == typeof(SceneReference)) + array.SetValue(new SceneReference(validator.SelectedID), i); + else if (ElementType.Type == typeof(string)) + array.SetValue(validator.SelectedPath, i); + else + array.SetValue(validator.SelectedAsset, i); + + validator.OnDestroy(); + } + Editor.SetValue(array); + } + else + { + foreach (var item in _dragItems.Objects) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = item, + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + list.Add(validator.SelectedItem); + else if (ElementType.Type == typeof(Guid)) + list.Add(validator.SelectedID); + else if (ElementType.Type == typeof(SceneReference)) + list.Add(new SceneReference(validator.SelectedID)); + else if (ElementType.Type == typeof(string)) + list.Add(validator.SelectedPath); + else + list.Add(validator.SelectedAsset); + + validator.OnDestroy(); + } + Editor.SetValue(list); + } + } + else if (_dragActors.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragActors.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var actor = _dragActors.Objects[i - oldSize].Actor; + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + array.SetValue(actor, i); + } + else + { + array.SetValue(actor.GetScript(ElementType.Type), i); + } + } + Editor.SetValue(array); + } + else + { + foreach (var actorNode in _dragActors.Objects) + { + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + list.Add(actorNode.Actor); + } + else + { + list.Add(actorNode.Actor.GetScript(ElementType.Type)); + } + } + Editor.SetValue(list); + } + } + + _dragHandlers.OnDragDrop(null); + } + + return result; + } + } } } diff --git a/Source/Editor/CustomEditors/Editors/GenericEditor.cs b/Source/Editor/CustomEditors/Editors/GenericEditor.cs index 55ac453a9..68c24a675 100644 --- a/Source/Editor/CustomEditors/Editors/GenericEditor.cs +++ b/Source/Editor/CustomEditors/Editors/GenericEditor.cs @@ -247,8 +247,9 @@ namespace FlaxEditor.CustomEditors.Editors /// The type. /// True if use type properties. /// True if use type fields. + /// True if use type properties that have only getter method without setter method (aka read-only). /// The items. - public static List GetItemsForType(ScriptType type, bool useProperties, bool useFields) + public static List GetItemsForType(ScriptType type, bool useProperties, bool useFields, bool usePropertiesWithoutSetter = false) { var items = new List(); @@ -264,7 +265,7 @@ namespace FlaxEditor.CustomEditors.Editors var showInEditor = attributes.Any(x => x is ShowInEditorAttribute); // Skip properties without getter or setter - if (!p.HasGet || (!p.HasSet && !showInEditor)) + if (!p.HasGet || (!p.HasSet && !showInEditor && !usePropertiesWithoutSetter)) continue; // Skip hidden fields, handle special attributes diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs index 9607680f2..f901b20d9 100644 --- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs @@ -28,14 +28,16 @@ namespace FlaxEditor.CustomEditors.Editors var group = layout.Group("Entry"); _group = group; - if (ParentEditor == null) + if (ParentEditor == null || HasDifferentTypes) return; var entry = (ModelInstanceEntry)Values[0]; var entryIndex = ParentEditor.ChildrenEditors.IndexOf(this); var materialLabel = new PropertyNameLabel("Material"); materialLabel.TooltipText = "The mesh surface material used for the rendering."; - if (ParentEditor.ParentEditor?.Values[0] is ModelInstanceActor modelInstance) + var parentEditorValues = ParentEditor.ParentEditor?.Values; + if (parentEditorValues?[0] is ModelInstanceActor modelInstance) { + // TODO: store _modelInstance and _material in array for each selected model instance actor _entryIndex = entryIndex; _modelInstance = modelInstance; var slots = modelInstance.MaterialSlots; @@ -56,6 +58,8 @@ namespace FlaxEditor.CustomEditors.Editors // Create material picker var materialValue = new CustomValueContainer(new ScriptType(typeof(MaterialBase)), _material, (instance, index) => _material, (instance, index, value) => _material = value as MaterialBase); + for (var i = 1; i < parentEditorValues.Count; i++) + materialValue.Add(_material); var materialEditor = (AssetRefEditor)_group.Property(materialLabel, materialValue); materialEditor.Values.SetDefaultValue(defaultValue); materialEditor.RefreshDefaultValue(); @@ -72,14 +76,14 @@ namespace FlaxEditor.CustomEditors.Editors return; _isRefreshing = true; var slots = _modelInstance.MaterialSlots; - var material = _materialEditor.Picker.SelectedAsset as MaterialBase; + var material = _materialEditor.Picker.Validator.SelectedAsset as MaterialBase; var defaultMaterial = GPUDevice.Instance.DefaultMaterial; var value = (ModelInstanceEntry)Values[0]; var prevMaterial = value.Material; if (!material) { // Fallback to default material - _materialEditor.Picker.SelectedAsset = defaultMaterial; + _materialEditor.Picker.Validator.SelectedAsset = defaultMaterial; value.Material = defaultMaterial; } else if (material == slots[_entryIndex].Material) diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 8d6b0f9e2..84f58daf1 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -5,6 +5,7 @@ using System.IO; using FlaxEditor.Content; using FlaxEditor.GUI.Drag; using FlaxEditor.Scripting; +using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -17,189 +18,21 @@ namespace FlaxEditor.GUI /// /// [HideInEditor] - public class AssetPicker : Control, IContentItemOwner + public class AssetPicker : Control { private const float DefaultIconSize = 64; private const float ButtonsOffset = 2; private const float ButtonsSize = 12; - private Asset _selected; - private ContentItem _selectedItem; - private ScriptType _type; - private string _fileExtension; - private bool _isMouseDown; private Float2 _mouseDownPos; private Float2 _mousePos; private DragItems _dragOverElement; /// - /// Gets or sets the selected item. + /// The asset validator. Used to ensure only appropriate items can be picked. /// - public ContentItem SelectedItem - { - get => _selectedItem; - set - { - if (_selectedItem == value) - return; - if (value == null) - { - if (_selected == null && _selectedItem is SceneItem) - { - // Deselect scene reference - _selectedItem.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - return; - } - - // Deselect - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - } - else if (value is SceneItem item) - { - if (_selectedItem == item) - return; - if (!IsValid(item)) - item = null; - - // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = null; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - else if (value is AssetItem assetItem) - { - SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); - } - else - { - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = value; - _selected = null; - OnSelectedItemChanged(); - } - } - } - - /// - /// Gets or sets the selected asset identifier. - /// - public Guid SelectedID - { - get - { - if (_selected != null) - return _selected.ID; - if (_selectedItem is AssetItem assetItem) - return assetItem.ID; - return Guid.Empty; - } - set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); - } - - /// - /// Gets or sets the selected content item path. - /// - public string SelectedPath - { - get - { - string path = _selectedItem?.Path ?? _selected?.Path; - if (path != null) - { - // Convert into path relative to the project (cross-platform) - var projectFolder = Globals.ProjectFolder; - if (path.StartsWith(projectFolder)) - path = path.Substring(projectFolder.Length + 1); - } - return path; - } - set - { - if (string.IsNullOrEmpty(value)) - { - SelectedItem = null; - } - else - { - var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value; - SelectedItem = Editor.Instance.ContentDatabase.Find(path); - } - } - } - - /// - /// Gets or sets the selected asset object. - /// - public Asset SelectedAsset - { - get => _selected; - set - { - // Check if value won't change - if (value == _selected) - return; - - // Find item from content database and check it - var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; - if (item != null && !IsValid(item)) - item = null; - - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = value; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - } - - /// - /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. - /// - public ScriptType AssetType - { - get => _type; - set - { - if (_type != value) - { - _type = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } - - /// - /// Gets or sets the content items extensions filter. Null if unused. - /// - public string FileExtension - { - get => _fileExtension; - set - { - if (_fileExtension != value) - { - _fileExtension = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } + public AssetPickerValidator Validator { get; } /// /// Occurs when selected item gets changed. @@ -216,38 +49,6 @@ namespace FlaxEditor.GUI /// public bool CanEdit = true; - private bool IsValid(ContentItem item) - { - if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) - return false; - if (CheckValid != null && !CheckValid(item)) - return false; - if (_type == ScriptType.Null) - return true; - - if (item is AssetItem assetItem) - { - // Faster path for binary items (in-built) - if (assetItem is BinaryAssetItem binaryItem) - return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); - - // Type filter - var type = TypeUtils.GetType(assetItem.TypeName); - if (_type.IsAssignableFrom(type)) - return true; - - // Json assets can contain any type of the object defined by the C# type (data oriented design) - if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) - return true; - - // Special case for scene asset references - if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) - return true; - } - - return false; - } - /// /// Initializes a new instance of the class. /// @@ -264,7 +65,8 @@ namespace FlaxEditor.GUI public AssetPicker(ScriptType assetType, Float2 location) : base(location, new Float2(DefaultIconSize + ButtonsOffset + ButtonsSize, DefaultIconSize)) { - _type = assetType; + Validator = new AssetPickerValidator(assetType); + Validator.SelectedItemChanged += OnSelectedItemChanged; _mousePos = Float2.Minimum; } @@ -275,10 +77,10 @@ namespace FlaxEditor.GUI { // Update tooltip string tooltip; - if (_selectedItem is AssetItem assetItem) + if (Validator.SelectedItem is AssetItem assetItem) tooltip = assetItem.NamePath; else - tooltip = SelectedPath; + tooltip = Validator.SelectedPath; TooltipText = tooltip; SelectedItemChanged?.Invoke(); @@ -289,37 +91,13 @@ namespace FlaxEditor.GUI // Do the drag drop operation if has selected element if (new Rectangle(Float2.Zero, Size).Contains(ref _mouseDownPos)) { - if (_selected != null) - DoDragDrop(DragAssets.GetDragData(_selected)); - else if (_selectedItem != null) - DoDragDrop(DragItems.GetDragData(_selectedItem)); + if (Validator.SelectedAsset != null) + DoDragDrop(DragAssets.GetDragData(Validator.SelectedAsset)); + else if (Validator.SelectedItem != null) + DoDragDrop(DragItems.GetDragData(Validator.SelectedItem)); } } - /// - public void OnItemDeleted(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - - /// - public void OnItemRenamed(ContentItem item) - { - } - - /// - public void OnItemReimported(ContentItem item) - { - } - - /// - public void OnItemDispose(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - private Rectangle IconRect => new Rectangle(0, 0, Height, Height); private Rectangle Button1Rect => new Rectangle(Height + ButtonsOffset, 0, ButtonsSize, ButtonsSize); @@ -341,10 +119,10 @@ namespace FlaxEditor.GUI if (CanEdit) Render2D.DrawSprite(style.ArrowDown, button1Rect, button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { // Draw item preview - _selectedItem.DrawThumbnail(ref iconRect); + Validator.SelectedItem.DrawThumbnail(ref iconRect); // Draw buttons if (CanEdit) @@ -363,7 +141,7 @@ namespace FlaxEditor.GUI { Render2D.DrawText( style.FontSmall, - _selectedItem.ShortName, + Validator.SelectedItem.ShortName, new Rectangle(button1Rect.Right + 2, 0, sizeForTextLeft, ButtonsSize), style.Foreground, TextAlignment.Near, @@ -371,7 +149,7 @@ namespace FlaxEditor.GUI } } // Check if has no item but has an asset (eg. virtual asset) - else if (_selected) + else if (Validator.SelectedAsset) { // Draw remove button Render2D.DrawSprite(style.Cross, button3Rect, button3Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); @@ -380,8 +158,8 @@ namespace FlaxEditor.GUI float sizeForTextLeft = Width - button1Rect.Right; if (sizeForTextLeft > 30) { - var name = _selected.GetType().Name; - if (_selected.IsVirtual) + var name = Validator.SelectedAsset.GetType().Name; + if (Validator.SelectedAsset.IsVirtual) name += " (virtual)"; Render2D.DrawText( style.FontSmall, @@ -395,8 +173,8 @@ namespace FlaxEditor.GUI else { // No element selected - Render2D.FillRectangle(iconRect, new Color(0.2f)); - Render2D.DrawText(style.FontMedium, "No asset\nselected", iconRect, Color.Wheat, TextAlignment.Center, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, Height / DefaultIconSize); + Render2D.FillRectangle(iconRect, style.BackgroundNormal); + Render2D.DrawText(style.FontMedium, "No asset\nselected", iconRect, Color.Orange, TextAlignment.Center, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, Height / DefaultIconSize); } // Check if drag is over @@ -407,9 +185,7 @@ namespace FlaxEditor.GUI /// public override void OnDestroy() { - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; + Validator.OnDestroy(); base.OnDestroy(); } @@ -463,57 +239,57 @@ namespace FlaxEditor.GUI // Buttons logic if (!CanEdit) { - if (Button1Rect.Contains(location) && _selectedItem != null) + if (Button1Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } } else if (Button1Rect.Contains(location)) { Focus(); - if (_type != ScriptType.Null) + if (Validator.AssetType != ScriptType.Null) { // Show asset picker popup - var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selected != null) + if (Validator.SelectedAsset != null) { - var selectedAssetName = Path.GetFileNameWithoutExtension(_selected.Path); + var selectedAssetName = Path.GetFileNameWithoutExtension(Validator.SelectedAsset.Path); popup.ScrollToAndHighlightItemByName(selectedAssetName); } } else { // Show content item picker popup - var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { - popup.ScrollToAndHighlightItemByName(_selectedItem.ShortName); + popup.ScrollToAndHighlightItemByName(Validator.SelectedItem.ShortName); } } } - else if (_selected != null || _selectedItem != null) + else if (Validator.SelectedAsset != null || Validator.SelectedItem != null) { - if (Button2Rect.Contains(location) && _selectedItem != null) + if (Button2Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } else if (Button3Rect.Contains(location)) { // Deselect asset Focus(); - SelectedItem = null; + Validator.SelectedItem = null; } } } @@ -540,10 +316,10 @@ namespace FlaxEditor.GUI { Focus(); - if (_selectedItem != null && IconRect.Contains(location)) + if (Validator.SelectedItem != null && IconRect.Contains(location)) { // Open it - Editor.Instance.ContentEditing.Open(_selectedItem); + Editor.Instance.ContentEditing.Open(Validator.SelectedItem); } // Handled @@ -557,7 +333,7 @@ namespace FlaxEditor.GUI // Check if drop asset if (_dragOverElement == null) - _dragOverElement = new DragItems(IsValid); + _dragOverElement = new DragItems(Validator.IsValid); if (CanEdit && _dragOverElement.OnDragEnter(data)) { } @@ -590,7 +366,7 @@ namespace FlaxEditor.GUI if (CanEdit && _dragOverElement.HasValidDrag) { // Select element - SelectedItem = _dragOverElement.Objects[0]; + Validator.SelectedItem = _dragOverElement.Objects[0]; } // Clear cache diff --git a/Source/Editor/GUI/Dialogs/Dialog.cs b/Source/Editor/GUI/Dialogs/Dialog.cs index 5054aee98..86e24e3b3 100644 --- a/Source/Editor/GUI/Dialogs/Dialog.cs +++ b/Source/Editor/GUI/Dialogs/Dialog.cs @@ -39,6 +39,11 @@ namespace FlaxEditor.GUI.Dialogs /// public DialogResult Result => _result; + /// + /// Returns the size of the dialog. + /// + public Float2 DialogSize => _dialogSize; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs index 52c5dcd3c..6e2353441 100644 --- a/Source/Editor/GUI/Docking/DockHintWindow.cs +++ b/Source/Editor/GUI/Docking/DockHintWindow.cs @@ -44,11 +44,11 @@ namespace FlaxEditor.GUI.Docking var mousePos = window.MousePosition; var previousSize = window.Size; window.Restore(); - window.Position = FlaxEngine.Input.MouseScreenPosition - mousePos * window.Size / previousSize; + window.Position = Platform.MousePosition - mousePos * window.Size / previousSize; } // Calculate dragging offset and move window to the destination position - var mouseScreenPosition = FlaxEngine.Input.MouseScreenPosition; + var mouseScreenPosition = Platform.MousePosition; // If the _toMove window was not focused when initializing this window, the result vector only contains zeros // and to prevent a failure, we need to perform an update for the drag offset at later time which will be done in the OnMouseMove event handler. @@ -83,6 +83,7 @@ namespace FlaxEditor.GUI.Docking // Enable hit window presentation Proxy.Window.RenderingEnabled = true; Proxy.Window.Show(); + Proxy.Window.Focus(); } /// @@ -113,7 +114,7 @@ namespace FlaxEditor.GUI.Docking var window = _toMove.Window?.Window; if (window == null) return; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; // Move base window window.Position = mouse - _dragOffset; @@ -193,7 +194,7 @@ namespace FlaxEditor.GUI.Docking // Move window to the mouse position (with some offset for caption bar) var window = (WindowRootControl)toMove.Root; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; window.Window.Position = mouse - new Float2(8, 8); // Get floating panel @@ -244,7 +245,7 @@ namespace FlaxEditor.GUI.Docking private void UpdateRects() { // Cache mouse position - _mouse = FlaxEngine.Input.MouseScreenPosition; + _mouse = Platform.MousePosition; // Check intersection with any dock panel var uiMouse = _mouse; @@ -270,15 +271,16 @@ namespace FlaxEditor.GUI.Docking // Cache dock rectangles var size = _rectDock.Size; var offset = _rectDock.Location; - float BorderMargin = 4.0f; - float ProxyHintWindowsSize2 = Proxy.HintWindowsSize * 0.5f; - float centerX = size.X * 0.5f; - float centerY = size.Y * 0.5f; - _rUpper = new Rectangle(centerX - ProxyHintWindowsSize2, BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rBottom = new Rectangle(centerX - ProxyHintWindowsSize2, size.Y - Proxy.HintWindowsSize - BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rLeft = new Rectangle(BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rRight = new Rectangle(size.X - Proxy.HintWindowsSize - BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rCenter = new Rectangle(centerX - ProxyHintWindowsSize2, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; + var borderMargin = 4.0f; + var hintWindowsSize = Proxy.HintWindowsSize * Platform.DpiScale; + var hintWindowsSize2 = hintWindowsSize * 0.5f; + var centerX = size.X * 0.5f; + var centerY = size.Y * 0.5f; + _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; // Hit test DockState toSet = DockState.Float; @@ -428,7 +430,6 @@ namespace FlaxEditor.GUI.Docking { if (Window == null) { - // Create proxy window var settings = CreateWindowSettings.Default; settings.Title = "DockHint.Window"; settings.Size = initSize; @@ -440,12 +441,10 @@ namespace FlaxEditor.GUI.Docking settings.IsRegularWindow = false; settings.SupportsTransparency = true; settings.ShowInTaskbar = false; - settings.ShowAfterFirstPaint = true; + settings.ShowAfterFirstPaint = false; settings.IsTopmost = true; Window = Platform.CreateWindow(ref settings); - - // Set opacity and background color Window.Opacity = 0.6f; Window.GUI.BackgroundColor = Style.Current.DragWindow; } @@ -465,7 +464,7 @@ namespace FlaxEditor.GUI.Docking var settings = CreateWindowSettings.Default; settings.Title = name; - settings.Size = new Float2(HintWindowsSize); + settings.Size = new Float2(HintWindowsSize * Platform.DpiScale); settings.AllowInput = false; settings.AllowMaximize = false; settings.AllowMinimize = false; @@ -479,7 +478,6 @@ namespace FlaxEditor.GUI.Docking settings.ShowAfterFirstPaint = false; win = Platform.CreateWindow(ref settings); - win.Opacity = 0.6f; win.GUI.BackgroundColor = Style.Current.DragWindow; } diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs index e544285a5..b04aad08c 100644 --- a/Source/Editor/GUI/Docking/DockPanel.cs +++ b/Source/Editor/GUI/Docking/DockPanel.cs @@ -465,36 +465,47 @@ namespace FlaxEditor.GUI.Docking { if (Parent.Parent is SplitPanel splitter) { - // Check if has any child panels - var childPanel = new List(_childPanels); - for (int i = 0; i < childPanel.Count; i++) + // Check if there is another nested dock panel inside this dock panel and extract it here + var childPanels = _childPanels.ToArray(); + if (childPanels.Length != 0) { - // Undock all tabs - var panel = childPanel[i]; - int count = panel.TabsCount; - while (count-- > 0) + // Move tabs from child panels into this one + DockWindow selectedTab = null; + foreach (var childPanel in childPanels) { - panel.GetTab(0).Close(); + var childPanelTabs = childPanel.Tabs.ToArray(); + for (var i = 0; i < childPanelTabs.Length; i++) + { + var childPanelTab = childPanelTabs[i]; + if (selectedTab == null && childPanelTab.IsSelected) + selectedTab = childPanelTab; + childPanel.UndockWindow(childPanelTab); + AddTab(childPanelTab, false); + } } + if (selectedTab != null) + SelectTab(selectedTab); } - - // Unlink splitter - var splitterParent = splitter.Parent; - Assert.IsNotNull(splitterParent); - splitter.Parent = null; - - // Move controls from second split panel to the split panel parent - var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; - var srcPanelChildrenCount = scrPanel.ChildrenCount; - for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + else { - scrPanel.GetChild(i).Parent = splitterParent; - } - Assert.IsTrue(scrPanel.ChildrenCount == 0); - Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + // Unlink splitter + var splitterParent = splitter.Parent; + Assert.IsNotNull(splitterParent); + splitter.Parent = null; - // Delete - splitter.Dispose(); + // Move controls from second split panel to the split panel parent + var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; + var srcPanelChildrenCount = scrPanel.ChildrenCount; + for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + { + scrPanel.GetChild(i).Parent = splitterParent; + } + Assert.IsTrue(scrPanel.ChildrenCount == 0); + Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + + // Delete + splitter.Dispose(); + } } else if (!IsMaster) { @@ -582,19 +593,17 @@ namespace FlaxEditor.GUI.Docking /// Adds the tab. /// /// The window to insert as a tab. - protected virtual void AddTab(DockWindow window) + /// True if auto-select newly added tab. + protected virtual void AddTab(DockWindow window, bool autoSelect = true) { - // Dock _tabs.Add(window); window.ParentDockPanel = this; - - // Select tab - SelectTab(window); + if (autoSelect) + SelectTab(window); } private void CreateTabsProxy() { - // Check if has no tabs proxy created if (_tabsProxy == null) { // Create proxy and make set simple full dock diff --git a/Source/Editor/GUI/Docking/DockPanelProxy.cs b/Source/Editor/GUI/Docking/DockPanelProxy.cs index 15ff2cad0..e6e57de8e 100644 --- a/Source/Editor/GUI/Docking/DockPanelProxy.cs +++ b/Source/Editor/GUI/Docking/DockPanelProxy.cs @@ -13,6 +13,7 @@ namespace FlaxEditor.GUI.Docking public class DockPanelProxy : ContainerControl { private DockPanel _panel; + private double _dragEnterTime = -1; /// /// The is mouse down flag (left button). @@ -256,8 +257,8 @@ namespace FlaxEditor.GUI.Docking else { tabColor = style.BackgroundHighlighted; - Render2D.DrawLine(tabRect.BottomLeft - new Float2(0 , 1), tabRect.UpperLeft, tabColor); - Render2D.DrawLine(tabRect.BottomRight - new Float2(0 , 1), tabRect.UpperRight, tabColor); + Render2D.DrawLine(tabRect.BottomLeft - new Float2(0, 1), tabRect.UpperLeft, tabColor); + Render2D.DrawLine(tabRect.BottomRight - new Float2(0, 1), tabRect.UpperRight, tabColor); } if (tab.Icon.IsValid) @@ -477,11 +478,7 @@ namespace FlaxEditor.GUI.Docking var result = base.OnDragEnter(ref location, data); if (result != DragDropEffect.None) return result; - - if (TrySelectTabUnderLocation(ref location)) - return DragDropEffect.Move; - - return DragDropEffect.None; + return TrySelectTabUnderLocation(ref location); } /// @@ -490,11 +487,15 @@ namespace FlaxEditor.GUI.Docking var result = base.OnDragMove(ref location, data); if (result != DragDropEffect.None) return result; + return TrySelectTabUnderLocation(ref location); + } - if (TrySelectTabUnderLocation(ref location)) - return DragDropEffect.Move; + /// + public override void OnDragLeave() + { + _dragEnterTime = -1; - return DragDropEffect.None; + base.OnDragLeave(); } /// @@ -503,17 +504,25 @@ namespace FlaxEditor.GUI.Docking rect = new Rectangle(0, DockPanel.DefaultHeaderHeight, Width, Height - DockPanel.DefaultHeaderHeight); } - private bool TrySelectTabUnderLocation(ref Float2 location) + private DragDropEffect TrySelectTabUnderLocation(ref Float2 location) { var tab = GetTabAtPos(location, out _); if (tab != null) { + // Auto-select tab only if drag takes some time + var time = Platform.TimeSeconds; + if (_dragEnterTime < 0) + _dragEnterTime = time; + if (time - _dragEnterTime < 0.3f) + return DragDropEffect.Link; + _dragEnterTime = -1; + _panel.SelectTab(tab); Update(0); // Fake update - return true; + return DragDropEffect.Move; } - - return false; + _dragEnterTime = -1; + return DragDropEffect.None; } private void ShowContextMenu(DockWindow tab, ref Float2 location) diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index f8ffe62ad..7bb85751a 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -72,7 +72,7 @@ namespace FlaxEditor.GUI.Docking settings.Size = size; settings.Position = location; settings.MinimumSize = new Float2(1); - settings.MaximumSize = new Float2(4096); + settings.MaximumSize = Float2.Zero; // Unlimited size settings.Fullscreen = false; settings.HasBorder = true; settings.SupportsTransparency = false; diff --git a/Source/Editor/GUI/Input/ColorValueBox.cs b/Source/Editor/GUI/Input/ColorValueBox.cs index 167cc65bb..fdd7d073a 100644 --- a/Source/Editor/GUI/Input/ColorValueBox.cs +++ b/Source/Editor/GUI/Input/ColorValueBox.cs @@ -14,6 +14,8 @@ namespace FlaxEditor.GUI.Input [HideInEditor] public class ColorValueBox : Control { + private bool _isMouseDown; + /// /// Delegate function used for the color picker events handling. /// @@ -134,11 +136,22 @@ namespace FlaxEditor.GUI.Input Render2D.DrawRectangle(r, IsMouseOver || IsNavFocused ? style.BackgroundSelected : Color.Black); } + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + _isMouseDown = true; + return base.OnMouseDown(location, button); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { - Focus(); - OnSubmit(); + if (_isMouseDown) + { + _isMouseDown = false; + Focus(); + OnSubmit(); + } return true; } diff --git a/Source/Editor/GUI/PlatformSelector.cs b/Source/Editor/GUI/PlatformSelector.cs index 1e03a9677..a29f62805 100644 --- a/Source/Editor/GUI/PlatformSelector.cs +++ b/Source/Editor/GUI/PlatformSelector.cs @@ -100,9 +100,10 @@ namespace FlaxEditor.GUI AutoResize = true; Offsets = new Margin(0, 0, 0, IconSize); - _mouseOverColor = style.Foreground; - _selectedColor = style.Foreground; - _defaultColor = style.ForegroundGrey; + // Ignoring style on purpose (style would make sense if the icons were white, but they are colored) + _mouseOverColor = new Color(0.8f, 0.8f, 0.8f, 1f); + _selectedColor = Color.White; + _defaultColor = new Color(0.7f, 0.7f, 0.7f, 0.5f); for (int i = 0; i < platforms.Length; i++) { diff --git a/Source/Editor/GUI/Row.cs b/Source/Editor/GUI/Row.cs index cb9cb09b2..f6bd5b02a 100644 --- a/Source/Editor/GUI/Row.cs +++ b/Source/Editor/GUI/Row.cs @@ -98,7 +98,7 @@ namespace FlaxEditor.GUI rect.Width -= leftDepthMargin; Render2D.PushClip(rect); - Render2D.DrawText(style.FontMedium, text, rect, Color.White, column.CellAlignment, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, text, rect, style.Foreground, column.CellAlignment, TextAlignment.Center); Render2D.PopClip(); x += width; diff --git a/Source/Editor/GUI/Tabs/Tab.cs b/Source/Editor/GUI/Tabs/Tab.cs index 3968cc17d..85f805af0 100644 --- a/Source/Editor/GUI/Tabs/Tab.cs +++ b/Source/Editor/GUI/Tabs/Tab.cs @@ -13,6 +13,8 @@ namespace FlaxEditor.GUI.Tabs [HideInEditor] public class Tab : ContainerControl { + internal Tabs _selectedInTabs; + /// /// Gets or sets the text. /// @@ -86,5 +88,25 @@ namespace FlaxEditor.GUI.Tabs { return new Tabs.TabHeader((Tabs)Parent, this); } + + /// + protected override void OnParentChangedInternal() + { + if (_selectedInTabs != null) + _selectedInTabs.SelectedTab = null; + + base.OnParentChangedInternal(); + } + + /// + public override void OnDestroy() + { + if (IsDisposing) + return; + if (_selectedInTabs != null) + _selectedInTabs.SelectedTab = null; + + base.OnDestroy(); + } } } diff --git a/Source/Editor/GUI/Tabs/Tabs.cs b/Source/Editor/GUI/Tabs/Tabs.cs index d830b9e0f..3c70363e7 100644 --- a/Source/Editor/GUI/Tabs/Tabs.cs +++ b/Source/Editor/GUI/Tabs/Tabs.cs @@ -239,7 +239,7 @@ namespace FlaxEditor.GUI.Tabs /// public Tab SelectedTab { - get => _selectedIndex == -1 && Children.Count > _selectedIndex + 1 ? null : Children[_selectedIndex + 1] as Tab; + get => _selectedIndex < 0 || Children.Count <= _selectedIndex ? null : Children[_selectedIndex + 1] as Tab; set => SelectedTabIndex = value != null ? Children.IndexOf(value) - 1 : -1; } @@ -263,7 +263,12 @@ namespace FlaxEditor.GUI.Tabs // Check if index will change if (_selectedIndex != index) { - SelectedTab?.OnDeselected(); + var prev = SelectedTab; + if (prev != null) + { + prev._selectedInTabs = null; + prev.OnDeselected(); + } _selectedIndex = index; PerformLayout(); OnSelectedTabChanged(); @@ -342,8 +347,13 @@ namespace FlaxEditor.GUI.Tabs /// protected virtual void OnSelectedTabChanged() { + var selectedTab = SelectedTab; SelectedTabChanged?.Invoke(this); - SelectedTab?.OnSelected(); + if (selectedTab != null) + { + selectedTab._selectedInTabs = this; + selectedTab.OnSelected(); + } } /// diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index a32b35692..6b4d7bf4c 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -627,10 +627,11 @@ namespace FlaxEditor.GUI.Timeline Parent = this }; + var style = Style.Current; var headerTopArea = new ContainerControl { AutoFocus = false, - BackgroundColor = Style.Current.LightBackground, + BackgroundColor = style.LightBackground, AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight), Parent = _splitter.Panel1 @@ -683,7 +684,7 @@ namespace FlaxEditor.GUI.Timeline { AutoFocus = false, ClipChildren = false, - BackgroundColor = Style.Current.LightBackground, + BackgroundColor = style.LightBackground, AnchorPreset = AnchorPresets.HorizontalStretchBottom, Offsets = new Margin(0, 0, -playbackButtonsSize, playbackButtonsSize), Parent = _splitter.Panel1 @@ -845,7 +846,7 @@ namespace FlaxEditor.GUI.Timeline _timeIntervalsHeader = new TimeIntervalsHeader(this) { AutoFocus = false, - BackgroundColor = Style.Current.Background.RGBMultiplied(0.9f), + BackgroundColor = style.Background.RGBMultiplied(0.9f), AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight), Parent = _splitter.Panel2 @@ -854,7 +855,7 @@ namespace FlaxEditor.GUI.Timeline { AutoFocus = false, ClipChildren = false, - BackgroundColor = Style.Current.Background.RGBMultiplied(0.7f), + BackgroundColor = style.Background.RGBMultiplied(0.7f), AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, HeaderTopAreaHeight, 0), Parent = _splitter.Panel2 diff --git a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs index b7c87cb01..3bbca15ef 100644 --- a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs @@ -39,7 +39,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks if (AssetID == value?.ID) return; AssetID = value?.ID ?? Guid.Empty; - _picker.SelectedAsset = value; + _picker.Validator.SelectedAsset = value; OnAssetChanged(); Timeline?.MarkAsEdited(); } @@ -63,10 +63,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks private void OnPickerSelectedItemChanged() { - if (Asset == (TAsset)_picker.SelectedAsset) + if (Asset == (TAsset)_picker.Validator.SelectedAsset) return; using (new TrackUndoBlock(this)) - Asset = (TAsset)_picker.SelectedAsset; + Asset = (TAsset)_picker.Validator.SelectedAsset; } /// diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index 7e3af05bf..703079469 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -776,11 +776,20 @@ namespace FlaxEditor.GUI.Tree // Check if mouse hits arrow if (_mouseOverArrow && HasAnyVisibleChild) { - // Toggle open state - if (_opened) - Collapse(); + if (ParentTree.Root.GetKey(KeyboardKeys.Alt)) + { + if (_opened) + CollapseAll(); + else + ExpandAll(); + } else - Expand(); + { + if (_opened) + Collapse(); + else + Expand(); + } } // Check if mouse hits bar diff --git a/Source/Editor/Gizmo/IGizmoOwner.cs b/Source/Editor/Gizmo/IGizmoOwner.cs index ff653196e..3ab0a917b 100644 --- a/Source/Editor/Gizmo/IGizmoOwner.cs +++ b/Source/Editor/Gizmo/IGizmoOwner.cs @@ -106,5 +106,11 @@ namespace FlaxEditor.Gizmo /// /// The nodes to select void Select(List nodes); + + /// + /// Spawns the actor in the viewport hierarchy. + /// + /// The new actor to spawn. + void Spawn(Actor actor); } } diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index ef7558b8d..4a3fa39ba 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -111,7 +111,8 @@ namespace FlaxEditor.Gizmo if (isSelected) { GetSelectedObjectsBounds(out var selectionBounds, out _); - ray.Position = ray.GetPoint(selectionBounds.Size.Y * 0.5f); + var offset = Mathf.Max(selectionBounds.Size.Y * 0.5f, 1.0f); + ray.Position = ray.GetPoint(offset); continue; } diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index e01fd04bb..969608676 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -513,7 +513,9 @@ DEFINE_INTERNAL_CALL(void) EditorInternal_RunVisualScriptBreakpointLoopTick(floa WindowsManager::WindowsLocker.Unlock(); } WindowsManager::WindowsLocker.Lock(); - for (auto& win : WindowsManager::Windows) + Array> windows; + windows.Add(WindowsManager::Windows); + for (Window* win : windows) { if (win->IsVisible()) win->OnUpdate(deltaTime); diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp index fd9219d35..bed742885 100644 --- a/Source/Editor/Managed/ManagedEditor.cpp +++ b/Source/Editor/Managed/ManagedEditor.cpp @@ -330,14 +330,15 @@ bool ManagedEditor::CanReloadScripts() bool ManagedEditor::CanAutoBuildCSG() { + if (!ManagedEditorOptions.AutoRebuildCSG) + return false; + // Skip calls from non-managed thread (eg. physics worker) if (!MCore::Thread::IsAttached()) return false; if (!HasManagedInstance()) return false; - if (!ManagedEditorOptions.AutoRebuildCSG) - return false; if (Internal_CanAutoBuildCSG == nullptr) { Internal_CanAutoBuildCSG = GetClass()->GetMethod("Internal_CanAutoBuildCSG"); @@ -348,14 +349,15 @@ bool ManagedEditor::CanAutoBuildCSG() bool ManagedEditor::CanAutoBuildNavMesh() { + if (!ManagedEditorOptions.AutoRebuildNavMesh) + return false; + // Skip calls from non-managed thread (eg. physics worker) if (!MCore::Thread::IsAttached()) return false; if (!HasManagedInstance()) return false; - if (!ManagedEditorOptions.AutoRebuildNavMesh) - return false; if (Internal_CanAutoBuildNavMesh == nullptr) { Internal_CanAutoBuildNavMesh = GetClass()->GetMethod("Internal_CanAutoBuildNavMesh"); diff --git a/Source/Editor/Modules/PrefabsModule.cs b/Source/Editor/Modules/PrefabsModule.cs index adb5f7685..6abf8e45c 100644 --- a/Source/Editor/Modules/PrefabsModule.cs +++ b/Source/Editor/Modules/PrefabsModule.cs @@ -124,6 +124,7 @@ namespace FlaxEditor.Modules if (!Editor.StateMachine.CurrentState.CanEditScene) return; undo = Editor.Undo; + Editor.Scene.MarkSceneEdited(actor.Scene); } // Record undo for prefab creating (backend links the target instance with the prefab) diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs index 7789eb7c4..954e3bd50 100644 --- a/Source/Editor/Modules/SceneModule.cs +++ b/Source/Editor/Modules/SceneModule.cs @@ -242,7 +242,6 @@ namespace FlaxEditor.Modules /// True if don't close opened scenes and just add new scene to them, otherwise will release current scenes and load single one. public void OpenScene(Guid sceneId, bool additive = false) { - // Check if cannot change scene now if (!Editor.StateMachine.CurrentState.CanChangeScene) return; @@ -266,13 +265,35 @@ namespace FlaxEditor.Modules Editor.StateMachine.ChangingScenesState.LoadScene(sceneId, additive); } + /// + /// Reload all loaded scenes. + /// + public void ReloadScenes() + { + if (!Editor.StateMachine.CurrentState.CanChangeScene) + return; + + if (!Editor.IsPlayMode) + { + if (CheckSaveBeforeClose()) + return; + } + + // Reload scenes + foreach (var scene in Level.Scenes) + { + var sceneId = scene.ID; + Level.UnloadScene(scene); + Level.LoadScene(sceneId); + } + } + /// /// Closes scene (async). /// /// The scene. public void CloseScene(Scene scene) { - // Check if cannot change scene now if (!Editor.StateMachine.CurrentState.CanChangeScene) return; @@ -296,7 +317,6 @@ namespace FlaxEditor.Modules /// public void CloseAllScenes() { - // Check if cannot change scene now if (!Editor.StateMachine.CurrentState.CanChangeScene) return; @@ -321,7 +341,6 @@ namespace FlaxEditor.Modules /// The scene to not close. public void CloseAllScenesExcept(Scene scene) { - // Check if cannot change scene now if (!Editor.StateMachine.CurrentState.CanChangeScene) return; diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index 225bad082..082439402 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -147,6 +147,17 @@ namespace FlaxEditor.Modules.SourceCodeEditing } if (key != null) xml.TryGetValue(key, out text); + + // Customize tooltips for properties to be more human-readable in UI + if (text != null && memberType.HasFlag(MemberTypes.Property) && text.StartsWith("Gets or sets ", StringComparison.Ordinal)) + { + text = text.Substring(13); + unsafe + { + fixed (char* e = text) + e[0] = char.ToUpper(e[0]); + } + } } } diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 1369ecd1e..3386d411c 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -39,6 +39,7 @@ namespace FlaxEditor.Modules ContextMenuSingleSelectGroup _numberOfClientsGroup = new ContextMenuSingleSelectGroup(); private ContextMenuButton _menuFileSaveScenes; + private ContextMenuButton _menuFileReloadScenes; private ContextMenuButton _menuFileCloseScenes; private ContextMenuButton _menuFileOpenScriptsProject; private ContextMenuButton _menuFileGenerateScriptsProjectFiles; @@ -470,13 +471,13 @@ namespace FlaxEditor.Modules // Place dialog nearby the target control var targetControlDesktopCenter = targetControl.PointToScreen(targetControl.Size * 0.5f); var desktopSize = Platform.GetMonitorBounds(targetControlDesktopCenter); - var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.Height * 0.5f); - var dialogEnd = pos + dialog.Size; + var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.DialogSize.Y * 0.5f); + var dialogEnd = pos + dialog.DialogSize; var desktopEnd = desktopSize.BottomRight - new Float2(10.0f); if (dialogEnd.X >= desktopEnd.X || dialogEnd.Y >= desktopEnd.Y) - pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.Width, dialog.Height); + pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.DialogSize.X, dialog.DialogSize.Y); var desktopBounds = Platform.VirtualDesktopBounds; - pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.Size); + pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.DialogSize); dialog.RootWindow.Window.Position = pos; // Register for context menu (prevent auto-closing context menu when selecting color) @@ -527,6 +528,7 @@ namespace FlaxEditor.Modules _menuFileSaveAll = cm.AddButton("Save All", inputOptions.Save, Editor.SaveAll); _menuFileSaveScenes = cm.AddButton("Save scenes", inputOptions.SaveScenes, Editor.Scene.SaveScenes); _menuFileCloseScenes = cm.AddButton("Close scenes", inputOptions.CloseScenes, Editor.Scene.CloseAllScenes); + _menuFileReloadScenes = cm.AddButton("Reload scenes", Editor.Scene.ReloadScenes); cm.AddSeparator(); _menuFileOpenScriptsProject = cm.AddButton("Open scripts project", inputOptions.OpenScriptsProject, Editor.CodeEditing.OpenSolution); _menuFileGenerateScriptsProjectFiles = cm.AddButton("Generate scripts project files", inputOptions.GenerateScriptsProject, Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync); @@ -830,6 +832,7 @@ namespace FlaxEditor.Modules _menuFileSaveScenes.Enabled = hasOpenedScene; _menuFileCloseScenes.Enabled = hasOpenedScene; + _menuFileReloadScenes.Enabled = hasOpenedScene; _menuFileGenerateScriptsProjectFiles.Enabled = !Editor.ProgressReporting.GenerateScriptsProjectFiles.IsActive; c.PerformLayout(); diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 1a4f5ce9e..cda83b56e 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -171,9 +171,13 @@ namespace FlaxEditor.Modules var mainWindow = MainWindow; if (mainWindow) { - var projectPath = Globals.ProjectFolder.Replace('/', '\\'); - var platformBit = Platform.Is64BitApp ? "64" : "32"; - var title = string.Format("Flax Editor - \'{0}\' ({1}-bit)", projectPath, platformBit); + var projectPath = Globals.ProjectFolder; +#if PLATFORM_WINDOWS + projectPath = projectPath.Replace('/', '\\'); +#endif + var engineVersion = Editor.EngineProject.Version; + var engineVersionText = engineVersion.Revision > 0 ? $"{engineVersion.Major}.{engineVersion.Minor}.{engineVersion.Revision}" : $"{engineVersion.Major}.{engineVersion.Minor}"; + var title = $"Flax Editor {engineVersionText} - \'{projectPath}\'"; mainWindow.Title = title; } } @@ -237,7 +241,11 @@ namespace FlaxEditor.Modules /// public void LoadDefaultLayout() { - LoadLayout(StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml")); + var path = StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml"); + if (File.Exists(path)) + { + LoadLayout(path); + } } /// @@ -731,7 +739,6 @@ namespace FlaxEditor.Modules settings.Size = Platform.DesktopSize * 0.75f; settings.StartPosition = WindowStartPosition.CenterScreen; settings.ShowAfterFirstPaint = true; - #if PLATFORM_WINDOWS if (!Editor.Instance.Options.Options.Interface.UseNativeWindowSystem) { @@ -743,12 +750,9 @@ namespace FlaxEditor.Modules #elif PLATFORM_LINUX settings.HasBorder = false; #endif - MainWindow = Platform.CreateWindow(ref settings); - if (MainWindow == null) { - // Error Editor.LogError("Failed to create editor main window!"); return; } diff --git a/Source/Editor/Options/InputBinding.cs b/Source/Editor/Options/InputBinding.cs index eb61c0f68..95c3c1d6f 100644 --- a/Source/Editor/Options/InputBinding.cs +++ b/Source/Editor/Options/InputBinding.cs @@ -259,10 +259,7 @@ namespace FlaxEditor.Options public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } - return base.CanConvertFrom(context, sourceType); } @@ -270,9 +267,7 @@ namespace FlaxEditor.Options public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } @@ -284,7 +279,6 @@ namespace FlaxEditor.Options InputBinding.TryParse(str, out var result); return result; } - return base.ConvertFrom(context, culture, value); } @@ -295,7 +289,6 @@ namespace FlaxEditor.Options { return ((InputBinding)value).ToString(); } - return base.ConvertTo(context, culture, value, destinationType); } } diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index 90d098bb6..e2e7d0e71 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -76,6 +76,10 @@ namespace FlaxEditor.Options [EditorDisplay("Common"), EditorOrder(230)] public InputBinding RotateSelection = new InputBinding(KeyboardKeys.R); + [DefaultValue(typeof(InputBinding), "F11")] + [EditorDisplay("Common"), EditorOrder(240)] + public InputBinding ToggleFullscreen = new InputBinding(KeyboardKeys.F11); + #endregion #region File @@ -208,16 +212,20 @@ namespace FlaxEditor.Options [EditorDisplay("Debugger", "Continue"), EditorOrder(810)] public InputBinding DebuggerContinue = new InputBinding(KeyboardKeys.F5); + [DefaultValue(typeof(InputBinding), "Shift+F11")] + [EditorDisplay("Debugger", "Unlock mouse in Play Mode"), EditorOrder(820)] + public InputBinding DebuggerUnlockMouse = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift); + [DefaultValue(typeof(InputBinding), "F10")] - [EditorDisplay("Debugger", "Step Over"), EditorOrder(820)] + [EditorDisplay("Debugger", "Step Over"), EditorOrder(830)] public InputBinding DebuggerStepOver = new InputBinding(KeyboardKeys.F10); [DefaultValue(typeof(InputBinding), "F11")] - [EditorDisplay("Debugger", "Step Into"), EditorOrder(830)] + [EditorDisplay("Debugger", "Step Into"), EditorOrder(840)] public InputBinding DebuggerStepInto = new InputBinding(KeyboardKeys.F11); [DefaultValue(typeof(InputBinding), "Shift+F11")] - [EditorDisplay("Debugger", "Step Out"), EditorOrder(840)] + [EditorDisplay("Debugger", "Step Out"), EditorOrder(850)] public InputBinding DebuggerStepOut = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift); #endregion diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs index 07e899c5e..1137e4c37 100644 --- a/Source/Editor/Options/OptionsModule.cs +++ b/Source/Editor/Options/OptionsModule.cs @@ -208,13 +208,20 @@ namespace FlaxEditor.Options // If a non-default style was chosen, switch to that style string styleName = themeOptions.SelectedStyle; - if (styleName != "Default" && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null) + if (styleName != ThemeOptions.DefaultName && styleName != ThemeOptions.LightDefault && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null) { Style.Current = style; } else { - Style.Current = CreateDefaultStyle(); + if (styleName == ThemeOptions.LightDefault) + { + Style.Current = CreateLightStyle(); + } + else + { + Style.Current = CreateDefaultStyle(); + } } } @@ -224,7 +231,6 @@ namespace FlaxEditor.Options /// The style object. public Style CreateDefaultStyle() { - // Metro Style colors var options = Options; var style = new Style { @@ -233,6 +239,7 @@ namespace FlaxEditor.Options Foreground = Color.FromBgra(0xFFFFFFFF), ForegroundGrey = Color.FromBgra(0xFFA9A9B3), ForegroundDisabled = Color.FromBgra(0xFF787883), + ForegroundViewport = Color.FromBgra(0xFFFFFFFF), BackgroundHighlighted = Color.FromBgra(0xFF54545C), BorderHighlighted = Color.FromBgra(0xFF6A6A75), BackgroundSelected = Color.FromBgra(0xFF007ACC), @@ -274,7 +281,58 @@ namespace FlaxEditor.Options SharedTooltip = new Tooltip(), }; style.DragWindow = style.BackgroundSelected * 0.7f; + return style; + } + /// + /// Creates the light style (2nd default). + /// + /// The style object. + public Style CreateLightStyle() + { + var options = Options; + var style = new Style + { + Background = new Color(0.92f, 0.92f, 0.92f, 1f), + LightBackground = new Color(0.84f, 0.84f, 0.88f, 1f), + DragWindow = new Color(0.0f, 0.26f, 0.43f, 0.70f), + Foreground = new Color(0.0f, 0.0f, 0.0f, 1f), + ForegroundGrey = new Color(0.30f, 0.30f, 0.31f, 1f), + ForegroundDisabled = new Color(0.45f, 0.45f, 0.49f, 1f), + ForegroundViewport = new Color(1.0f, 1.0f, 1.0f, 1f), + BackgroundHighlighted = new Color(0.59f, 0.59f, 0.64f, 1f), + BorderHighlighted = new Color(0.50f, 0.50f, 0.55f, 1f), + BackgroundSelected = new Color(0.00f, 0.46f, 0.78f, 0.78f), + BorderSelected = new Color(0.11f, 0.57f, 0.88f, 0.65f), + BackgroundNormal = new Color(0.67f, 0.67f, 0.75f, 1f), + BorderNormal = new Color(0.59f, 0.59f, 0.64f, 1f), + TextBoxBackground = new Color(0.75f, 0.75f, 0.81f, 1f), + TextBoxBackgroundSelected = new Color(0.73f, 0.73f, 0.80f, 1f), + CollectionBackgroundColor = new Color(0.85f, 0.85f, 0.88f, 1f), + ProgressNormal = new Color(0.03f, 0.65f, 0.12f, 1f), + + // Fonts + FontTitle = options.Interface.TitleFont.GetFont(), + FontLarge = options.Interface.LargeFont.GetFont(), + FontMedium = options.Interface.MediumFont.GetFont(), + FontSmall = options.Interface.SmallFont.GetFont(), + + // Icons + ArrowDown = Editor.Icons.ArrowDown12, + ArrowRight = Editor.Icons.ArrowRight12, + Search = Editor.Icons.Search12, + Settings = Editor.Icons.Settings12, + Cross = Editor.Icons.Cross12, + CheckBoxIntermediate = Editor.Icons.CheckBoxIntermediate12, + CheckBoxTick = Editor.Icons.CheckBoxTick12, + StatusBarSizeGrip = Editor.Icons.WindowDrag12, + Translate = Editor.Icons.Translate32, + Rotate = Editor.Icons.Rotate32, + Scale = Editor.Icons.Scale32, + Scalar = Editor.Icons.Scalar32, + + SharedTooltip = new Tooltip(), + }; return style; } diff --git a/Source/Editor/Options/ThemeOptions.cs b/Source/Editor/Options/ThemeOptions.cs index 243918939..674e281af 100644 --- a/Source/Editor/Options/ThemeOptions.cs +++ b/Source/Editor/Options/ThemeOptions.cs @@ -15,6 +15,9 @@ namespace FlaxEditor.Options [CustomEditor(typeof(ThemeOptionsEditor))] public sealed class ThemeOptions { + internal const string DefaultName = "Default"; + internal const string LightDefault = "LightDefault"; + internal class ThemeOptionsEditor : Editor { private LabelElement _infoLabel; @@ -63,13 +66,14 @@ namespace FlaxEditor.Options private void ReloadOptions(ComboBox obj) { var themeOptions = (ThemeOptions)ParentEditor.Values[0]; - var options = new string[themeOptions.Styles.Count + 1]; - options[0] = "Default"; + var options = new string[themeOptions.Styles.Count + 2]; + options[0] = DefaultName; + options[1] = LightDefault; int i = 0; foreach (var styleName in themeOptions.Styles.Keys) { - options[i + 1] = styleName; + options[i + 2] = styleName; i++; } _combobox.ComboBox.SetItems(options); diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index cee63a562..0fd018cbc 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -26,45 +26,108 @@ namespace FlaxEditor.Options public float MouseWheelSensitivity { get; set; } = 1.0f; /// - /// Gets or sets the default movement speed for the viewport camera (must match the dropdown menu values in the viewport). + /// Gets or sets the total amount of steps the camera needs to go from minimum to maximum speed. /// - [DefaultValue(1.0f), Limit(0.01f, 100.0f)] - [EditorDisplay("Defaults"), EditorOrder(110), Tooltip("The default movement speed for the viewport camera (must match the dropdown menu values in the viewport).")] - public float DefaultMovementSpeed { get; set; } = 1.0f; + [DefaultValue(64), Limit(1, 128)] + [EditorDisplay("Camera"), EditorOrder(110), Tooltip("The total amount of steps the camera needs to go from minimum to maximum speed.")] + public int TotalCameraSpeedSteps { get; set; } = 64; + + /// + /// Gets or sets the degree to which the camera will be eased when using camera flight in the editor window. + /// + [DefaultValue(3.0f), Limit(1.0f, 8.0f)] + [EditorDisplay("Camera"), EditorOrder(111), Tooltip("The degree to which the camera will be eased when using camera flight in the editor window (ignored if camera easing degree is enabled).")] + public float CameraEasingDegree { get; set; } = 3.0f; + + /// + /// Gets or sets the default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values). + /// + [DefaultValue(1.0f), Limit(0.05f, 32.0f)] + [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).")] + public float MovementSpeed { get; set; } = 1.0f; + + /// + /// Gets or sets the default minimum camera movement speed. + /// + [DefaultValue(0.05f), Limit(0.05f, 32.0f)] + [EditorDisplay("Defaults"), EditorOrder(121), Tooltip("The default minimum movement speed for the viewport camera.")] + public float MinMovementSpeed { get; set; } = 0.05f; + + /// + /// Gets or sets the default maximum camera movement speed. + /// + [DefaultValue(32.0f), Limit(16.0f, 1000.0f)] + [EditorDisplay("Defaults"), EditorOrder(122), Tooltip("The default maximum movement speed for the viewport camera.")] + public float MaxMovementSpeed { get; set; } = 32f; + + /// + /// Gets or sets the default camera easing mode. + /// + [DefaultValue(true)] + [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default camera easing mode.")] + public bool UseCameraEasing { get; set; } = true; /// /// Gets or sets the default near clipping plane distance for the viewport camera. /// [DefaultValue(10.0f), Limit(0.001f, 1000.0f)] - [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default near clipping plane distance for the viewport camera.")] - public float DefaultNearPlane { get; set; } = 10.0f; + [EditorDisplay("Defaults"), EditorOrder(140), Tooltip("The default near clipping plane distance for the viewport camera.")] + public float NearPlane { get; set; } = 10.0f; /// /// Gets or sets the default far clipping plane distance for the viewport camera. /// [DefaultValue(40000.0f), Limit(10.0f)] - [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default far clipping plane distance for the viewport camera.")] - public float DefaultFarPlane { get; set; } = 40000.0f; + [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("The default far clipping plane distance for the viewport camera.")] + public float FarPlane { get; set; } = 40000.0f; /// /// Gets or sets the default field of view angle (in degrees) for the viewport camera. /// [DefaultValue(60.0f), Limit(35.0f, 160.0f, 0.1f)] - [EditorDisplay("Defaults", "Default Field Of View"), EditorOrder(140), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] - public float DefaultFieldOfView { get; set; } = 60.0f; + [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] + public float FieldOfView { get; set; } = 60.0f; /// - /// Gets or sets if the panning direction is inverted for the viewport camera. + /// Gets or sets the default camera orthographic mode. /// [DefaultValue(false)] - [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("Invert the panning direction for the viewport camera.")] - public bool DefaultInvertPanning { get; set; } = false; + [EditorDisplay("Defaults"), EditorOrder(170), Tooltip("The default camera orthographic mode.")] + public bool UseOrthographicProjection { get; set; } = false; /// - /// Scales editor viewport grid. + /// Gets or sets the default camera orthographic scale (if camera uses orthographic mode). + /// + [DefaultValue(5.0f), Limit(0.001f, 100000.0f, 0.1f)] + [EditorDisplay("Defaults"), EditorOrder(180), Tooltip("The default camera orthographic scale (if camera uses orthographic mode).")] + public float OrthographicScale { get; set; } = 5.0f; + + /// + /// Gets or sets the default panning direction for the viewport camera. + /// + [DefaultValue(false)] + [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default panning direction for the viewport camera.")] + public bool InvertPanning { get; set; } = false; + + /// + /// Gets or sets the default relative panning mode. + /// + [DefaultValue(true)] + [EditorDisplay("Defaults"), EditorOrder(200), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")] + public bool UseRelativePanning { get; set; } = true; + + /// + /// Gets or sets the default panning speed (ignored if relative panning is speed enabled). + /// + [DefaultValue(0.8f), Limit(0.01f, 128.0f, 0.1f)] + [EditorDisplay("Defaults"), EditorOrder(210), Tooltip("The default camera panning speed (ignored if relative panning is enabled).")] + public float PanningSpeed { get; set; } = 0.8f; + + /// + /// Gets or sets the default editor viewport grid scale. /// [DefaultValue(50.0f), Limit(25.0f, 500.0f, 5.0f)] - [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("Scales editor viewport grid.")] + [EditorDisplay("Defaults"), EditorOrder(220), Tooltip("The default editor viewport grid scale.")] public float ViewportGridScale { get; set; } = 50.0f; } } diff --git a/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs b/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs index 310612a73..cbb383b4c 100644 --- a/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs +++ b/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs @@ -25,7 +25,6 @@ namespace FlaxEditor.Progress.Handlers ScriptsBuilder.ScriptsReloadCalled += () => OnUpdate(0.8f, "Reloading scripts..."); ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; - ScriptsBuilder.ScriptsReload += OnScriptsReload; } private void OnScriptsReloadBegin() @@ -38,14 +37,6 @@ namespace FlaxEditor.Progress.Handlers Editor.Instance.Scene.ClearRefsToSceneObjects(true); } - private void OnScriptsReload() - { -#if !USE_NETCORE - // Clear types cache - Newtonsoft.Json.JsonSerializer.ClearCache(); -#endif - } - private void OnCompilationFailed() { OnFail("Scripts compilation failed"); diff --git a/Source/Editor/ProjectInfo.cpp b/Source/Editor/ProjectInfo.cpp index 4e7ee4483..30c558b5d 100644 --- a/Source/Editor/ProjectInfo.cpp +++ b/Source/Editor/ProjectInfo.cpp @@ -154,7 +154,8 @@ bool ProjectInfo::LoadProject(const String& projectPath) Version = ::Version( JsonTools::GetInt(version, "Major", 0), JsonTools::GetInt(version, "Minor", 0), - JsonTools::GetInt(version, "Build", 0)); + JsonTools::GetInt(version, "Build", -1), + JsonTools::GetInt(version, "Revision", -1)); } } if (Version.Revision() == 0) diff --git a/Source/Editor/ProjectInfo.cs b/Source/Editor/ProjectInfo.cs index b00c4e042..083665f0f 100644 --- a/Source/Editor/ProjectInfo.cs +++ b/Source/Editor/ProjectInfo.cs @@ -23,17 +23,11 @@ namespace FlaxEditor public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { if (value == null) - { writer.WriteNull(); - } else if (value is Version) - { writer.WriteValue(value.ToString()); - } else - { throw new JsonSerializationException("Expected Version object value"); - } } /// @@ -47,65 +41,60 @@ namespace FlaxEditor public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) - { return null; - } - else + + if (reader.TokenType == JsonToken.StartObject) { - if (reader.TokenType == JsonToken.StartObject) + try { - try + reader.Read(); + var values = new Dictionary(); + while (reader.TokenType == JsonToken.PropertyName) { + var key = reader.Value as string; reader.Read(); - Dictionary values = new Dictionary(); - while (reader.TokenType == JsonToken.PropertyName) - { - var key = reader.Value as string; - reader.Read(); - var val = (long)reader.Value; - reader.Read(); - values.Add(key, (int)val); - } + var val = (long)reader.Value; + reader.Read(); + values.Add(key, (int)val); + } - int major = 0, minor = 0, build = 0; - values.TryGetValue("Major", out major); - values.TryGetValue("Minor", out minor); - values.TryGetValue("Build", out build); + values.TryGetValue("Major", out var major); + values.TryGetValue("Minor", out var minor); + if (!values.TryGetValue("Build", out var build)) + build = -1; + if (!values.TryGetValue("Revision", out var revision)) + revision = -1; - Version v = new Version(major, minor, build); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); - } + if (build <= 0) + return new Version(major, minor); + if (revision <= 0) + return new Version(major, minor, build); + return new Version(major, minor, build, revision); } - else if (reader.TokenType == JsonToken.String) + catch (Exception ex) { - try - { - Version v = new Version((string)reader.Value!); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); - } - } - else - { - throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); + throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); } } + if (reader.TokenType == JsonToken.String) + { + try + { + return new Version((string)reader.Value!); + } + catch (Exception ex) + { + throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); + } + } + throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); } /// /// Determines whether this instance can convert the specified object type. /// /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// + /// true if this instance can convert the specified object type; otherwise, false. public override bool CanConvert(Type objectType) { return objectType == typeof(Version); diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index c09e1a246..ed6fc08d6 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -7,6 +7,7 @@ using Real = System.Single; #endif using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.SceneGraph.Actors { @@ -30,6 +31,13 @@ namespace FlaxEditor.SceneGraph.Actors // Rotate to match the space (GUI uses upper left corner as a root) Actor.LocalOrientation = Quaternion.Euler(0, -180, -180); + var uiControl = new UIControl + { + Name = "Canvas Scalar", + Transform = Actor.Transform, + Control = new CanvasScaler() + }; + Root.Spawn(uiControl, Actor); } /// diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index d392e8309..f64e46385 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -66,7 +66,8 @@ namespace FlaxEditor.SceneGraph.GUI _orderInParent = actor.OrderInParent; Visible = (actor.HideFlags & HideFlags.HideInHierarchy) == 0; - var id = actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID; if (Editor.Instance.ProjectCache.IsExpandedActor(ref id)) { Expand(true); @@ -171,7 +172,8 @@ namespace FlaxEditor.SceneGraph.GUI // Restore cached state on query filter clear if (noFilter && actor != null) { - var id = actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene.Scene == null ? actor.PrefabObjectID : actor.ID; isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id); } @@ -264,7 +266,7 @@ namespace FlaxEditor.SceneGraph.GUI /// /// Starts the actor renaming action. /// - public void StartRenaming(EditorWindow window) + public void StartRenaming(EditorWindow window, Panel treePanel = null) { // Block renaming during scripts reload if (Editor.Instance.ProgressReporting.CompileScripts.IsActive) @@ -279,7 +281,13 @@ namespace FlaxEditor.SceneGraph.GUI (window as PrefabWindow).ScrollingOnTreeView(false); // Start renaming the actor - var dialog = RenamePopup.Show(this, TextRect, _actorNode.Name, false); + var rect = TextRect; + if (treePanel != null) + { + treePanel.ScrollViewTo(this, true); + rect.Size = new Float2(treePanel.Width - TextRect.Location.X, TextRect.Height); + } + var dialog = RenamePopup.Show(this, rect, _actorNode.Name, false); dialog.Renamed += OnRenamed; dialog.Closed += popup => { @@ -301,10 +309,12 @@ namespace FlaxEditor.SceneGraph.GUI protected override void OnExpandedChanged() { base.OnExpandedChanged(); + var actor = Actor; - if (!IsLayoutLocked && Actor) + if (!IsLayoutLocked && actor) { - var id = Actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID; Editor.Instance.ProjectCache.SetExpandedActor(ref id, IsExpanded); } } diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp index 7f5ca6f17..de0a30e40 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp @@ -207,9 +207,9 @@ void RiderCodeEditor::FindEditors(Array* output) FileSystem::GetChildDirectories(subDirectories, TEXT("/opt/")); // Versions installed via JetBrains Toolbox - SearchDirectory(&installations, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/rider/")); - FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-0")); - FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions + SearchDirectory(&installations, localAppDataPath / TEXT("JetBrains/Toolbox/apps/rider/")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-0")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions // Detect Flatpak installations SearchDirectory(&installations, diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp index adccbba54..93fe7b300 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp @@ -78,7 +78,10 @@ void VisualStudioCodeEditor::FindEditors(Array* output) // Detect Flatpak installations { - if (Platform::RunProcess(TEXT("/bin/bash -c \"flatpak list --app --columns=application | grep com.visualstudio.code -c\""), String::Empty) == 0) + CreateProcessSettings procSettings; + procSettings.FileName = TEXT("/bin/bash -c \"flatpak list --app --columns=application | grep com.visualstudio.code -c\""); + procSettings.HiddenWindow = true; + if (Platform::CreateProcess(procSettings) == 0) { const String runPath(TEXT("flatpak run com.visualstudio.code")); output->Add(New(runPath, false)); diff --git a/Source/Editor/States/LoadingState.cs b/Source/Editor/States/LoadingState.cs index 698dc192f..d48918e1b 100644 --- a/Source/Editor/States/LoadingState.cs +++ b/Source/Editor/States/LoadingState.cs @@ -56,12 +56,14 @@ namespace FlaxEditor.States else if (Editor.Options.Options.General.ForceScriptCompilationOnStartup && !skipCompile) { // Generate project files when Cache is missing or was cleared previously - if (!Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Intermediate")) || - !Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Projects"))) + var projectFolderPath = Editor.GameProject?.ProjectFolderPath; + if (!Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Intermediate")) || + !Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Projects"))) { - var customArgs = Editor.Instance.CodeEditing.SelectedEditor.GenerateProjectCustomArgs; + var customArgs = Editor.CodeEditing.SelectedEditor?.GenerateProjectCustomArgs; ScriptsBuilder.GenerateProject(customArgs); } + // Compile scripts before loading any scenes, then we load them and can open scenes ScriptsBuilder.Compile(); } diff --git a/Source/Editor/States/ReloadingScriptsState.cs b/Source/Editor/States/ReloadingScriptsState.cs index 4b8866202..d9024d5c8 100644 --- a/Source/Editor/States/ReloadingScriptsState.cs +++ b/Source/Editor/States/ReloadingScriptsState.cs @@ -1,7 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using FlaxEngine; -using FlaxEditor.Utilities; using FlaxEngine.Utilities; namespace FlaxEditor.States diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs index 9773e9695..7c3aa4f13 100644 --- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs +++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs @@ -465,7 +465,7 @@ namespace FlaxEditor.Surface.Archetypes if (selectedIndex != -1) { var index = 5 + selectedIndex * 2; - SetValue(index, _animationPicker.SelectedID); + SetValue(index, _animationPicker.Validator.SelectedID); } } @@ -495,7 +495,7 @@ namespace FlaxEditor.Surface.Archetypes { if (isValid) { - _animationPicker.SelectedID = data1; + _animationPicker.Validator.SelectedID = data1; _animationSpeed.Value = data0.W; var path = string.Empty; @@ -505,7 +505,7 @@ namespace FlaxEditor.Surface.Archetypes } else { - _animationPicker.SelectedID = Guid.Empty; + _animationPicker.Validator.SelectedID = Guid.Empty; _animationSpeed.Value = 1.0f; } _animationPicker.Enabled = isValid; diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index bfc5bfad8..f8cd7bb5a 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -288,6 +288,9 @@ namespace FlaxEditor.Surface.Archetypes } } SetValue(2, ids); + + // Force refresh UI + ResizeAuto(); } } diff --git a/Source/Editor/Surface/Archetypes/Bitwise.cs b/Source/Editor/Surface/Archetypes/Bitwise.cs index 06f719adc..43dcb91b5 100644 --- a/Source/Editor/Surface/Archetypes/Bitwise.cs +++ b/Source/Editor/Surface/Archetypes/Bitwise.cs @@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes Op1(1, "Bitwise NOT", "Negates the value using bitwise operation", new[] { "!", "~" }), Op2(2, "Bitwise AND", "Performs a bitwise conjunction on two values", new[] { "&" }), Op2(3, "Bitwise OR", "", new[] { "|" }), - Op2(4, "Bitwise XOR", ""), + Op2(4, "Bitwise XOR", "", new[] { "^" }), }; } } diff --git a/Source/Editor/Surface/Archetypes/Boolean.cs b/Source/Editor/Surface/Archetypes/Boolean.cs index 153b2fead..ed97a9642 100644 --- a/Source/Editor/Surface/Archetypes/Boolean.cs +++ b/Source/Editor/Surface/Archetypes/Boolean.cs @@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes Op1(1, "Boolean NOT", "Negates the boolean value", new[] { "!", "~" }), Op2(2, "Boolean AND", "Performs a logical conjunction on two values", new[] { "&&" }), Op2(3, "Boolean OR", "Returns true if either (or both) of its operands is true", new[] { "||" }), - Op2(4, "Boolean XOR", ""), + Op2(4, "Boolean XOR", "", new [] { "^" } ), Op2(5, "Boolean NOR", ""), Op2(6, "Boolean NAND", ""), }; diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index a0efbd92f..b72111c46 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -14,7 +14,7 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Comparisons { - private static NodeArchetype Op(ushort id, string title, string desc) + private static NodeArchetype Op(ushort id, string title, string desc, string[] altTitles = null) { return new NodeArchetype { @@ -22,6 +22,7 @@ namespace FlaxEditor.Surface.Archetypes Title = title, Description = desc, Flags = NodeFlags.AllGraphs, + AlternativeTitles = altTitles, ConnectionsHints = ConnectionsHint.Value, Size = new Float2(100, 40), IndependentBoxes = new[] @@ -170,12 +171,12 @@ namespace FlaxEditor.Surface.Archetypes /// public static NodeArchetype[] Nodes = { - Op(1, "==", "Determines whether two values are equal"), - Op(2, "!=", "Determines whether two values are not equal"), - Op(3, ">", "Determines whether the first value is greater than the other"), - Op(4, "<", "Determines whether the first value is less than the other"), - Op(5, "<=", "Determines whether the first value is less or equal to the other"), - Op(6, ">=", "Determines whether the first value is greater or equal to the other"), + Op(1, "==", "Determines whether two values are equal", new[] { "equals" }), + Op(2, "!=", "Determines whether two values are not equal", new[] { "not equals" }), + Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than" }), + Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than" }), + Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than" }), + Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than" }), new NodeArchetype { TypeID = 7, diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index cbe7822e3..e58d917d1 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -7,11 +7,12 @@ using Real = System.Single; #endif using System; -using System.Reflection; +using System.Linq; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; +using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -24,6 +25,109 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Constants { + /// + /// A special type of node that adds the functionality to convert nodes to parameters. + /// + internal class ConvertToParameterNode : SurfaceNode + { + private readonly ScriptType _type; + private readonly Func _convertFunction; + + /// + public ConvertToParameterNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) + : base(id, context, nodeArch, groupArch) + { + _type = type; + _convertFunction = convertFunction; + } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + menu.AddButton("Convert to Parameter", OnConvertToParameter); + } + + private void OnConvertToParameter() + { + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + + Asset asset = Surface.Owner.SurfaceAsset; + if (asset == null || !asset.IsLoaded) + { + Editor.LogError("Asset is null or not loaded"); + return; + } + + // Add parameter to editor + var paramIndex = Surface.Parameters.Count; + var initValue = _convertFunction == null ? Values[0] : _convertFunction.Invoke(Values); + var paramAction = new AddRemoveParamAction + { + Window = window, + IsAdd = true, + Name = Utilities.Utils.IncrementNameNumber("New parameter", OnParameterRenameValidate), + Type = _type, + Index = paramIndex, + InitValue = initValue, + }; + paramAction.Do(); + Surface.AddBatchedUndoAction(paramAction); + + // Spawn Get Parameter Node based on the added parameter + Guid parameterGuid = Surface.Parameters[paramIndex].ID; + bool undoEnabled = Surface.Undo.Enabled; + Surface.Undo.Enabled = false; + NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); + SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, Location, new object[] { parameterGuid }); + Surface.Undo.Enabled = undoEnabled; + if (node is not Parameters.SurfaceNodeParamsGet getNode) + throw new Exception("Node is not a ParamsGet node!"); + Surface.AddBatchedUndoAction(new AddRemoveNodeAction(getNode, true)); + + // Recreate connections of constant node + // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections + var editConnectionsAction1 = new EditNodeConnections(Context, this); + var editConnectionsAction2 = new EditNodeConnections(Context, node); + var boxes = GetBoxes(); + for (int i = 0; i < boxes.Count; i++) + { + Box box = boxes[i]; + if (!box.HasAnyConnection) + continue; + if (!getNode.TryGetBox(box.ID, out Box paramBox)) + continue; + + // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true + for (int k = box.Connections.Count - 1; k >= 0; k--) + { + Box connectedBox = box.Connections[k]; + paramBox.CreateConnection(connectedBox); + } + } + editConnectionsAction1.End(); + editConnectionsAction2.End(); + Surface.AddBatchedUndoAction(editConnectionsAction1); + Surface.AddBatchedUndoAction(editConnectionsAction2); + + // Add undo actions and remove constant node + var removeConstantAction = new AddRemoveNodeAction(this, false); + Surface.AddBatchedUndoAction(removeConstantAction); + removeConstantAction.Do(); + Surface.MarkAsEdited(); + } + + private bool OnParameterRenameValidate(string value) + { + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); + } + } + private class EnumNode : SurfaceNode { private EnumComboBox _picker; @@ -356,6 +460,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Bool", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -388,6 +493,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 2, Title = "Integer", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -415,6 +521,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Float", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -442,6 +549,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Float2", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), Description = "Constant Float2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -472,6 +580,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 5, Title = "Float3", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), Description = "Constant Float3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -504,6 +613,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Title = "Float4", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), Description = "Constant Float4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), @@ -538,6 +648,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Color", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), Description = "RGBA color", Flags = NodeFlags.AllGraphs, Size = new Float2(70, 100), @@ -570,6 +681,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 8, Title = "Rotation", + Create = (id, context, arch, groupArch) => + new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Quaternion)), values => Quaternion.Euler((float)values[0], (float)values[1], (float)values[2])), Description = "Euler angle rotation", Flags = NodeFlags.AnimGraph | NodeFlags.VisualScriptGraph | NodeFlags.ParticleEmitterGraph, Size = new Float2(110, 60), @@ -594,6 +707,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 9, Title = "String", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(string))), Description = "Text", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, Size = new Float2(200, 20), @@ -644,6 +758,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 12, Title = "Unsigned Integer", + AlternativeTitles = new[] { "UInt", "U Int" }, + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(170, 20), @@ -683,6 +799,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 15, Title = "Double", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -700,6 +817,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 16, Title = "Vector2", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), Description = "Constant Vector2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -720,6 +838,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 17, Title = "Vector3", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), Description = "Constant Vector3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -742,6 +861,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Vector4", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), Description = "Constant Vector4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 7d12a0625..53950dad2 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -95,7 +95,7 @@ namespace FlaxEditor.Surface.Archetypes private void OnAssetPickerSelectedItemChanged() { - SetValue(0, _assetPicker.SelectedID); + SetValue(0, _assetPicker.Validator.SelectedID); } private void TryRestoreConnections(Box box, Box[] prevBoxes, ref NodeElementArchetype arch) @@ -133,7 +133,7 @@ namespace FlaxEditor.Surface.Archetypes var prevOutputs = _outputs; // Extract function signature parameters (inputs and outputs packed) - _asset = LoadSignature(_assetPicker.SelectedID, out var typeNames, out var names); + _asset = LoadSignature(_assetPicker.Validator.SelectedID, out var typeNames, out var names); if (typeNames != null && names != null) { var types = new Type[typeNames.Length]; @@ -174,7 +174,7 @@ namespace FlaxEditor.Surface.Archetypes _outputs[i] = box; } - Title = _assetPicker.SelectedItem.ShortName; + Title = _assetPicker.Validator.SelectedItem.ShortName; } else { @@ -2470,6 +2470,7 @@ namespace FlaxEditor.Surface.Archetypes Title = string.Empty, Description = "Overrides the base class method with custom implementation", Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste, + SortScore = 10, IsInputCompatible = MethodOverrideNode.IsInputCompatible, IsOutputCompatible = MethodOverrideNode.IsOutputCompatible, Size = new Float2(240, 60), diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index b85d1c9d4..f65804023 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -882,6 +882,60 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(1, "Inv Size", typeof(Float2), 1), } }, + new NodeArchetype + { + TypeID = 40, + Title = "Rectangle Mask", + Description = "Creates a rectangle mask", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 40), + ConnectionsHints = ConnectionsHint.Vector, + DefaultValues = new object[] + { + new Float2(0.5f, 0.5f), + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "UV", true, typeof(Float2), 0), + NodeElementArchetype.Factory.Input(1, "Rectangle", true, typeof(Float2), 1, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 2), + } + }, + new NodeArchetype + { + TypeID = 41, + Title = "FWidth", + Description = "Creates a partial derivative (fwidth)", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 20), + ConnectionsHints = ConnectionsHint.Numeric, + IndependentBoxes = new[] { 0 }, + DependentBoxes = new[] { 1 }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "Value", true, null, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, null, 1), + } + }, + new NodeArchetype + { + TypeID = 42, + Title = "AA Step", + Description = "Smooth version of step function with less aliasing", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 40), + ConnectionsHints = ConnectionsHint.Vector, + DefaultValues = new object[] + { + 0.5f + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "Value", true, typeof(float), 0), + NodeElementArchetype.Factory.Input(1, "Gradient", true, typeof(float), 1, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 2), + } + }, }; } } diff --git a/Source/Editor/Surface/Archetypes/Math.cs b/Source/Editor/Surface/Archetypes/Math.cs index fe2f1e044..7f4afa6a6 100644 --- a/Source/Editor/Surface/Archetypes/Math.cs +++ b/Source/Editor/Surface/Archetypes/Math.cs @@ -13,6 +13,11 @@ namespace FlaxEditor.Surface.Archetypes public static class Math { private static NodeArchetype Op1(ushort id, string title, string desc, ConnectionsHint hints = ConnectionsHint.Numeric, Type type = null) + { + return Op1(id, title, desc, null, hints, type); + } + + private static NodeArchetype Op1(ushort id, string title, string desc, string[] altTitles, ConnectionsHint hints = ConnectionsHint.Numeric, Type type = null) { return new NodeArchetype { @@ -20,6 +25,7 @@ namespace FlaxEditor.Surface.Archetypes Title = title, Description = desc, Flags = NodeFlags.AllGraphs, + AlternativeTitles = altTitles, Size = new Float2(110, 20), DefaultType = new ScriptType(type), ConnectionsHints = hints, @@ -92,6 +98,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 11, Title = "Length", + AlternativeTitles = new[] { "Magnitude", "Mag" }, Description = "Returns the length of A vector", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -107,10 +114,10 @@ namespace FlaxEditor.Surface.Archetypes Op1(13, "Round", "Rounds A to the nearest integer"), Op1(14, "Saturate", "Clamps A to the range [0, 1]"), Op1(15, "Sine", "Returns sine of A"), - Op1(16, "Sqrt", "Returns square root of A"), + Op1(16, "Sqrt", "Returns square root of A", new [] { "Square Root", "Square", "Root" }), Op1(17, "Tangent", "Returns tangent of A"), Op2(18, "Cross", "Returns the cross product of A and B", ConnectionsHint.None, typeof(Float3)), - Op2(19, "Distance", "Returns a distance scalar between A and B", ConnectionsHint.Vector, null, typeof(float), false), + Op2(19, "Distance", "Returns a distance scalar between A and B", new [] { "Magnitude", "Mag", "Length" }, ConnectionsHint.Vector, null, typeof(float), false), Op2(20, "Dot", "Returns the dot product of A and B", ConnectionsHint.Vector, null, typeof(float), false), Op2(21, "Max", "Selects the greater of A and B"), Op2(22, "Min", "Selects the lesser of A and B"), @@ -185,7 +192,7 @@ namespace FlaxEditor.Surface.Archetypes } }, // - Op1(27, "Negate", "Returns opposite value"), + Op1(27, "Negate", "Returns opposite value", new [] { "Invert" }), Op1(28, "One Minus", "Returns 1 - value"), // new NodeArchetype @@ -225,6 +232,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 31, Title = "Mad", + AlternativeTitles = new [] { "Multiply", "Add", "*+" }, Description = "Performs value multiplication and addition at once", Flags = NodeFlags.AllGraphs, Size = new Float2(160, 60), diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 6ed5a61e6..4cf1af4f6 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -1084,7 +1084,7 @@ namespace FlaxEditor.Surface.Archetypes }, Elements = new[] { - NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 0), + NodeElementArchetype.Factory.Input(0, string.Empty, false, typeof(void), 0), NodeElementArchetype.Factory.Input(1, string.Empty, true, ScriptType.Null, 1, 1), NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 2, true), NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116) diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 18c21ecea..e35af7192 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -3,6 +3,7 @@ using System; using FlaxEditor.Content.Settings; using FlaxEditor.GUI; +using FlaxEditor.Scripting; using FlaxEngine; namespace FlaxEditor.Surface.Archetypes @@ -95,6 +96,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Texture", + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -131,6 +133,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Cube Texture", + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), Description = "Set of 6 textures arranged in a cube", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -154,6 +157,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Normal Map", + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(NormalMap))), Description = "Two dimensional texture object sampled as a normal map", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 6d2d31b8c..029422d05 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1483,7 +1483,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 11, Title = "Comment", - AlternativeTitles = new[] { "//" }, + AlternativeTitles = new[] { "//" , "Group" }, TryParseText = (string filterText, out object[] data) => { data = null; @@ -1510,6 +1510,7 @@ namespace FlaxEditor.Surface.Archetypes "Comment", // Title new Color(1.0f, 1.0f, 1.0f, 0.2f), // Color new Float2(400.0f, 400.0f), // Size + -1, // Order }, }, CurveNode.GetArchetype(12, "Curve", typeof(float), 0.0f, 1.0f), @@ -1638,6 +1639,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 22, Title = "As", + AlternativeTitles = new [] { "Cast" }, Create = (id, context, arch, groupArch) => new AsNode(id, context, arch, groupArch), Description = "Casts the object to a different type. Returns null if cast fails.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs index 37da7d74d..207875a92 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs @@ -78,6 +78,7 @@ namespace FlaxEditor.Surface.ContextMenu if (!Visible) return; + SortScore += _archetype.SortScore; if (selectedBox != null && CanConnectTo(selectedBox)) SortScore += 1; if (Data != null) diff --git a/Source/Editor/Surface/Elements/AssetSelect.cs b/Source/Editor/Surface/Elements/AssetSelect.cs index e38989e08..5984ce9aa 100644 --- a/Source/Editor/Surface/Elements/AssetSelect.cs +++ b/Source/Editor/Surface/Elements/AssetSelect.cs @@ -38,13 +38,13 @@ namespace FlaxEditor.Surface.Elements private void OnNodeValuesChanged() { - SelectedID = (Guid)ParentNode.Values[Archetype.ValueIndex]; + Validator.SelectedID = (Guid)ParentNode.Values[Archetype.ValueIndex]; } /// protected override void OnSelectedItemChanged() { - var selectedId = SelectedID; + var selectedId = Validator.SelectedID; if (ParentNode != null && (Guid)ParentNode.Values[Archetype.ValueIndex] != selectedId) { ParentNode.SetValue(Archetype.ValueIndex, selectedId); diff --git a/Source/Editor/Surface/Elements/FloatValue.cs b/Source/Editor/Surface/Elements/FloatValue.cs index 7674c68b9..56539e3e3 100644 --- a/Source/Editor/Surface/Elements/FloatValue.cs +++ b/Source/Editor/Surface/Elements/FloatValue.cs @@ -33,6 +33,10 @@ namespace FlaxEditor.Surface.Elements Archetype = archetype; ParentNode.ValuesChanged += OnNodeValuesChanged; + + // Disable slider if surface doesn't allow it + if (!ParentNode.Surface.CanLivePreviewValueChanges) + _slideSpeed = 0.0f; } private void OnNodeValuesChanged() diff --git a/Source/Editor/Surface/MaterialSurface.cs b/Source/Editor/Surface/MaterialSurface.cs index 08e1161d9..ba7f2e01e 100644 --- a/Source/Editor/Surface/MaterialSurface.cs +++ b/Source/Editor/Surface/MaterialSurface.cs @@ -22,6 +22,9 @@ namespace FlaxEditor.Surface { } + /// + public override bool CanLivePreviewValueChanges => false; + /// public override string GetTypeName(ScriptType type) { diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index 0284fc76d..3a77cef84 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -149,6 +149,11 @@ namespace FlaxEditor.Surface /// public object Tag; + /// + /// Custom score value to use when sorting node archetypes in Editor. If positive (eg. 1, 2) can be used to add more importance for a specific node type. + /// + public float SortScore; + /// /// Default node values. This array supports types: bool, int, float, Vector2, Vector3, Vector4, Color, Rectangle, Guid, string, Matrix and byte[]. /// @@ -204,14 +209,17 @@ namespace FlaxEditor.Surface Size = Size, Flags = Flags, Title = Title, - Description = Title, + SubTitle = SubTitle, + Description = Description, AlternativeTitles = (string[])AlternativeTitles?.Clone(), Tag = Tag, + SortScore = SortScore, DefaultValues = (object[])DefaultValues?.Clone(), DefaultType = DefaultType, ConnectionsHints = ConnectionsHints, IndependentBoxes = (int[])IndependentBoxes?.Clone(), DependentBoxes = (int[])DependentBoxes?.Clone(), + DependentBoxFilter = DependentBoxFilter, Elements = (NodeElementArchetype[])Elements?.Clone(), TryParseText = TryParseText, }; diff --git a/Source/Editor/Surface/ParticleEmitterSurface.cs b/Source/Editor/Surface/ParticleEmitterSurface.cs index 76f96f06c..b7cf83c62 100644 --- a/Source/Editor/Surface/ParticleEmitterSurface.cs +++ b/Source/Editor/Surface/ParticleEmitterSurface.cs @@ -93,7 +93,7 @@ namespace FlaxEditor.Surface } /// - protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[1]; diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index ff38b1aa8..aad45190e 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEngine.GUI; @@ -52,6 +53,12 @@ namespace FlaxEditor.Surface set => SetValue(2, value, false); } + private int OrderValue + { + get => (int)Values[3]; + set => SetValue(3, value, false); + } + /// public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) @@ -67,6 +74,19 @@ namespace FlaxEditor.Surface Title = TitleValue; Color = ColorValue; Size = SizeValue; + + // Order + // Backwards compatibility - When opening with an older version send the old comments to the back + if (Values.Length < 4) + { + if (IndexInParent > 0) + IndexInParent = 0; + OrderValue = IndexInParent; + } + else if(OrderValue != -1) + { + IndexInParent = OrderValue; + } } /// @@ -76,6 +96,10 @@ namespace FlaxEditor.Surface // Randomize color Color = ColorValue = Color.FromHSV(new Random().NextFloat(0, 360), 0.7f, 0.25f, 0.8f); + + if(OrderValue == -1) + OrderValue = Context.CommentCount - 1; + IndexInParent = OrderValue; } /// @@ -314,5 +338,38 @@ namespace FlaxEditor.Surface Color = ColorValue = color; Surface.MarkAsEdited(false); } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + ContextMenuChildMenu cmOrder = menu.AddChildMenu("Order"); + { + cmOrder.ContextMenu.AddButton("Bring Forward", () => + { + if(IndexInParent < Context.CommentCount-1) + IndexInParent++; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Bring to Front", () => + { + IndexInParent = Context.CommentCount-1; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Send Backward", () => + { + if(IndexInParent > 0) + IndexInParent--; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Send to Back", () => + { + IndexInParent = 0; + OrderValue = IndexInParent; + }); + } + } } } diff --git a/Source/Editor/Surface/SurfaceParameter.cs b/Source/Editor/Surface/SurfaceParameter.cs index 6eafdc2aa..dae82e111 100644 --- a/Source/Editor/Surface/SurfaceParameter.cs +++ b/Source/Editor/Surface/SurfaceParameter.cs @@ -3,7 +3,6 @@ using System; using FlaxEditor.Scripting; using FlaxEngine; -using FlaxEngine.Utilities; namespace FlaxEditor.Surface { @@ -27,7 +26,7 @@ namespace FlaxEditor.Surface /// /// Parameter unique ID /// - public Guid ID; + public Guid ID = Guid.Empty; /// /// Parameter name @@ -49,23 +48,5 @@ namespace FlaxEditor.Surface /// [NoSerialize, HideInEditor] public readonly SurfaceMeta Meta = new SurfaceMeta(); - - /// - /// Creates the new parameter of the given type. - /// - /// The type. - /// The name. - /// The created parameter. - public static SurfaceParameter Create(ScriptType type, string name) - { - return new SurfaceParameter - { - ID = Guid.NewGuid(), - IsPublic = true, - Name = name, - Type = type, - Value = TypeUtils.GetDefaultValue(type), - }; - } } } diff --git a/Source/Editor/Surface/VisjectSurface.DragDrop.cs b/Source/Editor/Surface/VisjectSurface.DragDrop.cs index 1728c282f..2ff82c269 100644 --- a/Source/Editor/Surface/VisjectSurface.DragDrop.cs +++ b/Source/Editor/Surface/VisjectSurface.DragDrop.cs @@ -151,7 +151,7 @@ namespace FlaxEditor.Surface /// /// The group ID. /// The node archetype. - protected virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[0]; diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 3fe722091..3ac702549 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -534,6 +534,11 @@ namespace FlaxEditor.Surface /// public virtual bool CanSetParameters => false; + /// + /// Gets a value indicating whether surface supports/allows live previewing graph modifications due to value sliders and color pickers. True by default but disabled for shader surfaces that generate and compile shader source at flight. + /// + public virtual bool CanLivePreviewValueChanges => true; + /// /// Determines whether the specified node archetype can be used in the surface. /// @@ -716,7 +721,18 @@ namespace FlaxEditor.Surface return null; Rectangle surfaceArea = GetNodesBounds(selection).MakeExpanded(80.0f); - return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f)); + // Order below other selected comments + bool hasCommentsSelected = false; + int lowestCommentOrder = int.MaxValue; + for (int i = 0; i < selection.Count; i++) + { + if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder) + continue; + hasCommentsSelected = true; + lowestCommentOrder = selection[i].IndexInParent; + } + + return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1); } private static Rectangle GetNodesBounds(List nodes) diff --git a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs index 7d75e006b..12f01d4f1 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs @@ -920,12 +920,6 @@ namespace FlaxEditor.Surface // Link control control.OnLoaded(action); control.Parent = RootControl; - - if (control is SurfaceComment) - { - // Move comments to the background - control.IndexInParent = 0; - } } /// diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 36e811c48..0886996b6 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -85,6 +85,27 @@ namespace FlaxEditor.Surface } } + /// + /// Gets the amount of surface comments + /// + /// + /// This is used as an alternative to , if only the amount of comments is important. + /// Is faster and doesn't allocate as much memory + /// + public int CommentCount + { + get + { + int count = 0; + for (int i = 0; i < RootControl.Children.Count; i++) + { + if (RootControl.Children[i] is SurfaceComment) + count++; + } + return count; + } + } + /// /// Gets a value indicating whether this context is modified (needs saving and flushing with surface data context source). /// @@ -285,14 +306,16 @@ namespace FlaxEditor.Surface /// The surface area to create comment. /// The comment title. /// The comment color. + /// The comment order or -1 to use default. /// The comment object - public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color) + public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { var values = new object[] { title, // Title color, // Color surfaceArea.Size, // Size + customOrder, // Order }; return (SurfaceComment)SpawnNode(7, 11, surfaceArea.Location, values); } @@ -303,11 +326,12 @@ namespace FlaxEditor.Surface /// The surface area to create comment. /// The comment title. /// The comment color. + /// The comment order or -1 to use default. /// The comment object - public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color) + public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { // Create comment - var comment = SpawnComment(ref surfaceArea, title, color); + var comment = SpawnComment(ref surfaceArea, title, color, customOrder); if (comment == null) { Editor.LogWarning("Failed to create comment."); diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index 305121e11..cce86d5c0 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -17,6 +17,7 @@ using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Utilities; namespace FlaxEditor.Surface { @@ -258,6 +259,11 @@ namespace FlaxEditor.Surface /// public IVisjectSurfaceWindow Window; + /// + /// The identifier of the parameter. Empty to auto generate it. + /// + public Guid Id = Guid.NewGuid(); + /// /// True if adding, false if removing parameter. /// @@ -278,6 +284,11 @@ namespace FlaxEditor.Surface /// public ScriptType Type; + /// + /// The value to initialize the parameter with. Can be null to use default one for the parameter type. + /// + public object InitValue; + /// public string ActionString => IsAdd ? "Add parameter" : "Remove parameter"; @@ -304,7 +315,14 @@ namespace FlaxEditor.Surface var type = Type; if (IsAdd && type.Type == typeof(NormalMap)) type = new ScriptType(typeof(Texture)); - var param = SurfaceParameter.Create(type, Name); + var param = new SurfaceParameter + { + ID = Id, + IsPublic = true, + Name = Name, + Type = type, + Value = InitValue ?? TypeUtils.GetDefaultValue(type), + }; if (IsAdd && Type.Type == typeof(NormalMap)) param.Value = FlaxEngine.Content.LoadAsyncInternal("Engine/Textures/NormalTexture"); Window.VisjectSurface.Parameters.Insert(Index, param); @@ -725,6 +743,8 @@ namespace FlaxEditor.Surface protected VisjectSurfaceWindow(Editor editor, AssetItem item, bool useTabs = false) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new FlaxEditor.Undo(); _undo.UndoDone += OnUndoRedo; @@ -775,10 +795,10 @@ namespace FlaxEditor.Surface // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); // Setup input actions @@ -1058,7 +1078,6 @@ namespace FlaxEditor.Surface public virtual void OnParamRemoveUndo() { _refreshPropertiesOnLoad = true; - //_propertiesEditor.BuildLayoutOnUpdate(); _propertiesEditor.BuildLayout(); } diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs index 902582311..2f8d4cda8 100644 --- a/Source/Editor/Surface/VisualScriptSurface.cs +++ b/Source/Editor/Surface/VisualScriptSurface.cs @@ -144,7 +144,7 @@ namespace FlaxEditor.Surface } /// - protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[2]; diff --git a/Source/Editor/Tools/ClothPainting.cs b/Source/Editor/Tools/ClothPainting.cs index 226c7b49c..28536ea83 100644 --- a/Source/Editor/Tools/ClothPainting.cs +++ b/Source/Editor/Tools/ClothPainting.cs @@ -18,21 +18,25 @@ namespace FlaxEngine.Tools /// /// Brush radius (world-space). /// + [Limit(0)] public float BrushSize = 50.0f; /// /// Brush paint intensity. /// + [Limit(0)] public float BrushStrength = 2.0f; /// /// Brush paint falloff. Hardens or softens painting. /// + [Limit(0)] public float BrushFalloff = 1.5f; /// /// Value to paint with. Hold Ctrl hey to paint with inverse value (1 - value). /// + [Limit(0, 1, 0.01f)] public float PaintValue = 0.0f; /// @@ -225,6 +229,7 @@ namespace FlaxEngine.Tools var cloth = _cloth; if (cloth == null) return; + var hasPaintInput = Owner.IsLeftMouseButtonDown && !Owner.IsAltKeyDown; // Perform detailed tracing to find cursor location for the brush var ray = Owner.MouseRay; @@ -240,7 +245,7 @@ namespace FlaxEngine.Tools // Cursor hit other object or nothing PaintEnd(); - if (Owner.IsLeftMouseButtonDown) + if (hasPaintInput) { // Select something else var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); @@ -253,7 +258,7 @@ namespace FlaxEngine.Tools } // Handle painting - if (Owner.IsLeftMouseButtonDown) + if (hasPaintInput) PaintStart(); else PaintEnd(); diff --git a/Source/Editor/Tools/Terrain/EditTab.cs b/Source/Editor/Tools/Terrain/EditTab.cs index 008be64f4..d52c1ae1d 100644 --- a/Source/Editor/Tools/Terrain/EditTab.cs +++ b/Source/Editor/Tools/Terrain/EditTab.cs @@ -290,7 +290,7 @@ namespace FlaxEditor.Tools.Terrain var patchCoord = Gizmo.SelectedPatchCoord; var chunkCoord = Gizmo.SelectedChunkCoord; - var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.SelectedAsset as MaterialBase); + var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.Validator.SelectedAsset as MaterialBase); action.Do(); CarveTab.Editor.Undo.AddAction(action); } @@ -336,12 +336,12 @@ namespace FlaxEditor.Tools.Terrain _isUpdatingUI = true; if (terrain.HasPatch(ref patchCoord)) { - _chunkOverrideMaterial.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord); + _chunkOverrideMaterial.Validator.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord); _chunkOverrideMaterial.Enabled = true; } else { - _chunkOverrideMaterial.SelectedAsset = null; + _chunkOverrideMaterial.Validator.SelectedAsset = null; _chunkOverrideMaterial.Enabled = false; } _isUpdatingUI = false; diff --git a/Source/Editor/Viewport/Cameras/FPSCamera.cs b/Source/Editor/Viewport/Cameras/FPSCamera.cs index e578933cf..bf2e840ea 100644 --- a/Source/Editor/Viewport/Cameras/FPSCamera.cs +++ b/Source/Editor/Viewport/Cameras/FPSCamera.cs @@ -259,7 +259,10 @@ namespace FlaxEditor.Viewport.Cameras // Pan if (input.IsPanning) { - var panningSpeed = 0.8f; + var panningSpeed = (Viewport.RelativePanning) + ? Mathf.Abs((position - TargetPoint).Length) * 0.005f + : Viewport.PanningSpeed; + if (Viewport.InvertPanning) { position += up * (mouseDelta.Y * panningSpeed); diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs index a24dd2f38..fe579e7e5 100644 --- a/Source/Editor/Viewport/EditorGizmoViewport.cs +++ b/Source/Editor/Viewport/EditorGizmoViewport.cs @@ -86,6 +86,9 @@ namespace FlaxEditor.Viewport /// public abstract void Select(List nodes); + /// + public abstract void Spawn(Actor actor); + /// protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false; diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 9a444f2da..c49392d01 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -128,12 +128,26 @@ namespace FlaxEditor.Viewport public const int FpsCameraFilteringFrames = 3; /// - /// The speed widget button. + /// The camera settings widget. /// - protected ViewportWidgetButton _speedWidget; + protected ViewportWidgetsContainer _cameraWidget; + + /// + /// The camera settings widget button. + /// + protected ViewportWidgetButton _cameraButton; + + /// + /// The orthographic mode widget button. + /// + protected ViewportWidgetButton _orthographicModeButton; + + private readonly Editor _editor; private float _mouseSensitivity; private float _movementSpeed; + private float _minMovementSpeed; + private float _maxMovementSpeed; private float _mouseAccelerationScale; private bool _useMouseFiltering; private bool _useMouseAcceleration; @@ -174,11 +188,17 @@ namespace FlaxEditor.Viewport private float _fieldOfView; private float _nearPlane; private float _farPlane; - private float _orthoSize = 1.0f; - private bool _isOrtho = false; - private float _wheelMovementChangeDeltaSum = 0; + private float _orthoSize; + private bool _isOrtho; + private bool _useCameraEasing; + private float _cameraEasingDegree; + private float _panningSpeed; + private bool _relativePanning; private bool _invertPanning; + private int _speedStep; + private int _maxSpeedSteps; + /// /// Speed of the mouse. /// @@ -189,6 +209,25 @@ namespace FlaxEditor.Viewport /// public float MouseWheelZoomSpeedFactor = 1; + /// + /// Format of the text for the camera move speed. + /// + private string MovementSpeedTextFormat + { + get + { + if (Mathf.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon || Mathf.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon) + return "{0:0.##}"; + + if (_movementSpeed < 10.0f) + return "{0:0.00}"; + else if (_movementSpeed < 100.0f) + return "{0:0.0}"; + else + return "{0:#}"; + } + } + /// /// Gets or sets the camera movement speed. /// @@ -197,19 +236,40 @@ namespace FlaxEditor.Viewport get => _movementSpeed; set { - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - if (Math.Abs(value - EditorViewportCameraSpeedValues[i]) < 0.001f) - { - _movementSpeed = EditorViewportCameraSpeedValues[i]; - if (_speedWidget != null) - _speedWidget.Text = _movementSpeed.ToString(); - break; - } - } + _movementSpeed = value; + + if (_cameraButton != null) + _cameraButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed); } } + /// + /// Gets or sets the minimum camera movement speed. + /// + public float MinMovementSpeed + { + get => _minMovementSpeed; + set => _minMovementSpeed = value; + } + + /// + /// Gets or sets the maximum camera movement speed. + /// + public float MaxMovementSpeed + { + get => _maxMovementSpeed; + set => _maxMovementSpeed = value; + } + + /// + /// Gets or sets the camera easing mode. + /// + public bool UseCameraEasing + { + get => _useCameraEasing; + set => _useCameraEasing = value; + } + /// /// Gets the mouse movement position delta (user press and move). /// @@ -396,6 +456,15 @@ namespace FlaxEditor.Viewport set => _isOrtho = value; } + /// + /// Gets or sets if the panning speed should be relative to the camera target. + /// + public bool RelativePanning + { + get => _relativePanning; + set => _relativePanning = value; + } + /// /// Gets or sets if the panning direction is inverted. /// @@ -405,6 +474,15 @@ namespace FlaxEditor.Viewport set => _invertPanning = value; } + /// + /// Gets or sets the camera panning speed. + /// + public float PanningSpeed + { + get => _panningSpeed; + set => _panningSpeed = value; + } + /// /// The input actions collection to processed during user input. /// @@ -419,6 +497,8 @@ namespace FlaxEditor.Viewport public EditorViewport(SceneRenderTask task, ViewportCamera camera, bool useWidgets) : base(task) { + _editor = Editor.Instance; + _mouseAccelerationScale = 0.1f; _useMouseFiltering = false; _useMouseAcceleration = false; @@ -431,43 +511,299 @@ namespace FlaxEditor.Viewport // Setup options { - var options = Editor.Instance.Options.Options; - _movementSpeed = options.Viewport.DefaultMovementSpeed; - _nearPlane = options.Viewport.DefaultNearPlane; - _farPlane = options.Viewport.DefaultFarPlane; - _fieldOfView = options.Viewport.DefaultFieldOfView; - _invertPanning = options.Viewport.DefaultInvertPanning; - Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged; - OnEditorOptionsChanged(options); + SetupViewportOptions(); } + // Initialize camera values from cache + if (_editor.ProjectCache.TryGetCustomData("CameraMovementSpeedValue", out var cachedState)) + MovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraMinMovementSpeedValue", out cachedState)) + _minMovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraMaxMovementSpeedValue", out cachedState)) + _maxMovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("UseCameraEasingState", out cachedState)) + _useCameraEasing = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraPanningSpeedValue", out cachedState)) + _panningSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraInvertPanningState", out cachedState)) + _invertPanning = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraRelativePanningState", out cachedState)) + _relativePanning = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicState", out cachedState)) + _isOrtho = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicSizeValue", out cachedState)) + _orthoSize = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraFieldOfViewValue", out cachedState)) + _fieldOfView = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraNearPlaneValue", out cachedState)) + _nearPlane = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraFarPlaneValue", out cachedState)) + _farPlane = float.Parse(cachedState); + + OnCameraMovementProgressChanged(); + if (useWidgets) { - var largestText = "Invert Panning"; + #region Camera settings widget + + var largestText = "Relative Panning"; var textSize = Style.Current.FontMedium.MeasureText(largestText); var xLocationForExtras = textSize.X + 5; - // Camera speed widget - var camSpeed = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); - var camSpeedCM = new ContextMenu(); - var camSpeedButton = new ViewportWidgetButton(_movementSpeed.ToString(), Editor.Instance.Icons.CamSpeed32, camSpeedCM) + var cameraSpeedTextWidth = Style.Current.FontMedium.MeasureText("0.00").X; + + // Camera Settings Widget + _cameraWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); + + // Camera Settings Menu + var cameraCM = new ContextMenu(); + _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth) { Tag = this, - TooltipText = "Camera speed scale" + TooltipText = "Camera Settings", + Parent = _cameraWidget }; - _speedWidget = camSpeedButton; - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - var v = EditorViewportCameraSpeedValues[i]; - var button = camSpeedCM.AddButton(v.ToString()); - button.Tag = v; - } - camSpeedCM.ButtonClicked += button => MovementSpeed = (float)button.Tag; - camSpeedCM.VisibleChanged += WidgetCamSpeedShowHide; - camSpeedButton.Parent = camSpeed; - camSpeed.Parent = this; + _cameraWidget.Parent = this; + + // Orthographic/Perspective Mode Widget + _orthographicModeButton = new ViewportWidgetButton(string.Empty, Editor.Instance.Icons.CamSpeed32, null, true) + { + Checked = !_isOrtho, + TooltipText = "Toggle Orthographic/Perspective Mode", + Parent = _cameraWidget + }; + _orthographicModeButton.Toggled += OnOrthographicModeToggled; + + // Camera Speed + var camSpeedButton = cameraCM.AddButton("Camera Speed"); + camSpeedButton.CloseMenuOnClick = false; + var camSpeedValue = new FloatValueBox(_movementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, _maxMovementSpeed, 0.5f) + { + Parent = camSpeedButton + }; + + camSpeedValue.ValueChanged += () => OnMovementSpeedChanged(camSpeedValue); + cameraCM.VisibleChanged += control => camSpeedValue.Value = _movementSpeed; + + // Minimum & Maximum Camera Speed + var minCamSpeedButton = cameraCM.AddButton("Min Cam Speed"); + minCamSpeedButton.CloseMenuOnClick = false; + var minCamSpeedValue = new FloatValueBox(_minMovementSpeed, xLocationForExtras, 2, 70.0f, 0.05f, _maxMovementSpeed, 0.5f) + { + Parent = minCamSpeedButton + }; + var maxCamSpeedButton = cameraCM.AddButton("Max Cam Speed"); + maxCamSpeedButton.CloseMenuOnClick = false; + var maxCamSpeedValue = new FloatValueBox(_maxMovementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, 1000.0f, 0.5f) + { + Parent = maxCamSpeedButton + }; + + minCamSpeedValue.ValueChanged += () => + { + OnMinMovementSpeedChanged(minCamSpeedValue); + + maxCamSpeedValue.MinValue = minCamSpeedValue.Value; + + if (Math.Abs(camSpeedValue.MinValue - minCamSpeedValue.Value) > Mathf.Epsilon) + camSpeedValue.MinValue = minCamSpeedValue.Value; + }; + cameraCM.VisibleChanged += control => minCamSpeedValue.Value = _minMovementSpeed; + maxCamSpeedValue.ValueChanged += () => + { + OnMaxMovementSpeedChanged(maxCamSpeedValue); + + minCamSpeedValue.MaxValue = maxCamSpeedValue.Value; + + if (Math.Abs(camSpeedValue.MaxValue - maxCamSpeedValue.Value) > Mathf.Epsilon) + camSpeedValue.MaxValue = maxCamSpeedValue.Value; + }; + cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed; + + // Camera Easing + { + var useCameraEasing = cameraCM.AddButton("Camera Easing"); + useCameraEasing.CloseMenuOnClick = false; + var useCameraEasingValue = new CheckBox(xLocationForExtras, 2, _useCameraEasing) + { + Parent = useCameraEasing + }; + + useCameraEasingValue.StateChanged += OnCameraEasingToggled; + cameraCM.VisibleChanged += control => useCameraEasingValue.Checked = _useCameraEasing; + } + + // Panning Speed + { + var panningSpeed = cameraCM.AddButton("Panning Speed"); + panningSpeed.CloseMenuOnClick = false; + var panningSpeedValue = new FloatValueBox(_panningSpeed, xLocationForExtras, 2, 70.0f, 0.01f, 128.0f, 0.1f) + { + Parent = panningSpeed + }; + + panningSpeedValue.ValueChanged += () => OnPanningSpeedChanged(panningSpeedValue); + cameraCM.VisibleChanged += control => + { + panningSpeed.Visible = !_relativePanning; + panningSpeedValue.Value = _panningSpeed; + }; + } + + // Relative Panning + { + var relativePanning = cameraCM.AddButton("Relative Panning"); + relativePanning.CloseMenuOnClick = false; + var relativePanningValue = new CheckBox(xLocationForExtras, 2, _relativePanning) + { + Parent = relativePanning + }; + + relativePanningValue.StateChanged += checkBox => + { + if (checkBox.Checked != _relativePanning) + { + OnRelativePanningToggled(checkBox); + cameraCM.Hide(); + } + }; + cameraCM.VisibleChanged += control => relativePanningValue.Checked = _relativePanning; + } + + // Invert Panning + { + var invertPanning = cameraCM.AddButton("Invert Panning"); + invertPanning.CloseMenuOnClick = false; + var invertPanningValue = new CheckBox(xLocationForExtras, 2, _invertPanning) + { + Parent = invertPanning + }; + + invertPanningValue.StateChanged += OnInvertPanningToggled; + cameraCM.VisibleChanged += control => invertPanningValue.Checked = _invertPanning; + } + + cameraCM.AddSeparator(); + + // Camera Viewpoints + { + var cameraView = cameraCM.AddChildMenu("Viewpoints").ContextMenu; + for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) + { + var co = EditorViewportCameraViewpointValues[i]; + var button = cameraView.AddButton(co.Name); + button.Tag = co.Orientation; + } + + cameraView.ButtonClicked += OnViewpointChanged; + } + + // Orthographic Mode + { + var ortho = cameraCM.AddButton("Orthographic"); + ortho.CloseMenuOnClick = false; + var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho) + { + Parent = ortho + }; + + orthoValue.StateChanged += checkBox => + { + if (checkBox.Checked != _isOrtho) + { + OnOrthographicModeToggled(checkBox); + cameraCM.Hide(); + } + }; + cameraCM.VisibleChanged += control => orthoValue.Checked = _isOrtho; + } + + // Field of View + { + var fov = cameraCM.AddButton("Field Of View"); + fov.CloseMenuOnClick = false; + var fovValue = new FloatValueBox(_fieldOfView, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f) + { + Parent = fov + }; + + fovValue.ValueChanged += () => OnFieldOfViewChanged(fovValue); + cameraCM.VisibleChanged += control => + { + fov.Visible = !_isOrtho; + fovValue.Value = _fieldOfView; + }; + } + + // Orthographic Scale + { + var orthoSize = cameraCM.AddButton("Ortho Scale"); + orthoSize.CloseMenuOnClick = false; + var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f) + { + Parent = orthoSize + }; + + orthoSizeValue.ValueChanged += () => OnOrthographicSizeChanged(orthoSizeValue); + cameraCM.VisibleChanged += control => + { + orthoSize.Visible = _isOrtho; + orthoSizeValue.Value = _orthoSize; + }; + } + + // Near Plane + { + var nearPlane = cameraCM.AddButton("Near Plane"); + nearPlane.CloseMenuOnClick = false; + var nearPlaneValue = new FloatValueBox(_nearPlane, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f) + { + Parent = nearPlane + }; + + nearPlaneValue.ValueChanged += () => OnNearPlaneChanged(nearPlaneValue); + cameraCM.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; + } + + // Far Plane + { + var farPlane = cameraCM.AddButton("Far Plane"); + farPlane.CloseMenuOnClick = false; + var farPlaneValue = new FloatValueBox(_farPlane, xLocationForExtras, 2, 70.0f, 10.0f) + { + Parent = farPlane + }; + + farPlaneValue.ValueChanged += () => OnFarPlaneChanged(farPlaneValue); + cameraCM.VisibleChanged += control => farPlaneValue.Value = _farPlane; + } + + cameraCM.AddSeparator(); + + // Reset Button + { + var reset = cameraCM.AddButton("Reset to default"); + reset.ButtonClicked += button => + { + SetupViewportOptions(); + + // if the context menu is opened without triggering the value changes beforehand, + // the movement speed will not be correctly reset to its default value in certain cases + // therefore, a UI update needs to be triggered here + minCamSpeedValue.Value = _minMovementSpeed; + camSpeedValue.Value = _movementSpeed; + maxCamSpeedValue.Value = _maxMovementSpeed; + }; + } + + #endregion Camera settings widget + + #region View mode widget + + largestText = "Brightness"; + textSize = Style.Current.FontMedium.MeasureText(largestText); + xLocationForExtras = textSize.X + 5; - // View mode widget var viewMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft); ViewWidgetButtonMenu = new ContextMenu(); var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu) @@ -484,8 +820,8 @@ namespace FlaxEditor.Viewport // Show FPS { InitFpsCounter(); - _showFpsButon = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter); - _showFpsButon.CloseMenuOnClick = false; + _showFpsButton = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter); + _showFpsButton.CloseMenuOnClick = false; } } @@ -610,104 +946,6 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu.AddSeparator(); - // Orthographic - { - var ortho = ViewWidgetButtonMenu.AddButton("Orthographic"); - ortho.CloseMenuOnClick = false; - var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho) - { - Parent = ortho - }; - orthoValue.StateChanged += checkBox => - { - if (checkBox.Checked != _isOrtho) - { - _isOrtho = checkBox.Checked; - ViewWidgetButtonMenu.Hide(); - if (_isOrtho) - { - var orient = ViewOrientation; - OrientViewport(ref orient); - } - } - }; - ViewWidgetButtonMenu.VisibleChanged += control => orthoValue.Checked = _isOrtho; - } - - // Camera Viewpoints - { - var cameraView = ViewWidgetButtonMenu.AddChildMenu("Viewpoints").ContextMenu; - for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) - { - var co = EditorViewportCameraViewpointValues[i]; - var button = cameraView.AddButton(co.Name); - button.Tag = co.Orientation; - } - cameraView.ButtonClicked += button => - { - var orient = Quaternion.Euler((Float3)button.Tag); - OrientViewport(ref orient); - }; - } - - // Field of View - { - var fov = ViewWidgetButtonMenu.AddButton("Field Of View"); - fov.CloseMenuOnClick = false; - var fovValue = new FloatValueBox(1, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f) - { - Parent = fov - }; - - fovValue.ValueChanged += () => _fieldOfView = fovValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => - { - fov.Visible = !_isOrtho; - fovValue.Value = _fieldOfView; - }; - } - - // Ortho Scale - { - var orthoSize = ViewWidgetButtonMenu.AddButton("Ortho Scale"); - orthoSize.CloseMenuOnClick = false; - var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f) - { - Parent = orthoSize - }; - - orthoSizeValue.ValueChanged += () => _orthoSize = orthoSizeValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => - { - orthoSize.Visible = _isOrtho; - orthoSizeValue.Value = _orthoSize; - }; - } - - // Near Plane - { - var nearPlane = ViewWidgetButtonMenu.AddButton("Near Plane"); - nearPlane.CloseMenuOnClick = false; - var nearPlaneValue = new FloatValueBox(2.0f, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f) - { - Parent = nearPlane - }; - nearPlaneValue.ValueChanged += () => _nearPlane = nearPlaneValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; - } - - // Far Plane - { - var farPlane = ViewWidgetButtonMenu.AddButton("Far Plane"); - farPlane.CloseMenuOnClick = false; - var farPlaneValue = new FloatValueBox(1000, xLocationForExtras, 2, 70.0f, 10.0f) - { - Parent = farPlane - }; - farPlaneValue.ValueChanged += () => _farPlane = farPlaneValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => farPlaneValue.Value = _farPlane; - } - // Brightness { var brightness = ViewWidgetButtonMenu.AddButton("Brightness"); @@ -732,24 +970,7 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu.VisibleChanged += control => resolutionValue.Value = ResolutionScale; } - // Invert Panning - { - var invert = ViewWidgetButtonMenu.AddButton("Invert Panning"); - invert.CloseMenuOnClick = false; - var invertValue = new CheckBox(xLocationForExtras, 2, _invertPanning) - { - Parent = invert - }; - - invertValue.StateChanged += checkBox => - { - if (checkBox.Checked != _invertPanning) - { - _invertPanning = checkBox.Checked; - } - }; - ViewWidgetButtonMenu.VisibleChanged += control => invertValue.Checked = _invertPanning; - } + #endregion View mode widget } InputActions.Add(options => options.ViewpointTop, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Top").Orientation))); @@ -766,6 +987,135 @@ namespace FlaxEditor.Viewport task.Begin += OnRenderBegin; } + /// + /// Sets the viewport options to the default values. + /// + private void SetupViewportOptions() + { + var options = Editor.Instance.Options.Options; + _minMovementSpeed = options.Viewport.MinMovementSpeed; + MovementSpeed = options.Viewport.MovementSpeed; + _maxMovementSpeed = options.Viewport.MaxMovementSpeed; + _useCameraEasing = options.Viewport.UseCameraEasing; + _panningSpeed = options.Viewport.PanningSpeed; + _invertPanning = options.Viewport.InvertPanning; + _relativePanning = options.Viewport.UseRelativePanning; + + _isOrtho = options.Viewport.UseOrthographicProjection; + _orthoSize = options.Viewport.OrthographicScale; + _fieldOfView = options.Viewport.FieldOfView; + _nearPlane = options.Viewport.NearPlane; + _farPlane = options.Viewport.FarPlane; + + OnEditorOptionsChanged(options); + } + + private void OnMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, _minMovementSpeed, _maxMovementSpeed); + MovementSpeed = value; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); + } + + private void OnMinMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, 0.05f, _maxMovementSpeed); + _minMovementSpeed = value; + + if (_movementSpeed < value) + MovementSpeed = value; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("CameraMinMovementSpeedValue", _minMovementSpeed.ToString()); + } + + private void OnMaxMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, _minMovementSpeed, 1000.0f); + _maxMovementSpeed = value; + + if (_movementSpeed > value) + MovementSpeed = value; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString()); + } + + private void OnCameraEasingToggled(Control control) + { + _useCameraEasing = !_useCameraEasing; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("UseCameraEasingState", _useCameraEasing.ToString()); + } + + private void OnPanningSpeedChanged(FloatValueBox control) + { + _panningSpeed = control.Value; + _editor.ProjectCache.SetCustomData("CameraPanningSpeedValue", _panningSpeed.ToString()); + } + + private void OnRelativePanningToggled(Control control) + { + _relativePanning = !_relativePanning; + _editor.ProjectCache.SetCustomData("CameraRelativePanningState", _relativePanning.ToString()); + } + + private void OnInvertPanningToggled(Control control) + { + _invertPanning = !_invertPanning; + _editor.ProjectCache.SetCustomData("CameraInvertPanningState", _invertPanning.ToString()); + } + + + private void OnViewpointChanged(ContextMenuButton button) + { + var orient = Quaternion.Euler((Float3)button.Tag); + OrientViewport(ref orient); + } + + private void OnFieldOfViewChanged(FloatValueBox control) + { + _fieldOfView = control.Value; + _editor.ProjectCache.SetCustomData("CameraFieldOfViewValue", _fieldOfView.ToString()); + } + + private void OnOrthographicModeToggled(Control control) + { + _isOrtho = !_isOrtho; + + if (_orthographicModeButton != null) + _orthographicModeButton.Checked = !_isOrtho; + + if (_isOrtho) + { + var orient = ViewOrientation; + OrientViewport(ref orient); + } + + _editor.ProjectCache.SetCustomData("CameraOrthographicState", _isOrtho.ToString()); + } + + private void OnOrthographicSizeChanged(FloatValueBox control) + { + _orthoSize = control.Value; + _editor.ProjectCache.SetCustomData("CameraOrthographicSizeValue", _orthoSize.ToString()); + } + + private void OnNearPlaneChanged(FloatValueBox control) + { + _nearPlane = control.Value; + _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _nearPlane.ToString()); + } + + private void OnFarPlaneChanged(FloatValueBox control) + { + _farPlane = control.Value; + _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _farPlane.ToString()); + } + /// /// Gets a value indicating whether this viewport is using mouse currently (eg. user moving objects). /// @@ -798,33 +1148,59 @@ namespace FlaxEditor.Viewport } } + private void OnCameraMovementProgressChanged() + { + // prevent NaN + if (Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon) + { + _speedStep = 0; + return; + } + + if (Math.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon) + { + _speedStep = _maxSpeedSteps; + return; + } + else if (Math.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon) + { + _speedStep = 0; + return; + } + + // calculate current linear/eased progress + var progress = Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); + + if (_useCameraEasing) + progress = Mathf.Pow(progress, 1.0f / _cameraEasingDegree); + + _speedStep = Mathf.RoundToInt(progress * _maxSpeedSteps); + } + /// /// Increases or decreases the camera movement speed. /// /// The stepping direction for speed adjustment. protected void AdjustCameraMoveSpeed(int step) { - int camValueIndex = -1; - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - if (Mathf.NearEqual(EditorViewportCameraSpeedValues[i], _movementSpeed)) - { - camValueIndex = i; - break; - } - } - if (camValueIndex == -1) - return; + _speedStep = Mathf.Clamp(_speedStep + step, 0, _maxSpeedSteps); - if (step > 0) - MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Min(camValueIndex + 1, EditorViewportCameraSpeedValues.Length - 1)]; - else if (step < 0) - MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Max(camValueIndex - 1, 0)]; + // calculate new linear/eased progress + var progress = _useCameraEasing + ? Mathf.Pow((float)_speedStep / _maxSpeedSteps, _cameraEasingDegree) + : (float)_speedStep / _maxSpeedSteps; + + var speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, progress); + MovementSpeed = (float)Math.Round(speed, 3); + _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); } private void OnEditorOptionsChanged(EditorOptions options) { _mouseSensitivity = options.Viewport.MouseSensitivity; + _maxSpeedSteps = options.Viewport.TotalCameraSpeedSteps; + _cameraEasingDegree = options.Viewport.CameraEasingDegree; + OnCameraMovementProgressChanged(); } private void OnRenderBegin(RenderTask task, GPUContext context) @@ -863,7 +1239,7 @@ namespace FlaxEditor.Viewport } private FpsCounter _fpsCounter; - private ContextMenuButton _showFpsButon; + private ContextMenuButton _showFpsButton; /// /// Gets or sets a value indicating whether show or hide FPS counter. @@ -875,7 +1251,7 @@ namespace FlaxEditor.Viewport { _fpsCounter.Visible = value; _fpsCounter.Enabled = value; - _showFpsButon.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + _showFpsButton.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } @@ -1016,8 +1392,6 @@ namespace FlaxEditor.Viewport /// The parent window. protected virtual void OnControlMouseBegin(Window win) { - _wheelMovementChangeDeltaSum = 0; - // Hide cursor and start tracking mouse movement win.StartTrackingMouse(false); win.Cursor = CursorType.Hidden; @@ -1113,8 +1487,8 @@ namespace FlaxEditor.Viewport _camera.Update(deltaTime); useMovementSpeed = _camera.UseMovementSpeed; - if (_speedWidget != null) - _speedWidget.Parent.Visible = useMovementSpeed; + if (_cameraButton != null) + _cameraButton.Parent.Visible = useMovementSpeed; } // Get parent window @@ -1217,18 +1591,8 @@ namespace FlaxEditor.Viewport rmbWheel = useMovementSpeed && (_input.IsMouseRightDown || _isVirtualMouseRightDown) && wheelInUse; if (rmbWheel) { - const float step = 4.0f; - _wheelMovementChangeDeltaSum += _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity; - if (_wheelMovementChangeDeltaSum >= step) - { - _wheelMovementChangeDeltaSum -= step; - AdjustCameraMoveSpeed(1); - } - else if (_wheelMovementChangeDeltaSum <= -step) - { - _wheelMovementChangeDeltaSum += step; - AdjustCameraMoveSpeed(-1); - } + var step = _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity; + AdjustCameraMoveSpeed(step > 0.0f ? 1 : -1); } } @@ -1497,22 +1861,6 @@ namespace FlaxEditor.Viewport new CameraViewpoint("Bottom", new Float3(-90, 0, 0)) }; - private readonly float[] EditorViewportCameraSpeedValues = - { - 0.05f, - 0.1f, - 0.25f, - 0.5f, - 1.0f, - 2.0f, - 4.0f, - 6.0f, - 8.0f, - 16.0f, - 32.0f, - 64.0f, - }; - private struct ViewModeOptions { public readonly string Name; @@ -1568,24 +1916,6 @@ namespace FlaxEditor.Viewport new ViewModeOptions(ViewMode.GlobalIllumination, "Global Illumination"), }; - private void WidgetCamSpeedShowHide(Control cm) - { - if (cm.Visible == false) - return; - - var ccm = (ContextMenu)cm; - foreach (var e in ccm.Items) - { - if (e is ContextMenuButton b) - { - var v = (float)b.Tag; - b.Icon = Mathf.Abs(MovementSpeed - v) < 0.001f - ? Style.Current.CheckBoxTick - : SpriteHandle.Invalid; - } - } - } - private void WidgetViewModeShowHideClicked(ContextMenuButton button) { if (button.Tag is ViewMode v) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index e85b6c2b3..24c8f1c4c 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; -using FlaxEditor.GUI.Drag; using FlaxEditor.SceneGraph; -using FlaxEditor.SceneGraph.Actors; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Modes; @@ -37,28 +35,8 @@ namespace FlaxEditor.Viewport private readonly ViewportWidgetButton _rotateSnapping; private readonly ViewportWidgetButton _scaleSnapping; - private readonly DragAssets _dragAssets; - private readonly DragActorType _dragActorType = new DragActorType(ValidateDragActorType); - private SelectionOutline _customSelectionOutline; - /// - /// The custom drag drop event arguments. - /// - /// - public class DragDropEventArgs : DragEventArgs - { - /// - /// The hit. - /// - public SceneGraphNode Hit; - - /// - /// The hit location. - /// - public Vector3 HitLocation; - } - /// /// The editor sprites rendering effect. /// @@ -137,15 +115,12 @@ namespace FlaxEditor.Viewport private bool _lockedFocus; private double _lockedFocusOffset; private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); - private StaticModel _previewStaticModel; - private int _previewModelEntryIndex; - private BrushSurface _previewBrushSurface; private EditorSpritesRenderer _editorSpritesRenderer; /// /// Drag and drop handlers /// - public readonly DragHandlers DragHandlers = new DragHandlers(); + public readonly ViewportDragHandlers DragHandlers; /// /// The transform gizmo. @@ -219,7 +194,7 @@ namespace FlaxEditor.Viewport : base(Object.New(), editor.Undo, editor.Scene.Root) { _editor = editor; - _dragAssets = new DragAssets(ValidateDragItem); + DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType); var inputOptions = editor.Options.Options.Input; // Prepare rendering task @@ -408,9 +383,6 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu.AddSeparator(); ViewWidgetButtonMenu.AddButton("Create camera here", CreateCameraAtView); - DragHandlers.Add(_dragActorType); - DragHandlers.Add(_dragAssets); - // Init gizmo modes { // Add default modes used by the editor @@ -430,7 +402,11 @@ namespace FlaxEditor.Viewport InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate); InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate); InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale); - InputActions.Add(options => options.ToggleTransformSpace, () => { OnTransformSpaceToggle(transformSpaceToggle); transformSpaceToggle.Checked = !transformSpaceToggle.Checked; }); + InputActions.Add(options => options.ToggleTransformSpace, () => + { + OnTransformSpaceToggle(transformSpaceToggle); + transformSpaceToggle.Checked = !transformSpaceToggle.Checked; + }); InputActions.Add(options => options.LockFocusSelection, LockFocusSelection); InputActions.Add(options => options.FocusSelection, FocusSelection); InputActions.Add(options => options.RotateSelection, RotateSelection); @@ -530,20 +506,9 @@ namespace FlaxEditor.Viewport private void OnCollectDrawCalls(ref RenderContext renderContext) { - if (_previewStaticModel) - { - _debugDrawData.HighlightModel(_previewStaticModel, _previewModelEntryIndex); - } - if (_previewBrushSurface.Brush != null) - { - _debugDrawData.HighlightBrushSurface(_previewBrushSurface); - } - + DragHandlers.CollectDrawCalls(_debugDrawData, ref renderContext); if (ShowNavigation) - { Editor.Internal_DrawNavMesh(); - } - _debugDrawData.OnDraw(ref renderContext); } @@ -942,78 +907,14 @@ namespace FlaxEditor.Viewport base.OnLeftMouseButtonUp(); } - private void GetHitLocation(ref Float2 location, out SceneGraphNode hit, out Vector3 hitLocation, out Vector3 hitNormal) - { - // Get mouse ray and try to hit any object - var ray = ConvertMouseToRay(ref location); - var view = new Ray(ViewPosition, ViewDirection); - var gridPlane = new Plane(Vector3.Zero, Vector3.Up); - var flags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives; - hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out var closest, out var normal, flags); - if (hit != null) - { - // Use hit location - hitLocation = ray.Position + ray.Direction * closest; - hitNormal = normal; - } - else if (Grid.Enabled && CollisionsHelper.RayIntersectsPlane(ref ray, ref gridPlane, out closest) && closest < 4000.0f) - { - // Use grid location - hitLocation = ray.Position + ray.Direction * closest; - hitNormal = Vector3.Up; - } - else - { - // Use area in front of the viewport - hitLocation = ViewPosition + ViewDirection * 100; - hitNormal = Vector3.Up; - } - } - - private void SetDragEffects(ref Float2 location) - { - if (_dragAssets.HasValidDrag && _dragAssets.Objects[0].IsOfType()) - { - GetHitLocation(ref location, out var hit, out _, out _); - ClearDragEffects(); - var material = FlaxEngine.Content.LoadAsync(_dragAssets.Objects[0].ID); - if (material.IsDecal) - return; - - if (hit is StaticModelNode staticModelNode) - { - _previewStaticModel = (StaticModel)staticModelNode.Actor; - var ray = ConvertMouseToRay(ref location); - _previewStaticModel.IntersectsEntry(ref ray, out _, out _, out _previewModelEntryIndex); - } - else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode) - { - _previewBrushSurface = brushSurfaceNode.Surface; - } - } - } - - private void ClearDragEffects() - { - _previewStaticModel = null; - _previewModelEntryIndex = -1; - _previewBrushSurface = new BrushSurface(); - } - /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { - ClearDragEffects(); - + DragHandlers.ClearDragEffects(); var result = base.OnDragEnter(ref location, data); if (result != DragDropEffect.None) return result; - - result = DragHandlers.OnDragEnter(data); - - SetDragEffects(ref location); - - return result; + return DragHandlers.DragEnter(ref location, data); } private bool ValidateDragItem(ContentItem contentItem) @@ -1042,167 +943,29 @@ namespace FlaxEditor.Viewport /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { - ClearDragEffects(); - + DragHandlers.ClearDragEffects(); var result = base.OnDragMove(ref location, data); if (result != DragDropEffect.None) return result; - - SetDragEffects(ref location); - - return DragHandlers.Effect; + return DragHandlers.DragEnter(ref location, data); } /// public override void OnDragLeave() { - ClearDragEffects(); - + DragHandlers.ClearDragEffects(); DragHandlers.OnDragLeave(); - base.OnDragLeave(); } - private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation) - { - // Refresh actor position to ensure that cached bounds are valid - actor.Position = Vector3.One; - actor.Position = Vector3.Zero; - - // Place the object - //var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection; - var editorBounds = actor.EditorBoxChildren; - var bottomToCenter = actor.Position.Y - editorBounds.Minimum.Y; - var location = hitLocation + new Vector3(0, bottomToCenter, 0); - - // Apply grid snapping if enabled - if (UseSnapping || TransformGizmo.TranslationSnapEnable) - { - float snapValue = TransformGizmo.TranslationSnapValue; - location = new Vector3( - (int)(location.X / snapValue) * snapValue, - (int)(location.Y / snapValue) * snapValue, - (int)(location.Z / snapValue) * snapValue); - } - - return location; - } - - private void Spawn(Actor actor, ref Vector3 hitLocation, ref Vector3 hitNormal) - { - actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); - var parent = actor.Parent ?? Level.GetScene(0); - actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parent.GetChild(x) == null); - Editor.Instance.SceneEditing.Spawn(actor); - Focus(); - } - - private void Spawn(AssetItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) - { - if (item.IsOfType()) - { - var material = FlaxEngine.Content.LoadAsync(item.ID); - if (material && !material.WaitForLoaded(100) && material.IsDecal) - { - var actor = new Decal - { - Material = material, - LocalOrientation = RootNode.RaycastNormalRotation(ref hitNormal), - Name = item.ShortName - }; - DebugDraw.DrawWireArrow(PostProcessSpawnedActorLocation(actor, ref hitNormal), actor.LocalOrientation, 1.0f, Color.Red, 1000000); - Spawn(actor, ref hitLocation, ref hitNormal); - } - else if (hit is StaticModelNode staticModelNode) - { - var staticModel = (StaticModel)staticModelNode.Actor; - var ray = ConvertMouseToRay(ref location); - if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex)) - { - using (new UndoBlock(Undo, staticModel, "Change material")) - staticModel.SetMaterial(entryIndex, material); - } - } - else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode) - { - using (new UndoBlock(Undo, brushSurfaceNode.Brush, "Change material")) - { - var surface = brushSurfaceNode.Surface; - surface.Material = material; - brushSurfaceNode.Surface = surface; - } - } - return; - } - if (item.IsOfType()) - { - Editor.Instance.Scene.OpenScene(item.ID, true); - return; - } - { - var actor = item.OnEditorDrop(this); - actor.Name = item.ShortName; - Spawn(actor, ref hitLocation, ref hitNormal); - } - } - - private void Spawn(ScriptType item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) - { - var actor = item.CreateInstance() as Actor; - if (actor == null) - { - Editor.LogWarning("Failed to spawn actor of type " + item.TypeName); - return; - } - actor.Name = item.Name; - Spawn(actor, ref hitLocation, ref hitNormal); - } - /// public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) { - ClearDragEffects(); - + DragHandlers.ClearDragEffects(); var result = base.OnDragDrop(ref location, data); if (result != DragDropEffect.None) return result; - - // Check if drag sth - Vector3 hitLocation = ViewPosition, hitNormal = -ViewDirection; - SceneGraphNode hit = null; - if (DragHandlers.HasValidDrag) - { - GetHitLocation(ref location, out hit, out hitLocation, out hitNormal); - } - - // Drag assets - if (_dragAssets.HasValidDrag) - { - result = _dragAssets.Effect; - - // Process items - for (int i = 0; i < _dragAssets.Objects.Count; i++) - { - var item = _dragAssets.Objects[i]; - Spawn(item, hit, ref location, ref hitLocation, ref hitNormal); - } - } - // Drag actor type - else if (_dragActorType.HasValidDrag) - { - result = _dragActorType.Effect; - - // Process items - for (int i = 0; i < _dragActorType.Objects.Count; i++) - { - var item = _dragActorType.Objects[i]; - Spawn(item, hit, ref location, ref hitLocation, ref hitNormal); - } - } - - DragHandlers.OnDragDrop(new DragDropEventArgs { Hit = hit, HitLocation = hitLocation }); - - return result; + return DragHandlers.DragDrop(ref location, data); } /// @@ -1211,6 +974,14 @@ namespace FlaxEditor.Viewport _editor.SceneEditing.Select(nodes); } + /// + public override void Spawn(Actor actor) + { + var parent = actor.Parent ?? Level.GetScene(0); + actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parent.GetChild(x) == null); + Editor.Instance.SceneEditing.Spawn(actor); + } + /// public override void OnDestroy() { diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index eb462deb1..6e111f588 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; -using FlaxEditor.GUI.Drag; using FlaxEditor.SceneGraph; -using FlaxEditor.SceneGraph.Actors; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Previews; @@ -56,9 +54,11 @@ namespace FlaxEditor.Viewport private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private PrefabSpritesRenderer _spritesRenderer; - private readonly DragAssets _dragAssets; - private readonly DragActorType _dragActorType = new DragActorType(ValidateDragActorType); - private readonly DragHandlers _dragHandlers = new DragHandlers(); + + /// + /// Drag and drop handlers + /// + public readonly ViewportDragHandlers DragHandlers; /// /// The transform gizmo. @@ -81,7 +81,7 @@ namespace FlaxEditor.Viewport _window.SelectionChanged += OnSelectionChanged; Undo = window.Undo; ViewportCamera = new FPSCamera(); - _dragAssets = new DragAssets(ValidateDragItem); + DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType); ShowDebugDraw = true; ShowEditorPrimitives = true; Gizmos = new GizmosCollection(this); @@ -228,14 +228,15 @@ namespace FlaxEditor.Viewport _gizmoModeScale.Toggled += OnGizmoModeToggle; gizmoMode.Parent = this; - _dragHandlers.Add(_dragActorType); - _dragHandlers.Add(_dragAssets); - // Setup input actions InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate); InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate); InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale); - InputActions.Add(options => options.ToggleTransformSpace, () => { OnTransformSpaceToggle(transformSpaceToggle); transformSpaceToggle.Checked = !transformSpaceToggle.Checked; }); + InputActions.Add(options => options.ToggleTransformSpace, () => + { + OnTransformSpaceToggle(transformSpaceToggle); + transformSpaceToggle.Checked = !transformSpaceToggle.Checked; + }); InputActions.Add(options => options.FocusSelection, ShowSelectedActors); SetUpdate(ref _update, OnUpdate); @@ -267,6 +268,7 @@ namespace FlaxEditor.Viewport private void OnCollectDrawCalls(ref RenderContext renderContext) { + DragHandlers.CollectDrawCalls(_debugDrawData, ref renderContext); _debugDrawData.OnDraw(ref renderContext); } @@ -306,6 +308,7 @@ namespace FlaxEditor.Viewport var orient = ViewOrientation; ((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orient); } + /// public EditorViewport Viewport => this; @@ -354,6 +357,12 @@ namespace FlaxEditor.Viewport _window.Select(nodes); } + /// + public void Spawn(Actor actor) + { + _window.Spawn(actor); + } + /// protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false; @@ -669,11 +678,11 @@ namespace FlaxEditor.Viewport /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { + DragHandlers.ClearDragEffects(); var result = base.OnDragEnter(ref location, data); if (result != DragDropEffect.None) return result; - - return _dragHandlers.OnDragEnter(data); + return DragHandlers.DragEnter(ref location, data); } private bool ValidateDragItem(ContentItem contentItem) @@ -685,7 +694,6 @@ namespace FlaxEditor.Viewport if (assetItem.IsOfType()) return true; } - return false; } @@ -697,86 +705,21 @@ namespace FlaxEditor.Viewport /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { + DragHandlers.ClearDragEffects(); var result = base.OnDragMove(ref location, data); if (result != DragDropEffect.None) return result; - - return _dragHandlers.Effect; + return DragHandlers.DragEnter(ref location, data); } /// public override void OnDragLeave() { - _dragHandlers.OnDragLeave(); - + DragHandlers.ClearDragEffects(); + DragHandlers.OnDragLeave(); base.OnDragLeave(); } - private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation) - { - // Place the object - //var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection; - var location = hitLocation; - - // Apply grid snapping if enabled - if (UseSnapping || TransformGizmo.TranslationSnapEnable) - { - float snapValue = TransformGizmo.TranslationSnapValue; - location = new Vector3( - (int)(location.X / snapValue) * snapValue, - (int)(location.Y / snapValue) * snapValue, - (int)(location.Z / snapValue) * snapValue); - } - - return location; - } - - private void Spawn(AssetItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation) - { - if (item is BinaryAssetItem binaryAssetItem) - { - if (typeof(MaterialBase).IsAssignableFrom(binaryAssetItem.Type)) - { - if (hit is StaticModelNode staticModelNode) - { - var staticModel = (StaticModel)staticModelNode.Actor; - var ray = ConvertMouseToRay(ref location); - if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex)) - { - var material = FlaxEngine.Content.LoadAsync(item.ID); - using (new UndoBlock(Undo, staticModel, "Change material")) - staticModel.SetMaterial(entryIndex, material); - } - } - return; - } - } - { - var actor = item.OnEditorDrop(this); - actor.Name = item.ShortName; - Spawn(actor, ref hitLocation); - } - } - - private void Spawn(Actor actor, ref Vector3 hitLocation) - { - actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); - _window.Spawn(actor); - Focus(); - } - - private void Spawn(ScriptType item, SceneGraphNode hit, ref Vector3 hitLocation) - { - var actor = item.CreateInstance() as Actor; - if (actor == null) - { - Editor.LogWarning("Failed to spawn actor of type " + item.TypeName); - return; - } - actor.Name = item.Name; - Spawn(actor, ref hitLocation); - } - /// /// Focuses the viewport on the current selection of the gizmo. /// @@ -814,57 +757,11 @@ namespace FlaxEditor.Viewport /// public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) { + DragHandlers.ClearDragEffects(); var result = base.OnDragDrop(ref location, data); if (result != DragDropEffect.None) return result; - - // Check if drag sth - Vector3 hitLocation = ViewPosition; - SceneGraphNode hit = null; - if (_dragHandlers.HasValidDrag) - { - // Get mouse ray and try to hit any object - var ray = ConvertMouseToRay(ref location); - var view = new Ray(ViewPosition, ViewDirection); - hit = _window.Graph.Root.RayCast(ref ray, ref view, out var closest, SceneGraphNode.RayCastData.FlagTypes.SkipColliders); - if (hit != null) - { - // Use hit location - hitLocation = ray.Position + ray.Direction * closest; - } - else - { - // Use area in front of the viewport - hitLocation = ViewPosition + ViewDirection * 100; - } - } - - // Drag assets - if (_dragAssets.HasValidDrag) - { - result = _dragAssets.Effect; - - // Process items - for (int i = 0; i < _dragAssets.Objects.Count; i++) - { - var item = _dragAssets.Objects[i]; - Spawn(item, hit, ref location, ref hitLocation); - } - } - // Drag actor type - else if (_dragActorType.HasValidDrag) - { - result = _dragActorType.Effect; - - // Process items - for (int i = 0; i < _dragActorType.Objects.Count; i++) - { - var item = _dragActorType.Objects[i]; - Spawn(item, hit, ref hitLocation); - } - } - - return result; + return DragHandlers.DragDrop(ref location, data); } /// diff --git a/Source/Editor/Viewport/Previews/MaterialPreview.cs b/Source/Editor/Viewport/Previews/MaterialPreview.cs index 5520d6c4c..46aac1cdf 100644 --- a/Source/Editor/Viewport/Previews/MaterialPreview.cs +++ b/Source/Editor/Viewport/Previews/MaterialPreview.cs @@ -7,6 +7,8 @@ using FlaxEngine.GUI; using FlaxEditor.Viewport.Widgets; using FlaxEditor.GUI.ContextMenu; using Object = FlaxEngine.Object; +using FlaxEditor.GUI; +using FlaxEditor.Scripting; namespace FlaxEditor.Viewport.Previews { @@ -49,6 +51,8 @@ namespace FlaxEditor.Viewport.Previews private Image _guiMaterialControl; private readonly MaterialBase[] _postFxMaterialsCache = new MaterialBase[1]; private ContextMenu _modelWidgetButtonMenu; + private AssetPicker _customModelPicker; + private Model _customModel; /// /// Gets or sets the material asset to preview. It can be or . @@ -74,15 +78,66 @@ namespace FlaxEditor.Viewport.Previews get => _selectedModelIndex; set { + if (value == -1) // Using Custom Model + return; if (value < 0 || value > Models.Length) throw new ArgumentOutOfRangeException(); + if (_customModelPicker != null) + _customModelPicker.Validator.SelectedAsset = null; _selectedModelIndex = value; _previewModel.Model = FlaxEngine.Content.LoadAsyncInternal("Editor/Primitives/" + Models[value]); _previewModel.Transform = Transforms[value]; } } + // Used to automatically update which entry is checked. + // TODO: Maybe a better system with predicate bool checks could be used? + private void ResetModelContextMenu() + { + _modelWidgetButtonMenu.ItemsContainer.DisposeChildren(); + + // Fill out all models + for (int i = 0; i < Models.Length; i++) + { + var index = i; + var button = _modelWidgetButtonMenu.AddButton(Models[index]); + button.ButtonClicked += _ => SelectedModelIndex = index; + button.Checked = SelectedModelIndex == index && _customModel == null; + button.Tag = index; + } + + _modelWidgetButtonMenu.AddSeparator(); + _customModelPicker = new AssetPicker(new ScriptType(typeof(Model)), Float2.Zero); + + // Label button + var customModelPickerLabel = _modelWidgetButtonMenu.AddButton("Custom Model:"); + customModelPickerLabel.CloseMenuOnClick = false; + customModelPickerLabel.Checked = _customModel != null; + + // Container button + var customModelPickerButton = _modelWidgetButtonMenu.AddButton(""); + customModelPickerButton.Height = _customModelPicker.Height + 4; + customModelPickerButton.CloseMenuOnClick = false; + _customModelPicker.Parent = customModelPickerButton; + _customModelPicker.Validator.SelectedAsset = _customModel; + _customModelPicker.SelectedItemChanged += () => + { + _customModel = _customModelPicker.Validator.SelectedAsset as Model; + if (_customModelPicker.Validator.SelectedAsset == null) + { + SelectedModelIndex = 0; + ResetModelContextMenu(); + return; + } + + _previewModel.Model = _customModel; + _previewModel.Transform = Transforms[0]; + SelectedModelIndex = -1; + ResetModelContextMenu(); + }; + } + /// /// Initializes a new instance of the class. /// @@ -107,17 +162,7 @@ namespace FlaxEditor.Viewport.Previews { if (!control.Visible) return; - _modelWidgetButtonMenu.ItemsContainer.DisposeChildren(); - - // Fill out all models - for (int i = 0; i < Models.Length; i++) - { - var index = i; - var button = _modelWidgetButtonMenu.AddButton(Models[index]); - button.ButtonClicked += _ => SelectedModelIndex = index; - button.Checked = SelectedModelIndex == index; - button.Tag = index; - } + ResetModelContextMenu(); }; new ViewportWidgetButton("Model", SpriteHandle.Invalid, _modelWidgetButtonMenu) { diff --git a/Source/Editor/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs new file mode 100644 index 000000000..8a1b4f183 --- /dev/null +++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs @@ -0,0 +1,257 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.Linq; +using FlaxEditor.Content; +using FlaxEditor.Gizmo; +using FlaxEditor.GUI.Drag; +using FlaxEditor.SceneGraph; +using FlaxEditor.SceneGraph.Actors; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Viewport +{ + /// + /// Utility to help managing dragging assets, actors and other objects into the editor viewport. + /// + public class ViewportDragHandlers : DragHandlers + { + /// + /// The custom drag drop event arguments. + /// + /// + public class DragDropEventArgs : DragEventArgs + { + /// + /// The hit. + /// + public SceneGraphNode Hit; + + /// + /// The hit location. + /// + public Vector3 HitLocation; + } + + private readonly IGizmoOwner _owner; + private readonly EditorViewport _viewport; + private readonly DragAssets _dragAssets; + private readonly DragActorType _dragActorType; + + private StaticModel _previewStaticModel; + private int _previewModelEntryIndex; + private BrushSurface _previewBrushSurface; + + internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func validateAsset, Func validateDragActorType) + { + _owner = owner; + _viewport = viewport; + Add(_dragAssets = new DragAssets(validateAsset)); + Add(_dragActorType = new DragActorType(validateDragActorType)); + } + + internal void ClearDragEffects() + { + _previewStaticModel = null; + _previewModelEntryIndex = -1; + _previewBrushSurface = new BrushSurface(); + } + + internal void CollectDrawCalls(ViewportDebugDrawData debugDrawData, ref RenderContext renderContext) + { + if (_previewStaticModel) + debugDrawData.HighlightModel(_previewStaticModel, _previewModelEntryIndex); + if (_previewBrushSurface.Brush != null) + debugDrawData.HighlightBrushSurface(_previewBrushSurface); + } + + internal DragDropEffect DragEnter(ref Float2 location, DragData data) + { + var result = OnDragEnter(data); + SetDragEffects(ref location); + return result; + } + + internal DragDropEffect DragMove(ref Float2 location, DragData data) + { + SetDragEffects(ref location); + return Effect; + } + + internal DragDropEffect DragDrop(ref Float2 location, DragData data) + { + Vector3 hitLocation = _viewport.ViewPosition, hitNormal = -_viewport.ViewDirection; + SceneGraphNode hit = null; + if (HasValidDrag) + { + GetHitLocation(ref location, out hit, out hitLocation, out hitNormal); + } + + var result = DragDropEffect.None; + if (_dragAssets.HasValidDrag) + { + result = _dragAssets.Effect; + foreach (var asset in _dragAssets.Objects) + Spawn(asset, hit, ref location, ref hitLocation, ref hitNormal); + } + else if (_dragActorType.HasValidDrag) + { + result = _dragActorType.Effect; + foreach (var actorType in _dragActorType.Objects) + Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal); + } + + OnDragDrop(new DragDropEventArgs { Hit = hit, HitLocation = hitLocation }); + + return result; + } + + private void SetDragEffects(ref Float2 location) + { + if (_dragAssets.HasValidDrag && _dragAssets.Objects[0].IsOfType()) + { + GetHitLocation(ref location, out var hit, out _, out _); + ClearDragEffects(); + var material = FlaxEngine.Content.LoadAsync(_dragAssets.Objects[0].ID); + if (material.IsDecal) + return; + + if (hit is StaticModelNode staticModelNode) + { + _previewStaticModel = (StaticModel)staticModelNode.Actor; + var ray = _viewport.ConvertMouseToRay(ref location); + _previewStaticModel.IntersectsEntry(ref ray, out _, out _, out _previewModelEntryIndex); + } + else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode) + { + _previewBrushSurface = brushSurfaceNode.Surface; + } + } + } + + private void GetHitLocation(ref Float2 location, out SceneGraphNode hit, out Vector3 hitLocation, out Vector3 hitNormal) + { + // Get mouse ray and try to hit any object + var ray = _viewport.ConvertMouseToRay(ref location); + var view = new Ray(_viewport.ViewPosition, _viewport.ViewDirection); + var gridPlane = new Plane(Vector3.Zero, Vector3.Up); + var flags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives; + hit = _owner.SceneGraphRoot.RayCast(ref ray, ref view, out var closest, out var normal, flags); + var girdGizmo = (GridGizmo)_owner.Gizmos.FirstOrDefault(x => x is GridGizmo); + if (hit != null) + { + // Use hit location + hitLocation = ray.Position + ray.Direction * closest; + hitNormal = normal; + } + else if (girdGizmo != null && girdGizmo.Enabled && CollisionsHelper.RayIntersectsPlane(ref ray, ref gridPlane, out closest) && closest < 4000.0f) + { + // Use grid location + hitLocation = ray.Position + ray.Direction * closest; + hitNormal = Vector3.Up; + } + else + { + // Use area in front of the viewport + hitLocation = view.GetPoint(100); + hitNormal = Vector3.Up; + } + } + + private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation) + { + // Refresh actor position to ensure that cached bounds are valid + actor.Position = Vector3.One; + actor.Position = Vector3.Zero; + + // Place the object + //var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection; + var editorBounds = actor.EditorBoxChildren; + var bottomToCenter = actor.Position.Y - editorBounds.Minimum.Y; + var location = hitLocation + new Vector3(0, bottomToCenter, 0); + + // Apply grid snapping if enabled + var transformGizmo = (TransformGizmo)_owner.Gizmos.FirstOrDefault(x => x is TransformGizmo); + if (transformGizmo != null && (_owner.UseSnapping || transformGizmo.TranslationSnapEnable)) + { + float snapValue = transformGizmo.TranslationSnapValue; + location = new Vector3( + (int)(location.X / snapValue) * snapValue, + (int)(location.Y / snapValue) * snapValue, + (int)(location.Z / snapValue) * snapValue); + } + + return location; + } + + private void Spawn(Actor actor, ref Vector3 hitLocation, ref Vector3 hitNormal) + { + actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); + _owner.Spawn(actor); + _viewport.Focus(); + } + + private void Spawn(ScriptType item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) + { + var actor = item.CreateInstance() as Actor; + if (actor == null) + { + Editor.LogWarning("Failed to spawn actor of type " + item.TypeName); + return; + } + actor.Name = item.Name; + Spawn(actor, ref hitLocation, ref hitNormal); + } + + private void Spawn(AssetItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) + { + if (item.IsOfType()) + { + var material = FlaxEngine.Content.LoadAsync(item.ID); + if (material && !material.WaitForLoaded(500) && material.IsDecal) + { + var actor = new Decal + { + Material = material, + LocalOrientation = RootNode.RaycastNormalRotation(ref hitNormal), + Name = item.ShortName + }; + DebugDraw.DrawWireArrow(PostProcessSpawnedActorLocation(actor, ref hitNormal), actor.LocalOrientation, 1.0f, Color.Red, 1000000); + Spawn(actor, ref hitLocation, ref hitNormal); + } + else if (hit is StaticModelNode staticModelNode) + { + var staticModel = (StaticModel)staticModelNode.Actor; + var ray = _viewport.ConvertMouseToRay(ref location); + if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex)) + { + using (new UndoBlock(_owner.Undo, staticModel, "Change material")) + staticModel.SetMaterial(entryIndex, material); + } + } + else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode) + { + using (new UndoBlock(_owner.Undo, brushSurfaceNode.Brush, "Change material")) + { + var surface = brushSurfaceNode.Surface; + surface.Material = material; + brushSurfaceNode.Surface = surface; + } + } + return; + } + if (item.IsOfType()) + { + Editor.Instance.Scene.OpenScene(item.ID, true); + return; + } + { + var actor = item.OnEditorDrop(this); + actor.Name = item.ShortName; + Spawn(actor, ref hitLocation, ref hitNormal); + } + } + } +} diff --git a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs index 481bc3f1b..77e94ae53 100644 --- a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs +++ b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs @@ -19,6 +19,7 @@ namespace FlaxEditor.Viewport.Widgets private bool _checked; private bool _autoCheck; private bool _isMosueDown; + private float _forcedTextWidth; /// /// Event fired when user toggles checked state. @@ -63,14 +64,16 @@ namespace FlaxEditor.Viewport.Widgets /// The text. /// The icon. /// The context menu. - /// if set to true will be automatic checked on mouse click. - public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false) - : base(0, 0, CalculateButtonWidth(0, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight) + /// If set to true will be automatic checked on mouse click. + /// Forces the text to be drawn with the specified width. + public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false, float textWidth = 0.0f) + : base(0, 0, CalculateButtonWidth(textWidth, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight) { _text = text; Icon = icon; _cm = contextMenu; _autoCheck = autoCheck; + _forcedTextWidth = textWidth; if (_cm != null) _cm.VisibleChanged += CmOnVisibleChanged; @@ -112,7 +115,7 @@ namespace FlaxEditor.Viewport.Widgets if (Icon.IsValid) { // Draw icon - Render2D.DrawSprite(Icon, iconRect, style.Foreground); + Render2D.DrawSprite(Icon, iconRect, style.ForegroundViewport); // Update text rectangle textRect.Location.X += iconSize; @@ -120,7 +123,7 @@ namespace FlaxEditor.Viewport.Widgets } // Draw text - Render2D.DrawText(style.FontMedium, _text, textRect, style.Foreground * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Center, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, _text, textRect, style.ForegroundViewport * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Center, TextAlignment.Center); } /// @@ -160,7 +163,7 @@ namespace FlaxEditor.Viewport.Widgets var style = Style.Current; if (style != null && style.FontMedium) - Width = CalculateButtonWidth(style.FontMedium.MeasureText(_text).X, Icon.IsValid); + Width = CalculateButtonWidth(_forcedTextWidth > 0.0f ? _forcedTextWidth : style.FontMedium.MeasureText(_text).X, Icon.IsValid); } } } diff --git a/Source/Editor/Windows/AssetReferencesGraphWindow.cs b/Source/Editor/Windows/AssetReferencesGraphWindow.cs index d49896e2e..b9e0e7257 100644 --- a/Source/Editor/Windows/AssetReferencesGraphWindow.cs +++ b/Source/Editor/Windows/AssetReferencesGraphWindow.cs @@ -46,14 +46,14 @@ namespace FlaxEditor.Windows if (asset != null) { var path = asset.Path; - picker.SelectedAsset = asset; + picker.Validator.SelectedAsset = asset; Title = System.IO.Path.GetFileNameWithoutExtension(path); TooltipText = asset.TypeName + '\n' + path; } else { - picker.SelectedID = AssetId; - var assetItem = picker.SelectedItem as AssetItem; + picker.Validator.SelectedID = AssetId; + var assetItem = picker.Validator.SelectedItem as AssetItem; if (assetItem != null) { Title = assetItem.ShortName; diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs index a765c2faa..8f88d93f6 100644 --- a/Source/Editor/Windows/Assets/AnimationWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationWindow.cs @@ -230,6 +230,8 @@ namespace FlaxEditor.Windows.Assets public AnimationWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -265,8 +267,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/animation/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 8cbf6cf75..01ee9cb8a 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -130,6 +130,8 @@ namespace FlaxEditor.Windows.Assets public BehaviorTreeWindow(Editor editor, BinaryAssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -172,10 +174,10 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, _surface.ShowWholeGraph).LinkTooltip("Show whole graph"); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/ai/behavior-trees/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs index 623c4ef5b..a8121162a 100644 --- a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs +++ b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs @@ -395,6 +395,8 @@ namespace FlaxEditor.Windows.Assets public GameplayGlobalsWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + _undo = new Undo(); _undo.ActionDone += OnUndo; _undo.UndoDone += OnUndo; @@ -411,10 +413,10 @@ namespace FlaxEditor.Windows.Assets _proxy = new PropertiesProxy(); _propertiesEditor.Select(_proxy); - _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save asset"); + _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip($"Save asset ({inputOptions.Save})"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _resetButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Rotate32, Reset).LinkTooltip("Resets the variables values to the default values"); diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 097d4992a..a1178fb68 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -34,6 +34,8 @@ namespace FlaxEditor.Windows.Assets public JsonAssetWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -43,8 +45,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); // Panel var panel = new Panel(ScrollBars.Vertical) diff --git a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs index 0de5ce315..85f351fef 100644 --- a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs +++ b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs @@ -126,6 +126,8 @@ namespace FlaxEditor.Windows.Assets public LocalizedStringTableWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -135,8 +137,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(Editor.Icons.Up64, OnExport).LinkTooltip("Export localization table entries for translation to .pot file"); diff --git a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs index 3e0525fb7..775e0c0dc 100644 --- a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs @@ -375,6 +375,8 @@ namespace FlaxEditor.Windows.Assets public MaterialInstanceWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -384,8 +386,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(Editor.Icons.Rotate64, OnRevertAllParameters).LinkTooltip("Revert all the parameters to the default values"); _toolstrip.AddSeparator(); diff --git a/Source/Editor/Windows/Assets/MaterialWindow.cs b/Source/Editor/Windows/Assets/MaterialWindow.cs index 6fbe32d7e..5aa77dbc3 100644 --- a/Source/Editor/Windows/Assets/MaterialWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialWindow.cs @@ -41,8 +41,6 @@ namespace FlaxEditor.Windows.Assets new ScriptType(typeof(Vector3)), new ScriptType(typeof(Vector4)), new ScriptType(typeof(Color)), - new ScriptType(typeof(Quaternion)), - new ScriptType(typeof(Transform)), new ScriptType(typeof(Matrix)), }; diff --git a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs index 17eda1358..4a5e02e6e 100644 --- a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs @@ -306,6 +306,8 @@ namespace FlaxEditor.Windows.Assets public ParticleSystemWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -359,8 +361,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index 70aa1dca3..a8d9ae1be 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -339,7 +339,7 @@ namespace FlaxEditor.Windows.Assets { if (selection.Count != 0) Select(actor); - actor.TreeNode.StartRenaming(this); + actor.TreeNode.StartRenaming(this, _treePanel); } } diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 4face0dc0..f50a832a1 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -94,6 +94,8 @@ namespace FlaxEditor.Windows.Assets public PrefabWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoEvent; @@ -149,6 +151,7 @@ namespace FlaxEditor.Windows.Assets // Prefab structure tree Graph = new LocalSceneGraph(new CustomRootNode(this)); + Graph.Root.TreeNode.Expand(true); _tree = new PrefabTree { Margin = new Margin(0.0f, 0.0f, -16.0f, _treePanel.ScrollBarsSize), // Hide root node @@ -175,12 +178,12 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate (1)"); - _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate (2)"); - _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale (3)"); + _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip($"Change Gizmo tool mode to Translate ({inputOptions.TranslateMode})"); + _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip($"Change Gizmo tool mode to Rotate ({inputOptions.RotateMode})"); + _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})"); _toolstrip.AddSeparator(); _toolStripLiveReload = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, () => LiveReload = !LiveReload).SetChecked(true).SetAutoCheck(true).LinkTooltip("Live changes preview (applies prefab changes on modification by auto)"); @@ -317,7 +320,7 @@ namespace FlaxEditor.Windows.Assets Graph.MainActor = _viewport.Instance; Selection.Clear(); Select(Graph.Main); - Graph.Root.TreeNode.ExpandAll(true); + Graph.Root.TreeNode.Expand(true); _undo.Clear(); ClearEditedFlag(); } @@ -413,7 +416,7 @@ namespace FlaxEditor.Windows.Assets _focusCamera = true; Selection.Clear(); Select(Graph.Main); - Graph.Root.TreeNode.ExpandAll(true); + Graph.Root.TreeNode.Expand(true); _undo.Clear(); ClearEditedFlag(); diff --git a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs index 162944144..05435cc77 100644 --- a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs +++ b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs @@ -627,6 +627,8 @@ namespace FlaxEditor.Windows.Assets public SceneAnimationWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -652,8 +654,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _previewButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, OnPreviewButtonClicked).SetAutoCheck(true).LinkTooltip("If checked, enables live-preview of the animation on a scene while editing"); _renderButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Build64, OnRenderButtonClicked).LinkTooltip("Open the scene animation rendering utility..."); diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs index d8790172b..95827240c 100644 --- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs +++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs @@ -371,7 +371,10 @@ namespace FlaxEditor.Windows.Assets private void OnTreeSelectedChanged(List before, List after) { if (after.Count != 0) - ((SkeletonPropertiesProxy)Values[0]).Window._preview.ShowDebugDraw = true; + { + var proxy = (SkeletonPropertiesProxy)Values[0]; + proxy.Window._preview.ShowDebugDraw = true; + } } private void OnTreeNodeCopyName(ContextMenuButton b) @@ -837,7 +840,7 @@ namespace FlaxEditor.Windows.Assets sourceAssetPicker.CheckValid = CheckSourceAssetValid; sourceAssetPicker.SelectedItemChanged += () => { - proxy.Setups.Add(sourceAssetPicker.SelectedAsset, new SetupProxy()); + proxy.Setups.Add(sourceAssetPicker.Validator.SelectedAsset, new SetupProxy()); proxy.Window.MarkAsEdited(); RebuildLayout(); }; @@ -856,7 +859,7 @@ namespace FlaxEditor.Windows.Assets // Source asset picker var sourceAssetPicker = setupGroup.AddPropertyItem("Source Asset").Custom().CustomControl; - sourceAssetPicker.SelectedAsset = sourceAsset; + sourceAssetPicker.Validator.SelectedAsset = sourceAsset; sourceAssetPicker.CanEdit = false; sourceAssetPicker.Height = 48; @@ -916,12 +919,12 @@ namespace FlaxEditor.Windows.Assets { // Show skeleton asset picker var sourceSkeletonPicker = setupGroup.AddPropertyItem("Skeleton", "Skinned model that contains a skeleton for this animation retargeting.").Custom().CustomControl; - sourceSkeletonPicker.AssetType = new ScriptType(typeof(SkinnedModel)); - sourceSkeletonPicker.SelectedAsset = setup.Value.Skeleton; + sourceSkeletonPicker.Validator.AssetType = new ScriptType(typeof(SkinnedModel)); + sourceSkeletonPicker.Validator.SelectedAsset = setup.Value.Skeleton; sourceSkeletonPicker.Height = 48; sourceSkeletonPicker.SelectedItemChanged += () => { - setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.SelectedAsset; + setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.Validator.SelectedAsset; proxy.Window.MarkAsEdited(); }; } @@ -1045,6 +1048,7 @@ namespace FlaxEditor.Windows.Assets { Proxy = new SkeletonPropertiesProxy(); Presenter.Select(Proxy); + // Draw highlight on selected node window._preview.CustomDebugDraw += OnDebugDraw; } @@ -1146,6 +1150,15 @@ namespace FlaxEditor.Windows.Assets _tabs.AddTab(new RetargetTab(this)); _tabs.AddTab(new ImportTab(this)); + // Automatically show nodes when switching to skeleton page + _tabs.SelectedTabChanged += (tabs) => + { + if (tabs.SelectedTab is SkeletonTab) + { + _preview.ShowNodes = true; + } + }; + // Highlight actor (used to highlight selected material slot, see UpdateEffectsOnAsset) _highlightActor = new AnimatedModel { diff --git a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs index 7ae6ae8be..e3575d0e8 100644 --- a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs +++ b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs @@ -61,6 +61,8 @@ namespace FlaxEditor.Windows.Assets protected VisjectFunctionSurfaceWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -70,10 +72,10 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); // Panel diff --git a/Source/Editor/Windows/Assets/VisualScriptWindow.cs b/Source/Editor/Windows/Assets/VisualScriptWindow.cs index 584f1c307..79157f850 100644 --- a/Source/Editor/Windows/Assets/VisualScriptWindow.cs +++ b/Source/Editor/Windows/Assets/VisualScriptWindow.cs @@ -561,6 +561,7 @@ namespace FlaxEditor.Windows.Assets : base(editor, item) { var isPlayMode = Editor.IsPlayMode; + var inputOptions = Editor.Options.Options.Input; // Undo _undo = new Undo(); @@ -598,21 +599,21 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/visual/index.html")).LinkTooltip("See documentation to learn more"); _debugToolstripControls = new[] { _toolstrip.AddSeparator(), - _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"), + _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"), _toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"), - _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip("Step Over (F10)"), - _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip("Step Into (F11)"), - _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip("Step Out (Shift+F11)"), + _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip($"Step Over ({inputOptions.DebuggerStepOver})"), + _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip($"Step Into ({inputOptions.DebuggerStepInto})"), + _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip($"Step Out ({inputOptions.DebuggerStepOut})"), _toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"), }; foreach (var control in _debugToolstripControls) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 56136cfbf..dda812f84 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -114,18 +114,32 @@ namespace FlaxEditor.Windows } } - cm.AddButton("Delete", () => Delete(item)); + if (isFolder && folder.Node is MainContentTreeNode) + { + cm.AddSeparator(); + } + else + { + cm.AddButton("Delete", () => Delete(item)); - cm.AddSeparator(); + cm.AddSeparator(); - cm.AddButton("Duplicate", _view.Duplicate); + cm.AddButton("Duplicate", _view.Duplicate); - cm.AddButton("Copy", _view.Copy); + cm.AddButton("Copy", _view.Copy); + } b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); - cm.AddButton("Rename", () => Rename(item)); + if (isFolder && folder.Node is MainContentTreeNode) + { + // Do nothing + } + else + { + cm.AddButton("Rename", () => Rename(item)); + } // Custom options ContextMenuShow?.Invoke(cm, item); @@ -187,12 +201,12 @@ namespace FlaxEditor.Windows continue; // Get context proxy - ContentProxy p; + ContentProxy p = null; if (type.Type.IsSubclassOf(typeof(ContentProxy))) { p = Editor.ContentDatabase.Proxy.Find(x => x.GetType() == type.Type); } - else + else if (type.CanCreateInstance) { // User can use attribute to put their own assets into the content context menu var generic = typeof(SpawnableJsonAssetProxy<>).MakeGenericType(type.Type); @@ -369,7 +383,7 @@ namespace FlaxEditor.Windows } var pluginPath = Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text); - if (Directory.Exists(pluginPath)) + if (!IsValidModuleName(nameTextBox.Text) || Directory.Exists(pluginPath)) { nameTextBox.BorderColor = Color.Red; nameTextBox.BorderSelectedColor = Color.Red; @@ -429,6 +443,12 @@ namespace FlaxEditor.Windows submitButton.Clicked += () => { // TODO: Check all modules in project including plugins + if (!IsValidModuleName(nameTextBox.Text)) + { + Editor.LogWarning("Invalid module name. Module names cannot contain spaces, start with a number or contain non-alphanumeric characters."); + return; + } + if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text))) { Editor.LogWarning("Cannot create module due to name conflict."); @@ -460,5 +480,16 @@ namespace FlaxEditor.Windows button.ParentContextMenu.Hide(); }; } + + private static bool IsValidModuleName(string text) + { + if (text.Contains(' ')) + return false; + if (char.IsDigit(text[0])) + return false; + if (text.Any(c => !char.IsLetterOrDigit(c) && c != '_')) + return false; + return true; + } } } diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs index 5ece067a0..2ef9c05cf 100644 --- a/Source/Editor/Windows/GameCookerWindow.cs +++ b/Source/Editor/Windows/GameCookerWindow.cs @@ -155,29 +155,42 @@ namespace FlaxEditor.Windows public virtual void OnNotAvailableLayout(LayoutElementsContainer layout) { - layout.Label("Missing platform data tools for the target platform.", TextAlignment.Center); + string text = "Missing platform data tools for the target platform."; if (FlaxEditor.Editor.IsOfficialBuild()) { switch (BuildPlatform) { +#if PLATFORM_WINDOWS case BuildPlatform.Windows32: case BuildPlatform.Windows64: case BuildPlatform.UWPx86: case BuildPlatform.UWPx64: case BuildPlatform.LinuxX64: case BuildPlatform.AndroidARM64: - layout.Label("Use Flax Launcher and download the required package.", TextAlignment.Center); + text += "\nUse Flax Launcher and download the required package."; break; +#endif default: - layout.Label("Engine source is required to target this platform.", TextAlignment.Center); + text += "\nEngine source is required to target this platform."; break; } } else { - var label = layout.Label("To target this platform separate engine source package is required.\nTo get access please contact via https://flaxengine.com/contact", TextAlignment.Center); - label.Label.AutoHeight = true; + text += "\nTo target this platform separate engine source package is required."; + switch (BuildPlatform) + { + case BuildPlatform.XboxOne: + case BuildPlatform.XboxScarlett: + case BuildPlatform.PS4: + case BuildPlatform.PS5: + case BuildPlatform.Switch: + text += "\nTo get access please contact via https://flaxengine.com/contact"; + break; + } } + var label = layout.Label(text, TextAlignment.Center); + label.Label.AutoHeight = true; } public virtual void Build() diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index fdedbb3c2..4a8c11176 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -271,8 +271,6 @@ namespace FlaxEditor.Windows Title = "Game"; AutoFocus = true; - FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); - var task = MainRenderTask.Instance; // Setup viewport @@ -304,6 +302,12 @@ namespace FlaxEditor.Windows // Link editor options Editor.Options.OptionsChanged += OnOptionsChanged; OnOptionsChanged(Editor.Options.Options); + + InputActions.Add(options => options.TakeScreenshot, () => Screenshot.Capture(string.Empty)); + InputActions.Add(options => options.DebuggerUnlockMouse, UnlockMouseInPlay); + InputActions.Add(options => options.ToggleFullscreen, () => { if (Editor.IsPlayMode) IsMaximized = !IsMaximized; }); + + FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); } private void ChangeViewportRatio(ViewportScaleOptions v) @@ -382,6 +386,7 @@ namespace FlaxEditor.Windows { _viewport.Bounds = new Rectangle(Width * (1 - scaleWidth) / 2, 0, Width * scaleWidth, Height); } + _viewport.SyncBackbufferSize(); PerformLayout(); } @@ -945,27 +950,6 @@ namespace FlaxEditor.Windows /// public override bool OnKeyDown(KeyboardKeys key) { - switch (key) - { - case KeyboardKeys.F12: - Screenshot.Capture(string.Empty); - return true; - case KeyboardKeys.F11: - if (Root.GetKey(KeyboardKeys.Shift)) - { - // Unlock mouse in game mode - UnlockMouseInPlay(); - return true; - } - else if (Editor.IsPlayMode) - { - // Maximized game window toggle - IsMaximized = !IsMaximized; - return true; - } - break; - } - // Prevent closing the game window tab during a play session if (Editor.StateMachine.IsPlayMode && Editor.Options.Options.Input.CloseTab.Process(this, key)) { diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 3665b7073..6526d7c8a 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -467,6 +467,7 @@ namespace FlaxEditor.Windows if (_isDirty) { _isDirty = false; + var wasEmpty = _output.TextLength == 0; // Cache fonts _output.DefaultStyle.Font.GetFont(); @@ -589,7 +590,7 @@ namespace FlaxEditor.Windows // Update the output var cachedScrollValue = _vScroll.Value; var cachedSelection = _output.SelectionRange; - var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f; + var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f || wasEmpty; _output.Text = _textBuffer.ToString(); _textBufferCount = _entries.Count; if (!_vScroll.IsThumbClicked) diff --git a/Source/Editor/Windows/PluginsWindow.cs b/Source/Editor/Windows/PluginsWindow.cs index c5ec02b31..f43fb0342 100644 --- a/Source/Editor/Windows/PluginsWindow.cs +++ b/Source/Editor/Windows/PluginsWindow.cs @@ -190,7 +190,7 @@ namespace FlaxEditor.Windows }; _addPluginProjectButton = new Button { - Text = "Create Plugin Project", + Text = "Create Project", TooltipText = "Add new plugin project.", AnchorPreset = AnchorPresets.TopLeft, LocalLocation = new Float2(70, 18), @@ -201,7 +201,7 @@ namespace FlaxEditor.Windows _cloneProjectButton = new Button { - Text = "Clone Plugin Project", + Text = "Clone Project", TooltipText = "Git Clone a plugin project.", AnchorPreset = AnchorPresets.TopLeft, LocalLocation = new Float2(70 + _addPluginProjectButton.Size.X + 8, 18), @@ -392,25 +392,48 @@ namespace FlaxEditor.Windows Editor.Log("Plugin project has been cloned."); + try + { + // Start git submodule clone + var settings = new CreateProcessSettings + { + FileName = "git", + WorkingDirectory = clonePath, + Arguments = "submodule update --init", + ShellExecute = false, + LogOutput = true, + }; + Platform.CreateProcess(ref settings); + } + catch (Exception e) + { + Editor.LogError($"Failed Git submodule process. {e}"); + return; + } + // Find project config file. Could be different then what the user named the folder. - var files = Directory.GetFiles(clonePath); string pluginProjectName = ""; - foreach (var file in files) + foreach (var file in Directory.GetFiles(clonePath)) { if (file.Contains(".flaxproj", StringComparison.OrdinalIgnoreCase)) { pluginProjectName = Path.GetFileNameWithoutExtension(file); - Debug.Log(pluginProjectName); + break; } } - if (string.IsNullOrEmpty(pluginProjectName)) - Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually."); - else { - await AddReferenceToProject(pluginName, pluginProjectName); - MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); + Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually."); + return; } + + await AddModuleReferencesInGameModule(clonePath); + await AddReferenceToProject(pluginName, pluginProjectName); + + if (Editor.Options.Options.SourceCode.AutoGenerateScriptsProjectFiles) + Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); + + MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); } private void OnAddButtonClicked() @@ -730,6 +753,37 @@ namespace FlaxEditor.Windows MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); } + private async Task AddModuleReferencesInGameModule(string pluginFolderPath) + { + // Common game build script location + var gameScript = Path.Combine(Globals.ProjectFolder, "Source/Game/Game.Build.cs"); + if (File.Exists(gameScript)) + { + var gameScriptContents = await File.ReadAllTextAsync(gameScript); + var insertLocation = gameScriptContents.IndexOf("base.Setup(options);", StringComparison.Ordinal); + if (insertLocation != -1) + { + insertLocation += 20; + var modifiedAny = false; + + // Find all code modules in a plugin to auto-reference them in game build script + foreach (var subDir in Directory.GetDirectories(Path.Combine(pluginFolderPath, "Source"))) + { + var pluginModuleName = Path.GetFileName(subDir); + var pluginModuleScriptPath = Path.Combine(subDir, pluginModuleName + ".Build.cs"); + if (File.Exists(pluginModuleScriptPath)) + { + gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");"); + modifiedAny = true; + } + } + + if (modifiedAny) + await File.WriteAllTextAsync(gameScript, gameScriptContents, Encoding.UTF8); + } + } + } + private async Task AddReferenceToProject(string pluginFolderName, string pluginName) { // Project flax config file diff --git a/Source/Editor/Windows/Profiler/Assets.cs b/Source/Editor/Windows/Profiler/Assets.cs index 3ccf6e2eb..536d65a74 100644 --- a/Source/Editor/Windows/Profiler/Assets.cs +++ b/Source/Editor/Windows/Profiler/Assets.cs @@ -62,7 +62,9 @@ namespace FlaxEditor.Windows.Profiler _memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged; // Table - var headerColor = Style.Current.LightBackground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] @@ -73,22 +75,26 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Resource", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Type", CellAlignment = TextAlignment.Center, TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "References", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Memory Usage", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = v => Utilities.Utils.FormatBytesCount((ulong)v), }, }, diff --git a/Source/Editor/Windows/Profiler/CPU.cs b/Source/Editor/Windows/Profiler/CPU.cs index fd4061276..0cbb3fa9b 100644 --- a/Source/Editor/Windows/Profiler/CPU.cs +++ b/Source/Editor/Windows/Profiler/CPU.cs @@ -92,7 +92,9 @@ namespace FlaxEditor.Windows.Profiler }; // Table - var headerColor = Style.Current.LightBackground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] @@ -103,36 +105,42 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Event", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Total", TitleBackgroundColor = headerColor, FormatValue = FormatCellPercentage, + TitleColor = textColor, }, new ColumnDefinition { Title = "Self", TitleBackgroundColor = headerColor, FormatValue = FormatCellPercentage, + TitleColor = textColor, }, new ColumnDefinition { Title = "Time ms", TitleBackgroundColor = headerColor, FormatValue = FormatCellMs, + TitleColor = textColor, }, new ColumnDefinition { Title = "Self ms", TitleBackgroundColor = headerColor, FormatValue = FormatCellMs, + TitleColor = textColor, }, new ColumnDefinition { Title = "Memory", TitleBackgroundColor = headerColor, FormatValue = FormatCellBytes, + TitleColor = textColor, }, }, Parent = layout, diff --git a/Source/Editor/Windows/Profiler/GPU.cs b/Source/Editor/Windows/Profiler/GPU.cs index 4ed18691a..d2c34d335 100644 --- a/Source/Editor/Windows/Profiler/GPU.cs +++ b/Source/Editor/Windows/Profiler/GPU.cs @@ -63,7 +63,9 @@ namespace FlaxEditor.Windows.Profiler }; // Table - var headerColor = Style.Current.LightBackground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] @@ -74,35 +76,41 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Event", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Total", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = (x) => ((float)x).ToString("0.0") + '%', }, new ColumnDefinition { Title = "GPU ms", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = (x) => ((float)x).ToString("0.000"), }, new ColumnDefinition { Title = "Draw Calls", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCountLong, }, new ColumnDefinition { Title = "Triangles", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCountLong, }, new ColumnDefinition { Title = "Vertices", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCountLong, }, }, diff --git a/Source/Editor/Windows/Profiler/MemoryGPU.cs b/Source/Editor/Windows/Profiler/MemoryGPU.cs index 20a7898e0..e7c085362 100644 --- a/Source/Editor/Windows/Profiler/MemoryGPU.cs +++ b/Source/Editor/Windows/Profiler/MemoryGPU.cs @@ -63,7 +63,9 @@ namespace FlaxEditor.Windows.Profiler _memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged; // Table - var headerColor = Style.Current.LightBackground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] @@ -74,18 +76,21 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Resource", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Type", CellAlignment = TextAlignment.Center, TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Memory Usage", TitleBackgroundColor = headerColor, FormatValue = v => Utilities.Utils.FormatBytesCount((ulong)v), + TitleColor = textColor, }, }, Parent = layout, diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index d29000d27..1ac9777c1 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -252,7 +252,9 @@ namespace FlaxEditor.Windows.Profiler private static Table InitTable(ContainerControl parent, string name) { - var headerColor = Style.Current.LightBackground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; var table = new Table { Columns = new[] @@ -263,28 +265,33 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = name, TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Count", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Data Size", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCellBytes, }, new ColumnDefinition { Title = "Message Size", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCellBytes, }, new ColumnDefinition { Title = "Receivers", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, }, Splits = new[] diff --git a/Source/Editor/Windows/Profiler/SingleChart.cs b/Source/Editor/Windows/Profiler/SingleChart.cs index e14480bee..4f36692e5 100644 --- a/Source/Editor/Windows/Profiler/SingleChart.cs +++ b/Source/Editor/Windows/Profiler/SingleChart.cs @@ -105,7 +105,7 @@ namespace FlaxEditor.Windows.Profiler if (_selectedSampleIndex != -1) { float selectedX = Width - (_samples.Count - _selectedSampleIndex - 1) * PointsOffset; - Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), Color.White, 1.5f); + Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), style.Foreground, 1.5f); } int samplesInViewCount = Math.Min((int)(Width / PointsOffset), _samples.Count) - 1; @@ -138,8 +138,8 @@ namespace FlaxEditor.Windows.Profiler var headerRect = new Rectangle(0, chartHeight, Width, TitleHeight); var headerTextRect = new Rectangle(2, chartHeight, Width - 4, TitleHeight); Render2D.FillRectangle(headerRect, style.BackgroundNormal); - Render2D.DrawText(style.FontMedium, Title, headerTextRect, Color.White * 0.8f, TextAlignment.Near, TextAlignment.Center); - Render2D.DrawText(style.FontMedium, _sample, headerTextRect, Color.White, TextAlignment.Far, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, Title, headerTextRect, style.ForegroundGrey, TextAlignment.Near, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, _sample, headerTextRect, style.Foreground, TextAlignment.Far, TextAlignment.Center); } private void OnClick(ref Float2 location) diff --git a/Source/Editor/Windows/Profiler/Timeline.cs b/Source/Editor/Windows/Profiler/Timeline.cs index a61ca1d05..59a7a0e26 100644 --- a/Source/Editor/Windows/Profiler/Timeline.cs +++ b/Source/Editor/Windows/Profiler/Timeline.cs @@ -90,7 +90,7 @@ namespace FlaxEditor.Windows.Profiler if (_nameLength < bounds.Width + 4) { Render2D.PushClip(bounds); - Render2D.DrawText(style.FontMedium, _name, bounds, Color.White, TextAlignment.Center, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, _name, bounds, Style.Current.Foreground, TextAlignment.Center, TextAlignment.Center); Render2D.PopClip(); } } @@ -115,7 +115,7 @@ namespace FlaxEditor.Windows.Profiler var style = Style.Current; var rect = new Rectangle(Float2.Zero, Size); Render2D.PushClip(rect); - Render2D.DrawText(style.FontMedium, Name, rect, Color.White, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapChars); + Render2D.DrawText(style.FontMedium, Name, rect, Style.Current.Foreground, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapChars); Render2D.PopClip(); } } diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index cbaa27371..63ba7b960 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -142,7 +142,7 @@ namespace FlaxEditor.Windows { if (selection.Count != 0) Editor.SceneEditing.Select(actor); - actor.TreeNode.StartRenaming(this); + actor.TreeNode.StartRenaming(this, _sceneTreePanel); } } @@ -423,6 +423,7 @@ namespace FlaxEditor.Windows var actor = item.OnEditorDrop(this); actor.Name = item.ShortName; Level.SpawnActor(actor); + Editor.Scene.MarkSceneEdited(actor.Scene); } result = DragDropEffect.Move; } @@ -440,6 +441,7 @@ namespace FlaxEditor.Windows } actor.Name = item.Name; Level.SpawnActor(actor); + Editor.Scene.MarkSceneEdited(actor.Scene); } result = DragDropEffect.Move; } diff --git a/Source/Editor/Windows/Search/ContentSearchWindow.cs b/Source/Editor/Windows/Search/ContentSearchWindow.cs index e6e407247..305fa16d6 100644 --- a/Source/Editor/Windows/Search/ContentSearchWindow.cs +++ b/Source/Editor/Windows/Search/ContentSearchWindow.cs @@ -72,6 +72,7 @@ namespace FlaxEngine.Windows.Search /// /// The content searching window. Allows to search inside Visual Scripts, Materials, Particles and other assets. /// + [HideInEditor] internal class ContentSearchWindow : EditorWindow { /// @@ -115,6 +116,7 @@ namespace FlaxEngine.Windows.Search } } + [HideInEditor] private sealed class SearchResultTreeNode : TreeNode { public Action Navigate; diff --git a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs index c84a9ba33..57f10f2f4 100644 --- a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs +++ b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs @@ -399,6 +399,8 @@ namespace FlaxEditor.Windows { Title = "Visual Script Debugger"; + var inputOptions = editor.Options.Options.Input; + var toolstrip = new ToolStrip { Parent = this @@ -407,7 +409,7 @@ namespace FlaxEditor.Windows _debugToolstripControls = new[] { toolstrip.AddSeparator(), - toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"), + toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"), toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"), toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"), }; diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp index a5b24fc8b..98fb9ba22 100644 --- a/Source/Engine/AI/Behavior.cpp +++ b/Source/Engine/AI/Behavior.cpp @@ -114,14 +114,19 @@ void Behavior::UpdateAsync() void Behavior::StartLogic() { + if (_result == BehaviorUpdateResult::Running) + return; PROFILE_CPU(); - // Ensure to have tree loaded on begin play + // Ensure to have tree loaded on play CHECK(Tree && !Tree->WaitForLoaded()); BehaviorTree* tree = Tree.Get(); CHECK(tree->Graph.Root); + // Setup state _result = BehaviorUpdateResult::Running; + _accumulatedTime = 0.0f; + _totalTime = 0; // Init knowledge _knowledge.InitMemory(tree); @@ -135,6 +140,7 @@ void Behavior::StopLogic(BehaviorUpdateResult result) _accumulatedTime = 0.0f; _totalTime = 0; _result = result; + _knowledge.FreeMemory(); } void Behavior::ResetLogic() @@ -170,7 +176,11 @@ void Behavior::OnDisable() bool Behavior::GetNodeDebugRelevancy(const BehaviorTreeNode* node, const Behavior* behavior) { - return node && behavior && node->_executionIndex != -1 && behavior->_knowledge.RelevantNodes.Get(node->_executionIndex); + return node && + behavior && + node->_executionIndex >= 0 && + node->_executionIndex < behavior->_knowledge.RelevantNodes.Count() && + behavior->_knowledge.RelevantNodes.Get(node->_executionIndex); } String Behavior::GetNodeDebugInfo(const BehaviorTreeNode* node, Behavior* behavior) @@ -179,7 +189,7 @@ String Behavior::GetNodeDebugInfo(const BehaviorTreeNode* node, Behavior* behavi return String::Empty; BehaviorUpdateContext context; Platform::MemoryClear(&context, sizeof(context)); - if (behavior && node->_executionIndex != -1 && behavior->_knowledge.RelevantNodes.Get(node->_executionIndex)) + if (GetNodeDebugRelevancy(node, behavior)) { // Pass behavior and knowledge data only for relevant nodes to properly access it context.Behavior = behavior; diff --git a/Source/Engine/AI/BehaviorKnowledge.cpp b/Source/Engine/AI/BehaviorKnowledge.cpp index a33738d31..a73a1cfd4 100644 --- a/Source/Engine/AI/BehaviorKnowledge.cpp +++ b/Source/Engine/AI/BehaviorKnowledge.cpp @@ -83,7 +83,7 @@ bool AccessVariant(Variant& instance, const StringAnsiView& member, Variant& val } } #endif - else + else if (typeName.HasChars()) { LOG(Warning, "Missing scripting type \'{0}\'", String(typeName)); } @@ -150,7 +150,13 @@ void BehaviorKnowledge::InitMemory(BehaviorTree* tree) RelevantNodes.Resize(tree->Graph.NodesCount, false); RelevantNodes.SetAll(false); if (!Memory && tree->Graph.NodesStatesSize) + { Memory = Allocator::Allocate(tree->Graph.NodesStatesSize); +#if !BUILD_RELEASE + // Clear memory to make it easier to spot missing data issues (eg. zero GCHandle in C# BT node due to missing state init) + Platform::MemoryClear(Memory, tree->Graph.NodesStatesSize); +#endif + } } void BehaviorKnowledge::FreeMemory() diff --git a/Source/Engine/AI/BehaviorKnowledgeSelector.cs b/Source/Engine/AI/BehaviorKnowledgeSelector.cs index 84e923ccf..5c642e92a 100644 --- a/Source/Engine/AI/BehaviorKnowledgeSelector.cs +++ b/Source/Engine/AI/BehaviorKnowledgeSelector.cs @@ -202,7 +202,7 @@ namespace FlaxEngine public T Get(BehaviorKnowledge knowledge) { if (knowledge != null && knowledge.Get(Path, out var value)) - return (T)value; + return Utilities.VariantUtils.Cast(value); return default; } @@ -218,7 +218,7 @@ namespace FlaxEngine object tmp = null; bool result = knowledge != null && knowledge.Get(Path, out tmp); if (result) - value = (T)tmp; + value = Utilities.VariantUtils.Cast(tmp); return result; } diff --git a/Source/Engine/AI/BehaviorTree.cs b/Source/Engine/AI/BehaviorTree.cs index 699c72976..a1c863e6b 100644 --- a/Source/Engine/AI/BehaviorTree.cs +++ b/Source/Engine/AI/BehaviorTree.cs @@ -95,12 +95,16 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref T GetState(IntPtr memory) where T : struct { - var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer(); - var handle = GCHandle.FromIntPtr(Unsafe.Read(ptr)); + var ptr = Unsafe.Read(IntPtr.Add(memory, _memoryOffset).ToPointer()); +#if !BUILD_RELEASE + if (ptr == IntPtr.Zero) + throw new Exception($"Missing state '{typeof(T).FullName}' for node '{GetType().FullName}'"); +#endif + var handle = GCHandle.FromIntPtr(ptr); var state = handle.Target; #if !BUILD_RELEASE if (state == null) - throw new NullReferenceException(); + throw new Exception($"Missing state '{typeof(T).FullName}' for node '{GetType().FullName}'"); #endif return ref Unsafe.Unbox(state); } @@ -111,8 +115,10 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public void FreeState(IntPtr memory) { - var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer(); - var handle = GCHandle.FromIntPtr(Unsafe.Read(ptr)); + var ptr = Unsafe.Read(IntPtr.Add(memory, _memoryOffset).ToPointer()); + if (ptr == IntPtr.Zero) + return; + var handle = GCHandle.FromIntPtr(ptr); handle.Free(); } } diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index 4be336b35..fb49a007e 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -85,6 +85,8 @@ BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext& result = BehaviorUpdateResult::Failed; else result = Update(context); + if ((int32)result < 0 || (int32)result > (int32)BehaviorUpdateResult::Failed) + result = BehaviorUpdateResult::Failed; // Invalid value is a failure // Post-process result from decorators for (BehaviorTreeDecorator* decorator : _decorators) diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 0518fe248..6f0d7661b 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -1342,7 +1342,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu { const bool xAxis = Math::IsZero(v0.X) && Math::IsZero(v1.X); const bool yAxis = Math::IsZero(v0.Y) && Math::IsZero(v1.Y); - if (xAxis || yAxis) + if (xAxis && yAxis) + { + // Single animation + value = SampleAnimation(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, aAnim, aData.W); + } + else if (xAxis || yAxis) { if (yAxis) { @@ -2109,7 +2114,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu bucket.LoopsLeft--; bucket.LoopsDone++; } - value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, slot.Speed); + // Speed is accounted for in the new time pos, so keep sample speed at 1 + value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, 1); bucket.TimePosition = newTimePos; if (bucket.LoopsLeft == 0 && slot.BlendOutTime > 0.0f && length - slot.BlendOutTime < bucket.TimePosition) { diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp index d04da7274..689f38d12 100644 --- a/Source/Engine/Audio/AudioClip.cpp +++ b/Source/Engine/Audio/AudioClip.cpp @@ -225,6 +225,7 @@ bool AudioClip::ExtractDataRaw(Array& resultData, AudioDataInfo& resultDat void AudioClip::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 613c7c2c2..93d904d5e 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -16,11 +16,13 @@ AssetReferenceBase::~AssetReferenceBase() { - if (_asset) + Asset* asset = _asset; + if (asset) { - _asset->OnLoaded.Unbind(this); - _asset->OnUnloaded.Unbind(this); - _asset->RemoveReference(); + _asset = nullptr; + asset->OnLoaded.Unbind(this); + asset->OnUnloaded.Unbind(this); + asset->RemoveReference(); } } @@ -70,8 +72,12 @@ void AssetReferenceBase::OnUnloaded(Asset* asset) WeakAssetReferenceBase::~WeakAssetReferenceBase() { - if (_asset) - _asset->OnUnloaded.Unbind(this); + Asset* asset = _asset; + if (asset) + { + _asset = nullptr; + asset->OnUnloaded.Unbind(this); + } } String WeakAssetReferenceBase::ToString() const @@ -101,6 +107,20 @@ void WeakAssetReferenceBase::OnUnloaded(Asset* asset) _asset = nullptr; } +SoftAssetReferenceBase::~SoftAssetReferenceBase() +{ + Asset* asset = _asset; + if (asset) + { + _asset = nullptr; + asset->OnUnloaded.Unbind(this); + asset->RemoveReference(); + } +#if !BUILD_RELEASE + _id = Guid::Empty; +#endif +} + String SoftAssetReferenceBase::ToString() const { return _asset ? _asset->ToString() : (_id.IsValid() ? _id.ToString() : TEXT("")); @@ -502,6 +522,14 @@ void Asset::InitAsVirtual() void Asset::CancelStreaming() { + // Cancel loading task but go over asset locker to prevent case if other load threads still loads asset while it's reimported on other thread + Locker.Lock(); + ContentLoadTask* loadTask = _loadingTask; + Locker.Unlock(); + if (loadTask) + { + loadTask->Cancel(); + } } #if USE_EDITOR diff --git a/Source/Engine/Content/AssetReference.h b/Source/Engine/Content/AssetReference.h index 6aee12246..b9d54f30a 100644 --- a/Source/Engine/Content/AssetReference.h +++ b/Source/Engine/Content/AssetReference.h @@ -9,9 +9,6 @@ /// class FLAXENGINE_API AssetReferenceBase { -public: - typedef Delegate<> EventType; - protected: Asset* _asset = nullptr; @@ -19,17 +16,17 @@ public: /// /// The asset loaded event (fired when asset gets loaded or is already loaded after change). /// - EventType Loaded; + Action Loaded; /// /// The asset unloading event (should cleanup refs to it). /// - EventType Unload; + Action Unload; /// /// Action fired when field gets changed (link a new asset or change to the another value). /// - EventType Changed; + Action Changed; public: NON_COPYABLE(AssetReferenceBase); diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index 5a008645d..691b00a50 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -783,6 +783,7 @@ void Model::InitAsVirtual() void Model::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index 5871087d9..b823db5a3 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -969,6 +969,7 @@ void SkinnedModel::InitAsVirtual() void SkinnedModel::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index 105d4ad2d..9748ba60c 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -1428,6 +1428,10 @@ Asset::LoadResult VisualScript::load() #if USE_EDITOR if (_instances.HasItems()) { + // Mark as already loaded so any WaitForLoaded checks during GetDefaultInstance bellow will handle this Visual Script as ready to use + _loadFailed = false; + _isLoaded = true; + // Setup scripting type CacheScriptingType(); @@ -1512,7 +1516,7 @@ void VisualScript::unload(bool isReloading) // Note: preserve the registered scripting type but invalidate the locally cached handle if (_scriptingTypeHandle) { - VisualScriptingModule.Locker.Lock(); + VisualScriptingBinaryModule::Locker.Lock(); auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex]; if (type.Script.DefaultInstance) { @@ -1523,7 +1527,7 @@ void VisualScript::unload(bool isReloading) VisualScriptingModule.Scripts[_scriptingTypeHandle.TypeIndex] = nullptr; _scriptingTypeHandleCached = _scriptingTypeHandle; _scriptingTypeHandle = ScriptingTypeHandle(); - VisualScriptingModule.Locker.Unlock(); + VisualScriptingBinaryModule::Locker.Unlock(); } } @@ -1534,8 +1538,8 @@ AssetChunksFlag VisualScript::getChunksToPreload() const void VisualScript::CacheScriptingType() { + ScopeLock lock(VisualScriptingBinaryModule::Locker); auto& binaryModule = VisualScriptingModule; - ScopeLock lock(binaryModule.Locker); // Find base type const StringAnsi baseTypename(Meta.BaseTypename); diff --git a/Source/Engine/Content/BinaryAsset.cpp b/Source/Engine/Content/BinaryAsset.cpp index b04603a96..9519f73df 100644 --- a/Source/Engine/Content/BinaryAsset.cpp +++ b/Source/Engine/Content/BinaryAsset.cpp @@ -323,26 +323,29 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool { // Ensure path is in a valid format String pathNorm(path); - FileSystem::NormalizePath(pathNorm); + ContentStorageManager::FormatPath(pathNorm); + const StringView filePath = pathNorm; // Find target storage container and the asset - auto storage = ContentStorageManager::TryGetStorage(pathNorm); - auto asset = Content::GetAsset(pathNorm); + auto storage = ContentStorageManager::TryGetStorage(filePath); + auto asset = Content::GetAsset(filePath); auto binaryAsset = dynamic_cast(asset); if (asset && !binaryAsset) { LOG(Warning, "Cannot write to the non-binary asset location."); return true; } + if (!binaryAsset && !storage && FileSystem::FileExists(filePath)) + { + // Force-resolve storage (asset at that path could be not yet loaded into registry) + storage = ContentStorageManager::GetStorage(filePath); + } // Check if can perform write operation to the asset container - if (storage) + if (storage && !storage->AllowDataModifications()) { - if (!storage->AllowDataModifications()) - { - LOG(Warning, "Cannot write to the asset storage container."); - return true; - } + LOG(Warning, "Cannot write to the asset storage container."); + return true; } // Initialize data container @@ -352,6 +355,11 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool // Use the same asset ID data.Header.ID = binaryAsset->GetID(); } + else if (storage && storage->GetEntriesCount()) + { + // Use the same file ID + data.Header.ID = storage->GetEntry(0).ID; + } else { // Randomize ID @@ -373,8 +381,8 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool } else { - ASSERT(pathNorm.HasChars()); - result = FlaxStorage::Create(pathNorm, data, silentMode); + ASSERT(filePath.HasChars()); + result = FlaxStorage::Create(filePath, data, silentMode); } if (binaryAsset) binaryAsset->_isSaving = false; diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index bd27abc5a..982ae599f 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -54,8 +54,7 @@ namespace // Assets CriticalSection AssetsLocker; Dictionary Assets(2048); - CriticalSection LoadCallAssetsLocker; - Array LoadCallAssets(64); + Array LoadCallAssets(PLATFORM_THREADS_LIMIT); CriticalSection LoadedAssetsToInvokeLocker; Array LoadedAssetsToInvoke(64); Array ToUnload; @@ -449,18 +448,19 @@ Asset* Content::LoadAsync(const StringView& path, const ScriptingTypeHandle& typ { // Ensure path is in a valid format String pathNorm(path); - StringUtils::PathRemoveRelativeParts(pathNorm); + ContentStorageManager::FormatPath(pathNorm); + const StringView filePath = pathNorm; #if USE_EDITOR - if (!FileSystem::FileExists(pathNorm)) + if (!FileSystem::FileExists(filePath)) { - LOG(Error, "Missing file \'{0}\'", pathNorm); + LOG(Error, "Missing file \'{0}\'", filePath); return nullptr; } #endif AssetInfo assetInfo; - if (GetAssetInfo(pathNorm, assetInfo)) + if (GetAssetInfo(filePath, assetInfo)) { return LoadAsync(assetInfo.ID, type); } @@ -910,9 +910,13 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) return nullptr; // Check if asset has been already loaded - Asset* result = GetAsset(id); + Asset* result = nullptr; + AssetsLocker.Lock(); + Assets.TryGet(id, result); if (result) { + AssetsLocker.Unlock(); + // Validate type if (IsAssetTypeIdInvalid(type, result->GetTypeHandle()) && !result->Is(type)) { @@ -923,57 +927,41 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) } // Check if that asset is during loading - LoadCallAssetsLocker.Lock(); if (LoadCallAssets.Contains(id)) { - LoadCallAssetsLocker.Unlock(); + AssetsLocker.Unlock(); - // Wait for load end - // TODO: dont use active waiting and prevent deadlocks if running on a main thread - //while (!Engine::ShouldExit()) - while (true) + // Wait for loading end by other thread + bool contains = true; + while (contains) { - LoadCallAssetsLocker.Lock(); - const bool contains = LoadCallAssets.Contains(id); - LoadCallAssetsLocker.Unlock(); - if (!contains) - return GetAsset(id); Platform::Sleep(1); + AssetsLocker.Lock(); + contains = LoadCallAssets.Contains(id); + AssetsLocker.Unlock(); } - } - else - { - // Mark asset as loading - LoadCallAssets.Add(id); - LoadCallAssetsLocker.Unlock(); + Assets.TryGet(id, result); + return result; } - // Load asset - AssetInfo assetInfo; - result = load(id, type, assetInfo); + // Mark asset as loading and release lock so other threads can load other assets + LoadCallAssets.Add(id); + AssetsLocker.Unlock(); - // End loading - LoadCallAssetsLocker.Lock(); - LoadCallAssets.Remove(id); - LoadCallAssetsLocker.Unlock(); +#define LOAD_FAILED() AssetsLocker.Lock(); LoadCallAssets.Remove(id); AssetsLocker.Unlock(); return nullptr - return result; -} - -Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& assetInfo) -{ // Get cached asset info (from registry) + AssetInfo assetInfo; if (!GetAssetInfo(id, assetInfo)) { LOG(Warning, "Invalid or missing asset ({0}, {1}).", id, type.ToString()); - return nullptr; + LOAD_FAILED(); } - #if ASSETS_LOADING_EXTRA_VERIFICATION if (!FileSystem::FileExists(assetInfo.Path)) { LOG(Error, "Cannot find file '{0}'", assetInfo.Path); - return nullptr; + LOAD_FAILED(); } #endif @@ -982,28 +970,27 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& if (factory == nullptr) { LOG(Error, "Cannot find asset factory. Info: {0}", assetInfo.ToString()); - return nullptr; + LOAD_FAILED(); } // Create asset object - auto result = factory->New(assetInfo); + result = factory->New(assetInfo); if (result == nullptr) { LOG(Error, "Cannot create asset object. Info: {0}", assetInfo.ToString()); - return nullptr; + LOAD_FAILED(); } - + ASSERT(result->GetID() == id); #if ASSETS_LOADING_EXTRA_VERIFICATION if (IsAssetTypeIdInvalid(type, result->GetTypeHandle()) && !result->Is(type)) { - LOG(Error, "Different loaded asset type! Asset: '{0}'. Expected type: {1}", assetInfo.ToString(), type.ToString()); + LOG(Warning, "Different loaded asset type! Asset: '{0}'. Expected type: {1}", assetInfo.ToString(), type.ToString()); result->DeleteObject(); - return nullptr; + LOAD_FAILED(); } #endif // Register asset - ASSERT(result->GetID() == id); AssetsLocker.Lock(); #if ASSETS_LOADING_EXTRA_VERIFICATION ASSERT(!Assets.ContainsKey(id)); @@ -1011,11 +998,14 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& Assets.Add(id, result); // Start asset loading - // TODO: refactor this to create asset loading task-chain before AssetsLocker.Lock() to allow better parallelization result->startLoading(); + // Remove from the loading queue and release lock + LoadCallAssets.Remove(id); AssetsLocker.Unlock(); +#undef LOAD_FAILED + return result; } diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h index 6393ce48b..cce57194c 100644 --- a/Source/Engine/Content/Content.h +++ b/Source/Engine/Content/Content.h @@ -366,7 +366,6 @@ private: static void onAssetLoaded(Asset* asset); static void onAssetUnload(Asset* asset); static void onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId); - static Asset* load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& assetInfo); private: static void deleteFileSafety(const StringView& path, const Guid& id); diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h index 23cf5f787..4a7bbb2bb 100644 --- a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h +++ b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h @@ -48,6 +48,8 @@ protected: // [ContentLoadTask] Result run() override { + if (IsCancelRequested()) + return Result::Ok; PROFILE_CPU(); AssetReference ref = _asset.Get(); @@ -67,8 +69,6 @@ protected: { if (IsCancelRequested()) return Result::Ok; - - // Load it #if TRACY_ENABLE ZoneScoped; ZoneName(*name, name.Length()); diff --git a/Source/Engine/Content/SoftAssetReference.h b/Source/Engine/Content/SoftAssetReference.h index fe1cde8c2..d237b5fd7 100644 --- a/Source/Engine/Content/SoftAssetReference.h +++ b/Source/Engine/Content/SoftAssetReference.h @@ -30,9 +30,7 @@ public: /// /// Finalizes an instance of the class. /// - ~SoftAssetReferenceBase() - { - } + ~SoftAssetReferenceBase(); public: /// diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp index d3e18e9d0..61e73a3f2 100644 --- a/Source/Engine/Content/Storage/ContentStorageManager.cpp +++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp @@ -6,6 +6,7 @@ #include "Engine/Core/Log.h" #include "Engine/Engine/Engine.h" #include "Engine/Engine/EngineService.h" +#include "Engine/Engine/Globals.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/TaskGraph.h" @@ -185,6 +186,16 @@ void ContentStorageManager::EnsureUnlocked() Locker.Unlock(); } +void ContentStorageManager::FormatPath(String& path) +{ + StringUtils::PathRemoveRelativeParts(path); + if (FileSystem::IsRelative(path)) + { + // Convert local-project paths into absolute format which is used by Content Storage system + path = Globals::ProjectFolder / path; + } +} + bool ContentStorageManager::IsFlaxStoragePath(const String& path) { auto extension = FileSystem::GetExtension(path).ToLower(); diff --git a/Source/Engine/Content/Storage/ContentStorageManager.h b/Source/Engine/Content/Storage/ContentStorageManager.h index c615632e9..84a6dc07e 100644 --- a/Source/Engine/Content/Storage/ContentStorageManager.h +++ b/Source/Engine/Content/Storage/ContentStorageManager.h @@ -75,6 +75,9 @@ public: /// static void EnsureUnlocked(); + // Formats path into valid format used by the storage system (normalized and absolute). + static void FormatPath(String& path); + public: /// /// Determines whether the specified path can be a binary asset file (based on it's extension). diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index d530e5456..a47e0bd0e 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -1302,15 +1302,15 @@ void FlaxStorage::CloseFileHandles() // In those situations all the async tasks using this storage should be cancelled externally // Ensure that no one is using this resource - int32 waitTime = 10; + int32 waitTime = 100; while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) - Platform::Sleep(10); + Platform::Sleep(1); if (Platform::AtomicRead(&_chunksLock) != 0) { // File can be locked by some streaming tasks (eg. AudioClip::StreamingTask or StreamModelLODTask) + Entry e; for (int32 i = 0; i < GetEntriesCount(); i++) { - Entry e; GetEntry(i, e); Asset* asset = Content::GetAsset(e.ID); if (asset) @@ -1320,8 +1320,12 @@ void FlaxStorage::CloseFileHandles() } } } + waitTime = 100; + while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) + Platform::Sleep(1); ASSERT(_chunksLock == 0); + // Close file handles (from all threads) _file.DeleteAll(); } diff --git a/Source/Engine/Core/Collections/Array.h b/Source/Engine/Core/Collections/Array.h index 58117cf0a..cf45ed060 100644 --- a/Source/Engine/Core/Collections/Array.h +++ b/Source/Engine/Core/Collections/Array.h @@ -25,6 +25,19 @@ private: int32 _capacity; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromCount, int32 fromCapacity) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromCapacity); + Memory::MoveItems(to.Get(), from.Get(), fromCount); + Memory::DestructItems(from.Get(), fromCount); + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -134,7 +147,7 @@ public: _capacity = other._capacity; other._count = 0; other._capacity = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _count, _capacity); } /// @@ -191,7 +204,7 @@ public: _capacity = other._capacity; other._count = 0; other._capacity = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _count, _capacity); } return *this; } @@ -713,9 +726,18 @@ public: /// The other collection. void Swap(Array& other) { - ::Swap(_count, other._count); - ::Swap(_capacity, other._capacity); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + _allocation.Swap(other._allocation); + ::Swap(_count, other._count); + ::Swap(_capacity, other._capacity); + } + else + { + Array tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } /// @@ -726,9 +748,7 @@ public: T* data = _allocation.Get(); const int32 count = _count / 2; for (int32 i = 0; i < count; i++) - { ::Swap(data[i], data[_count - i - 1]); - } } public: diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 01238d434..eeadc82e9 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -22,6 +22,16 @@ private: int32 _capacity; AllocationData _allocation; + FORCE_INLINE static int32 ToItemCount(int32 size) + { + return Math::DivideAndRoundUp(size, sizeof(ItemType)); + } + + FORCE_INLINE static int32 ToItemCapacity(int32 size) + { + return Math::Max(Math::DivideAndRoundUp(size, sizeof(ItemType)), 1); + } + public: /// /// Initializes a new instance of the class. @@ -41,7 +51,7 @@ public: , _capacity(capacity) { if (capacity > 0) - _allocation.Allocate(Math::Max(capacity / sizeof(ItemType), 1)); + _allocation.Allocate(ToItemCapacity(capacity)); } /// @@ -53,7 +63,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -69,7 +79,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -101,7 +111,7 @@ public: { _allocation.Free(); _capacity = other._count; - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -246,7 +256,7 @@ public: return; ASSERT(capacity >= 0); const int32 count = preserveContents ? (_count < capacity ? _count : capacity) : 0; - _allocation.Relocate(Math::Max(capacity / sizeof(ItemType), 1), _count / sizeof(ItemType), count / sizeof(ItemType)); + _allocation.Relocate(ToItemCapacity(capacity), ToItemCount(_count), ToItemCount(count)); _capacity = capacity; _count = count; } @@ -272,7 +282,7 @@ public: { if (_capacity < minCapacity) { - const int32 capacity = _allocation.CalculateCapacityGrow(Math::Max(_capacity / sizeof(ItemType), 1), minCapacity); + const int32 capacity = _allocation.CalculateCapacityGrow(ToItemCapacity(_capacity), minCapacity); SetCapacity(capacity, preserveContents); } } @@ -284,7 +294,7 @@ public: void SetAll(const bool value) { if (_count != 0) - Platform::MemorySet(_allocation.Get(), Math::Max(_count / sizeof(ItemType), 1), value ? MAX_int32 : 0); + Platform::MemorySet(_allocation.Get(), ToItemCount(_count) * sizeof(ItemType), value ? MAX_uint32 : 0); } /// diff --git a/Source/Engine/Core/Collections/Config.h b/Source/Engine/Core/Collections/Config.h index 792ae57c8..ce7656dcd 100644 --- a/Source/Engine/Core/Collections/Config.h +++ b/Source/Engine/Core/Collections/Config.h @@ -2,13 +2,26 @@ #pragma once -/// -/// Default capacity for the dictionaries (amount of space for the elements) -/// -#define DICTIONARY_DEFAULT_CAPACITY 256 +#include "Engine/Platform/Defines.h" /// -/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size) +/// Default capacity for the dictionaries (amount of space for the elements). +/// +#ifndef DICTIONARY_DEFAULT_CAPACITY +#if PLATFORM_DESKTOP +#define DICTIONARY_DEFAULT_CAPACITY 256 +#else +#define DICTIONARY_DEFAULT_CAPACITY 64 +#endif +#endif + +/// +/// Default slack space divider for the dictionaries. +/// +#define DICTIONARY_DEFAULT_SLACK_SCALE 3 + +/// +/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size). /// #define DICTIONARY_PROB_FUNC(size, numChecks) (numChecks) //#define DICTIONARY_PROB_FUNC(size, numChecks) (1) diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index 575863dc9..dd73be390 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -40,7 +40,7 @@ public: private: State _state; - void Free() + FORCE_INLINE void Free() { if (_state == Occupied) { @@ -50,7 +50,7 @@ public: _state = Empty; } - void Delete() + FORCE_INLINE void Delete() { _state = Deleted; Memory::DestructItem(&Key); @@ -58,7 +58,7 @@ public: } template - void Occupy(const KeyComparableType& key) + FORCE_INLINE void Occupy(const KeyComparableType& key) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItem(&Value); @@ -66,7 +66,7 @@ public: } template - void Occupy(const KeyComparableType& key, const ValueType& value) + FORCE_INLINE void Occupy(const KeyComparableType& key, const ValueType& value) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItems(&Value, &value, 1); @@ -74,7 +74,7 @@ public: } template - void Occupy(const KeyComparableType& key, ValueType&& value) + FORCE_INLINE void Occupy(const KeyComparableType& key, ValueType&& value) { Memory::ConstructItems(&Key, &key, 1); Memory::MoveItems(&Value, &value, 1); @@ -110,6 +110,33 @@ private: int32 _size = 0; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromSize); + Bucket* toData = to.Get(); + Bucket* fromData = from.Get(); + for (int32 i = 0; i < fromSize; i++) + { + Bucket& fromBucket = fromData[i]; + if (fromBucket.IsOccupied()) + { + Bucket& toBucket = toData[i]; + Memory::MoveItems(&toBucket.Key, &fromBucket.Key, 1); + Memory::MoveItems(&toBucket.Value, &fromBucket.Value, 1); + toBucket._state = Bucket::Occupied; + Memory::DestructItem(&fromBucket.Key); + Memory::DestructItem(&fromBucket.Value); + fromBucket._state = Bucket::Empty; + } + } + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -132,9 +159,6 @@ public: /// /// The other collection to move. Dictionary(Dictionary&& other) noexcept - : _elementsCount(other._elementsCount) - , _deletedCount(other._deletedCount) - , _size(other._size) { _elementsCount = other._elementsCount; _deletedCount = other._deletedCount; @@ -142,7 +166,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } /// @@ -183,7 +207,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } return *this; } @@ -375,8 +399,12 @@ public: template ValueType& At(const KeyComparableType& key) { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); // Find location of the item or place to insert it FindPositionResult pos; @@ -388,9 +416,9 @@ public: // Insert ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; Bucket& bucket = _allocation.Get()[pos.FreeSlotIndex]; bucket.Occupy(key); - _elementsCount++; return bucket.Value; } @@ -493,7 +521,7 @@ public: for (Iterator i = Begin(); i.IsNotEnd(); ++i) { if (i->Value) - Delete(i->Value); + ::Delete(i->Value); } Clear(); } @@ -509,7 +537,7 @@ public: return; ASSERT(capacity >= 0); AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _deletedCount = _elementsCount = 0; @@ -533,13 +561,22 @@ public: } _size = capacity; Bucket* oldData = oldAllocation.Get(); - if (oldElementsCount != 0 && preserveContents) + if (oldElementsCount != 0 && capacity != 0 && preserveContents) { - // TODO; move keys and values on realloc + FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) { - if (oldData[i].IsOccupied()) - Add(oldData[i].Key, MoveTemp(oldData[i].Value)); + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Key, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); + Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); + bucket->_state = Bucket::Occupied; + _elementsCount++; + } } } if (oldElementsCount != 0) @@ -558,9 +595,9 @@ public: { if (_size >= minCapacity) return; - if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) - minCapacity = DICTIONARY_DEFAULT_CAPACITY; - const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + if (capacity < DICTIONARY_DEFAULT_CAPACITY) + capacity = DICTIONARY_DEFAULT_CAPACITY; SetCapacity(capacity, preserveContents); } @@ -570,10 +607,19 @@ public: /// The other collection. void Swap(Dictionary& other) { - ::Swap(_elementsCount, other._elementsCount); - ::Swap(_deletedCount, other._deletedCount); - ::Swap(_size, other._size); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + else + { + Dictionary tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } public: @@ -584,24 +630,10 @@ public: /// The value. /// Weak reference to the stored bucket. template - Bucket* Add(const KeyComparableType& key, const ValueType& value) + FORCE_INLINE Bucket* Add(const KeyComparableType& key, const ValueType& value) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); - - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(key, pos); - - // Ensure key is unknown - ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Bucket* bucket = OnAdd(key); bucket->Occupy(key, value); - _elementsCount++; - return bucket; } @@ -612,24 +644,10 @@ public: /// The value. /// Weak reference to the stored bucket. template - Bucket* Add(const KeyComparableType& key, ValueType&& value) + FORCE_INLINE Bucket* Add(const KeyComparableType& key, ValueType&& value) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); - - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(key, pos); - - // Ensure key is unknown - ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Bucket* bucket = OnAdd(key); bucket->Occupy(key, MoveTemp(value)); - _elementsCount++; - return bucket; } @@ -851,7 +869,7 @@ public: return Iterator(this, _size); } -protected: +private: /// /// The result container of the dictionary item lookup searching. /// @@ -911,4 +929,66 @@ protected: result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } + + template + Bucket* OnAdd(const KeyComparableType& key) + { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + + // Ensure to have enough memory for the next item (in case of new element insertion) + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); + + // Find location of the item or place to insert it + FindPositionResult pos; + FindPosition(key, pos); + + // Ensure key is unknown + ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); + + // Insert + ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; + return &_allocation.Get()[pos.FreeSlotIndex]; + } + + void Compact() + { + if (_elementsCount == 0) + { + // Fast path if it's empty + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + } + else + { + // Rebuild entire table completely + AllocationData oldAllocation; + MoveToEmpty(oldAllocation, _allocation, _size); + _allocation.Allocate(_size); + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + Bucket* oldData = oldAllocation.Get(); + FindPositionResult pos; + for (int32 i = 0; i < _size; i++) + { + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Key, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); + Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); + bucket->_state = Bucket::Occupied; + } + } + for (int32 i = 0; i < _size; i++) + oldData[i].Free(); + } + _deletedCount = 0; + } }; diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index 107e42e65..a683edf15 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -37,26 +37,33 @@ public: private: State _state; - void Free() + FORCE_INLINE void Free() { if (_state == Occupied) Memory::DestructItem(&Item); _state = Empty; } - void Delete() + FORCE_INLINE void Delete() { _state = Deleted; Memory::DestructItem(&Item); } template - void Occupy(const ItemType& item) + FORCE_INLINE void Occupy(const ItemType& item) { Memory::ConstructItems(&Item, &item, 1); _state = Occupied; } + template + FORCE_INLINE void Occupy(ItemType& item) + { + Memory::MoveItems(&Item, &item, 1); + _state = Occupied; + } + FORCE_INLINE bool IsEmpty() const { return _state == Empty; @@ -86,6 +93,31 @@ private: int32 _size = 0; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromSize); + Bucket* toData = to.Get(); + Bucket* fromData = from.Get(); + for (int32 i = 0; i < fromSize; i++) + { + Bucket& fromBucket = fromData[i]; + if (fromBucket.IsOccupied()) + { + Bucket& toBucket = toData[i]; + Memory::MoveItems(&toBucket.Item, &fromBucket.Item, 1); + toBucket._state = Bucket::Occupied; + Memory::DestructItem(&fromBucket.Item); + fromBucket._state = Bucket::Empty; + } + } + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -108,9 +140,6 @@ public: /// /// The other collection to move. HashSet(HashSet&& other) noexcept - : _elementsCount(other._elementsCount) - , _deletedCount(other._deletedCount) - , _size(other._size) { _elementsCount = other._elementsCount; _deletedCount = other._deletedCount; @@ -118,7 +147,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } /// @@ -159,7 +188,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } return *this; } @@ -169,7 +198,7 @@ public: /// ~HashSet() { - SetCapacity(0, false); + Clear(); } public: @@ -216,6 +245,7 @@ public: HashSet* _collection; int32 _index; + public: Iterator(HashSet* collection, const int32 index) : _collection(collection) , _index(index) @@ -228,7 +258,12 @@ public: { } - public: + Iterator() + : _collection(nullptr) + , _index(-1) + { + } + Iterator(const Iterator& i) : _collection(i._collection) , _index(i._index) @@ -242,6 +277,11 @@ public: } public: + FORCE_INLINE int32 Index() const + { + return _index; + } + FORCE_INLINE bool IsEnd() const { return _index == _collection->_size; @@ -374,7 +414,7 @@ public: return; ASSERT(capacity >= 0); AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _deletedCount = _elementsCount = 0; @@ -398,13 +438,21 @@ public: } _size = capacity; Bucket* oldData = oldAllocation.Get(); - if (oldElementsCount != 0 && preserveContents) + if (oldElementsCount != 0 && capacity != 0 && preserveContents) { - // TODO; move keys and values on realloc + FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) { - if (oldData[i].IsOccupied()) - Add(oldData[i].Item); + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Item, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); + bucket->_state = Bucket::Occupied; + _elementsCount++; + } } } if (oldElementsCount != 0) @@ -421,14 +469,35 @@ public: /// True if preserve collection data when changing its size, otherwise collection after resize will be empty. void EnsureCapacity(int32 minCapacity, bool preserveContents = true) { - if (Capacity() >= minCapacity) + if (_size >= minCapacity) return; - if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) - minCapacity = DICTIONARY_DEFAULT_CAPACITY; - const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + if (capacity < DICTIONARY_DEFAULT_CAPACITY) + capacity = DICTIONARY_DEFAULT_CAPACITY; SetCapacity(capacity, preserveContents); } + /// + /// Swaps the contents of collection with the other object without copy operation. Performs fast internal data exchange. + /// + /// The other collection. + void Swap(HashSet& other) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + else + { + HashSet tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } + } + public: /// /// Add element to the collection. @@ -438,24 +507,23 @@ public: template bool Add(const ItemType& item) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); + Bucket* bucket = OnAdd(item); + if (bucket) + bucket->Occupy(item); + return bucket != nullptr; + } - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(item, pos); - - // Check if object has been already added - if (pos.ObjectIndex != -1) - return false; - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; - bucket->Occupy(item); - _elementsCount++; - - return true; + /// + /// Add element to the collection. + /// + /// The element to add to the set. + /// True if element has been added to the collection, otherwise false if the element is already present. + bool Add(T&& item) + { + Bucket* bucket = OnAdd(item); + if (bucket) + bucket->Occupy(MoveTemp(item)); + return bucket != nullptr; } /// @@ -593,7 +661,7 @@ public: return Iterator(this, _size); } -protected: +private: /// /// The result container of the set item lookup searching. /// @@ -654,4 +722,66 @@ protected: result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } + + template + Bucket* OnAdd(const ItemType& key) + { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + + // Ensure to have enough memory for the next item (in case of new element insertion) + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); + + // Find location of the item or place to insert it + FindPositionResult pos; + FindPosition(key, pos); + + // Check if object has been already added + if (pos.ObjectIndex != -1) + return nullptr; + + // Insert + ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; + return &_allocation.Get()[pos.FreeSlotIndex]; + } + + void Compact() + { + if (_elementsCount == 0) + { + // Fast path if it's empty + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + } + else + { + // Rebuild entire table completely + AllocationData oldAllocation; + MoveToEmpty(oldAllocation, _allocation, _size); + _allocation.Allocate(_size); + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + Bucket* oldData = oldAllocation.Get(); + FindPositionResult pos; + for (int32 i = 0; i < _size; i++) + { + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Item, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); + bucket->_state = Bucket::Occupied; + } + } + for (int32 i = 0; i < _size; i++) + oldData[i].Free(); + } + _deletedCount = 0; + } }; diff --git a/Source/Engine/Core/Compiler.h b/Source/Engine/Core/Compiler.h index 9a33b8758..4ea246077 100644 --- a/Source/Engine/Core/Compiler.h +++ b/Source/Engine/Core/Compiler.h @@ -93,3 +93,10 @@ #endif #define PACK_STRUCT(__Declaration__) PACK_BEGIN() __Declaration__ PACK_END() + +// C++ 17 +#if __cplusplus >= 201703L +#define IF_CONSTEXPR constexpr +#else +#define IF_CONSTEXPR +#endif diff --git a/Source/Engine/Core/Config/PlatformSettingsBase.h b/Source/Engine/Core/Config/PlatformSettingsBase.h index 6d7e8601e..b372e3dd5 100644 --- a/Source/Engine/Core/Config/PlatformSettingsBase.h +++ b/Source/Engine/Core/Config/PlatformSettingsBase.h @@ -3,7 +3,7 @@ #pragma once #include "Engine/Core/Config/Settings.h" -#include "Engine/Serialization/Serialization.h" +#include "Engine/Serialization/SerializationFwd.h" /// /// Specifies the display mode of a game window. diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 4e081faef..f0f8fdef0 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -226,7 +226,7 @@ public: /// Function result FORCE_INLINE ReturnType operator()(Params... params) const { - ASSERT(_function); + ASSERT_LOW_LAYER(_function); return _function(_callee, Forward(params)...); } @@ -289,8 +289,13 @@ protected: intptr volatile _ptr = 0; intptr volatile _size = 0; #else - HashSet* _functions = nullptr; - CriticalSection* _locker = nullptr; + struct Data + { + HashSet Functions; + CriticalSection Locker; + }; + // Holds pointer to Data with Functions and Locker. Thread-safe access via atomic operations. + intptr volatile _data = 0; #endif typedef void (*StubSignature)(void*, Params...); @@ -314,15 +319,12 @@ public: _ptr = (intptr)newBindings; _size = newSize; #else - if (other._functions == nullptr) + Data* otherData = (Data*)Platform::AtomicRead(&_data); + if (otherData == nullptr) return; - _functions = New>(*other._functions); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._function && i->Item._lambda) - i->Item.LambdaCtor(); - } - _locker = other._locker; + ScopeLock lock(otherData->Locker); + for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i) + Bind(i->Item); #endif } @@ -334,10 +336,8 @@ public: other._ptr = 0; other._size = 0; #else - _functions = other._functions; - _locker = other._locker; - other._functions = nullptr; - other._locker = nullptr; + _data = other._data; + other._data = 0; #endif } @@ -356,20 +356,11 @@ public: Allocator::Free((void*)_ptr); } #else - if (_locker != nullptr) + Data* data = (Data*)_data; + if (data) { - Allocator::Free(_locker); - _locker = nullptr; - } - if (_functions != nullptr) - { - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._lambda) - i->Item.LambdaCtor(); - } - Allocator::Free(_functions); - _functions = nullptr; + _data = 0; + Delete(data); } #endif } @@ -385,8 +376,13 @@ public: for (intptr i = 0; i < size; i++) Bind(bindings[i]); #else - for (auto i = other._functions->Begin(); i.IsNotEnd(); ++i) - Bind(i->Item); + Data* otherData = (Data*)Platform::AtomicRead(&_data); + if (otherData != nullptr) + { + ScopeLock lock(otherData->Locker); + for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i) + Bind(i->Item); + } #endif } return *this; @@ -402,10 +398,8 @@ public: other._ptr = 0; other._size = 0; #else - _functions = other._functions; - _locker = other._locker; - other._functions = nullptr; - other._locker = nullptr; + _data = other._data; + other._data = 0; #endif } return *this; @@ -507,12 +501,20 @@ public: Allocator::Free(bindings); } #else - if (_locker == nullptr) - _locker = New(); - ScopeLock lock(*_locker); - if (_functions == nullptr) - _functions = New>(32); - _functions->Add(f); + Data* data = (Data*)Platform::AtomicRead(&_data); + while (!data) + { + Data* newData = New(); + Data* oldData = (Data*)Platform::InterlockedCompareExchange(&_data, (intptr)newData, (intptr)data); + if (oldData != data) + { + // Other thread already set the new data so free it and try again + Delete(newData); + } + data = (Data*)Platform::AtomicRead(&_data); + } + ScopeLock lock(data->Locker); + data->Functions.Add(f); #endif } @@ -568,13 +570,22 @@ public: } } #else - if (_locker == nullptr) - _locker = New(); - ScopeLock lock(*_locker); - if (_functions && _functions->Contains(f)) - return; + Data* data = (Data*)Platform::AtomicRead(&_data); + if (data) + { + data->Locker.Lock(); + if (data->Functions.Contains(f)) + { + data->Locker.Unlock(); + return; + } + } #endif Bind(f); +#if !DELEGATE_USE_ATOMIC + if (data) + data->Locker.Unlock(); +#endif } /// @@ -583,18 +594,9 @@ public: template void Unbind() { -#if DELEGATE_USE_ATOMIC FunctionType f; f.template Bind(); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f; - f.template Bind(); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -604,18 +606,9 @@ public: template void Unbind(T* callee) { -#if DELEGATE_USE_ATOMIC FunctionType f; f.template Bind(callee); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f; - f.template Bind(callee); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -624,16 +617,8 @@ public: /// The method. void Unbind(Signature method) { -#if DELEGATE_USE_ATOMIC FunctionType f(method); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f(method); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -666,10 +651,11 @@ public: Unbind(f); } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead(&_data); + if (!data) return; - ScopeLock lock(*_locker); - _functions->Remove(f); + ScopeLock lock(data->Locker); + data->Functions.Remove(f); #endif } @@ -692,15 +678,11 @@ public: Platform::AtomicStore((intptr volatile*)&bindings[i]._callee, 0); } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead(&_data); + if (!data) return; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._lambda) - i->Item.LambdaDtor(); - } - _functions->Clear(); + ScopeLock lock(data->Locker); + data->Functions.Clear(); #endif } @@ -710,22 +692,24 @@ public: /// The bound functions count. int32 Count() const { + int32 result = 0; #if DELEGATE_USE_ATOMIC - int32 count = 0; const intptr size = Platform::AtomicRead((intptr volatile*)&_size); FunctionType* bindings = (FunctionType*)Platform::AtomicRead((intptr volatile*)&_ptr); for (intptr i = 0; i < size; i++) { if (Platform::AtomicRead((intptr volatile*)&bindings[i]._function) != 0) - count++; + result++; } - return count; #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - return _functions->Count(); + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Count(); + } #endif + return result; } /// @@ -736,10 +720,14 @@ public: #if DELEGATE_USE_ATOMIC return (int32)Platform::AtomicRead((intptr volatile*)&_size); #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - return _functions->Capacity(); + int32 result = 0; + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Capacity(); + } + return result; #endif } @@ -759,10 +747,14 @@ public: } return false; #else - if (_functions == nullptr) - return false; - ScopeLock lock(*_locker); - return _functions->Count() > 0; + bool result = false; + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Count() != 0; + } + return result; #endif } @@ -791,18 +783,13 @@ public: } } #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) { - if (i->Item._function != nullptr) + ScopeLock lock(data->Locker); + for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i) { - buffer[count]._function = (StubSignature)i->Item._function; - buffer[count]._callee = (void*)i->Item._callee; - buffer[count]._lambda = (typename FunctionType::Lambda*)i->Item._lambda; - if (buffer[count]._lambda) - buffer[count].LambdaCtor(); + new(buffer + count) FunctionType((const FunctionType&)i->Item); count++; } } @@ -828,15 +815,15 @@ public: ++bindings; } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (!data) return; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) + ScopeLock lock(data->Locker); + for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i) { - auto function = (StubSignature)(i->Item._function); - auto callee = (void*)(i->Item._callee); - if (function != nullptr) - function(callee, Forward(params)...); + const FunctionType& item = i->Item; + ASSERT_LOW_LAYER(item._function); + item._function(item._callee, Forward(params)...); } #endif } diff --git a/Source/Engine/Core/Math/Math.h b/Source/Engine/Core/Math/Math.h index 276f64884..a3c05fce4 100644 --- a/Source/Engine/Core/Math/Math.h +++ b/Source/Engine/Core/Math/Math.h @@ -728,9 +728,7 @@ namespace Math /// /// Returns value based on comparand. The main purpose of this function is to avoid branching based on floating point comparison which can be avoided via compiler intrinsics. /// - /// - /// Please note that this doesn't define what happens in the case of NaNs as there might be platform specific differences. - /// + /// Please note that this doesn't define what happens in the case of NaNs as there might be platform specific differences. /// Comparand the results are based on. /// The result value if comparand >= 0. /// The result value if comparand < 0. @@ -891,6 +889,18 @@ namespace Math return Lerp(a, b, alpha < 0.5f ? InterpCircularIn(0.f, 1.f, alpha * 2.f) * 0.5f : InterpCircularOut(0.f, 1.f, alpha * 2.f - 1.f) * 0.5f + 0.5f); } + /// + /// Ping pongs the value , so that it is never larger than and never smaller than 0. + /// + /// + /// + /// + template + static FORCE_INLINE T PingPong(const T& t, T length) + { + return length - Abs(Repeat(t, length * 2.0f) - length); + } + // Rotates position about the given axis by the given angle, in radians, and returns the offset to position Vector3 FLAXENGINE_API RotateAboutAxis(const Vector3& normalizedRotationAxis, float angle, const Vector3& positionOnAxis, const Vector3& position); diff --git a/Source/Engine/Core/Math/Quaternion.cpp b/Source/Engine/Core/Math/Quaternion.cpp index bdbe3037d..1ca05a9c3 100644 --- a/Source/Engine/Core/Math/Quaternion.cpp +++ b/Source/Engine/Core/Math/Quaternion.cpp @@ -382,9 +382,8 @@ void Quaternion::GetRotationFromTo(const Float3& from, const Float3& to, Quatern v0.Normalize(); v1.Normalize(); - const float d = Float3::Dot(v0, v1); - // If dot == 1, vectors are the same + const float d = Float3::Dot(v0, v1); if (d >= 1.0f) { result = Identity; diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs index ff50a98bf..34ee160f0 100644 --- a/Source/Engine/Core/Math/Quaternion.cs +++ b/Source/Engine/Core/Math/Quaternion.cs @@ -1077,6 +1077,115 @@ namespace FlaxEngine } } + /// + /// Gets the shortest arc quaternion to rotate this vector to the destination vector. + /// + /// The source vector. + /// The destination vector. + /// The result. + /// The fallback axis. + public static void GetRotationFromTo(ref Float3 from, ref Float3 to, out Quaternion result, ref Float3 fallbackAxis) + { + // Based on Stan Melax's article in Game Programming Gems + + Float3 v0 = from; + Float3 v1 = to; + v0.Normalize(); + v1.Normalize(); + + // If dot == 1, vectors are the same + float d = Float3.Dot(ref v0, ref v1); + if (d >= 1.0f) + { + result = Identity; + return; + } + + if (d < 1e-6f - 1.0f) + { + if (fallbackAxis != Float3.Zero) + { + // Rotate 180 degrees about the fallback axis + RotationAxis(ref fallbackAxis, Mathf.Pi, out result); + } + else + { + // Generate an axis + Float3 axis = Float3.Cross(Float3.UnitX, from); + if (axis.LengthSquared < Mathf.Epsilon) // Pick another if colinear + axis = Float3.Cross(Float3.UnitY, from); + axis.Normalize(); + RotationAxis(ref axis, Mathf.Pi, out result); + } + } + else + { + float s = Mathf.Sqrt((1 + d) * 2); + float invS = 1 / s; + Float3.Cross(ref v0, ref v1, out var c); + result.X = c.X * invS; + result.Y = c.Y * invS; + result.Z = c.Z * invS; + result.W = s * 0.5f; + result.Normalize(); + } + } + + /// + /// Gets the shortest arc quaternion to rotate this vector to the destination vector. + /// + /// The source vector. + /// The destination vector. + /// The fallback axis. + /// The rotation. + public static Quaternion GetRotationFromTo(Float3 from, Float3 to, Float3 fallbackAxis) + { + GetRotationFromTo(ref from, ref to, out var result, ref fallbackAxis); + return result; + } + + /// + /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized. + /// + /// The source vector. + /// The destination vector. + /// The result. + public static void FindBetween(ref Float3 from, ref Float3 to, out Quaternion result) + { + // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final + float normFromNormTo = Mathf.Sqrt(from.LengthSquared * to.LengthSquared); + if (normFromNormTo < Mathf.Epsilon) + { + result = Identity; + return; + } + float w = normFromNormTo + Float3.Dot(from, to); + if (w < 1.0-6f * normFromNormTo) + { + result = Mathf.Abs(from.X) > Mathf.Abs(from.Z) + ? new Quaternion(-from.Y, from.X, 0.0f, 0.0f) + : new Quaternion(0.0f, -from.Z, from.Y, 0.0f); + } + else + { + Float3 cross = Float3.Cross(from, to); + result = new Quaternion(cross.X, cross.Y, cross.Z, w); + } + result.Normalize(); + } + + /// + /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized. + /// + /// The source vector. + /// The destination vector. + /// The rotation. + public static Quaternion FindBetween(Float3 from, Float3 to) + { + FindBetween(ref from, ref to, out var result); + return result; + } + /// /// Creates a left-handed spherical billboard that rotates around a specified object position. /// diff --git a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs index afd34bbfd..65377acaa 100644 --- a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs +++ b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs @@ -13,9 +13,7 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } return base.CanConvertFrom(context, sourceType); } @@ -23,9 +21,7 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } diff --git a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs index 4eebbfce4..e0670df05 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double2Converter : TypeConverter + internal class Double2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double2(double.Parse(v[0], culture), double.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs index 420e0016c..a66892ecb 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double3Converter : TypeConverter + internal class Double3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double3(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs index fc1d9a7fe..d085217ef 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double4Converter : TypeConverter + internal class Double4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double4(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture), double.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs index a41a0f4d5..4b2ffadf5 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float2Converter : TypeConverter + internal class Float2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float2(float.Parse(v[0], culture), float.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs index aded4117e..3739c44ef 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float3Converter : TypeConverter + internal class Float3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs index 58c76ac65..620f2c838 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs @@ -7,15 +7,13 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float4Converter : TypeConverter + internal class VectorConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } return base.CanConvertFrom(context, sourceType); } @@ -23,18 +21,32 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } + internal static string[] GetParts(string str) + { + string[] v = str.Split(','); + if (v.Length == 1) + { + // When converting from ToString() + v = str.Split(' '); + for (int i = 0; i < v.Length; i++) + v[i] = v[i].Substring(v[i].IndexOf(':') + 1); + } + return v; + } + } + + internal class Float4Converter : VectorConverter + { /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs index c4989c085..f528aa46b 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int2Converter : TypeConverter + internal class Int2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int2(int.Parse(v[0], culture), int.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs index fe01f91fd..520f806d0 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int3Converter : TypeConverter + internal class Int3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int3(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs index 2ce0fc202..e9a27dfda 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int4Converter : TypeConverter + internal class Int4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int4(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture), int.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs index 23bb901be..5d9aa206b 100644 --- a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs +++ b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class QuaternionConverter : TypeConverter + internal class QuaternionConverter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Quaternion(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs index 96d6beadc..acb5b5817 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector2Converter : TypeConverter + internal class Vector2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector2(float.Parse(v[0], culture), float.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs index 23ee4df11..66ec831f0 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector3Converter : TypeConverter + internal class Vector3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs index c3b4d074b..f4781f45b 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector4Converter : TypeConverter + internal class Vector4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index 83deb009a..cea03ec10 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -646,6 +646,12 @@ inline Vector2Base operator/(typename TOtherFloat::Type a, const Vector2Ba return Vector2Base(a) / b; } +template +inline uint32 GetHash(const Vector2Base& key) +{ + return (*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y; +} + namespace Math { template diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h index c0ebdf01d..01b55e9bd 100644 --- a/Source/Engine/Core/Math/Vector3.h +++ b/Source/Engine/Core/Math/Vector3.h @@ -977,6 +977,12 @@ inline Vector3Base operator/(typename TOtherFloat::Type a, const Vector3Ba return Vector3Base(a) / b; } +template +inline uint32 GetHash(const Vector3Base& key) +{ + return (((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z; +} + namespace Math { template diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 5c7b24c4a..1cc6d4db8 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -552,6 +552,12 @@ inline Vector4Base operator/(typename TOtherFloat::Type a, const Vector4Ba return Vector4Base(a) / b; } +template +inline uint32 GetHash(const Vector4Base& key) +{ + return (((((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z) * 397) ^*(uint32*)&key.W; +} + namespace Math { template diff --git a/Source/Engine/Core/Memory/Allocation.h b/Source/Engine/Core/Memory/Allocation.h index 89d2f2003..fb6b4555b 100644 --- a/Source/Engine/Core/Memory/Allocation.h +++ b/Source/Engine/Core/Memory/Allocation.h @@ -12,6 +12,8 @@ template class FixedAllocation { public: + enum { HasSwap = false }; + template class Data { @@ -43,14 +45,14 @@ public: return Capacity; } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(capacity <= Capacity); #endif } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(capacity <= Capacity); @@ -61,12 +63,9 @@ public: { } - FORCE_INLINE void Swap(Data& other) + void Swap(Data& other) { - byte tmp[Capacity * sizeof(T)]; - Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T)); - Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T)); - Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T)); + // Not supported } }; }; @@ -77,6 +76,8 @@ public: class HeapAllocation { public: + enum { HasSwap = true }; + template class Data { @@ -120,12 +121,15 @@ public: capacity |= capacity >> 4; capacity |= capacity >> 8; capacity |= capacity >> 16; - capacity = (capacity + 1) * 2; + uint64 capacity64 = (uint64)(capacity + 1) * 2; + if (capacity64 > MAX_int32) + capacity64 = MAX_int32; + capacity = (int32)capacity64; } return capacity; } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(!_data); @@ -137,7 +141,7 @@ public: #endif } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { T* newData = capacity != 0 ? (T*)Allocator::Allocate(capacity * sizeof(T)) : nullptr; #if !BUILD_RELEASE @@ -176,6 +180,8 @@ template class InlinedAllocation { public: + enum { HasSwap = false }; + template class Data { @@ -210,7 +216,7 @@ public: return minCapacity <= Capacity ? Capacity : _other.CalculateCapacityGrow(capacity, minCapacity); } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { if (capacity > Capacity) { @@ -219,7 +225,7 @@ public: } } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { // Check if the new allocation will fit into inlined storage if (capacity <= Capacity) @@ -264,14 +270,9 @@ public: } } - FORCE_INLINE void Swap(Data& other) + void Swap(Data& other) { - byte tmp[Capacity * sizeof(T)]; - Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T)); - Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T)); - Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T)); - ::Swap(_useOther, other._useOther); - _other.Swap(other._other); + // Not supported } }; }; diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp index a020f7844..a185ce8b3 100644 --- a/Source/Engine/Core/ObjectsRemovalService.cpp +++ b/Source/Engine/Core/ObjectsRemovalService.cpp @@ -5,6 +5,7 @@ #include "Collections/Dictionary.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/EngineService.h" +#include "Engine/Platform/CriticalSection.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Scripting/ScriptingObject.h" diff --git a/Source/Engine/Core/Types/StringBuilder.h b/Source/Engine/Core/Types/StringBuilder.h index 051554a23..068f245d0 100644 --- a/Source/Engine/Core/Types/StringBuilder.h +++ b/Source/Engine/Core/Types/StringBuilder.h @@ -3,6 +3,7 @@ #pragma once #include "String.h" +#include "StringView.h" #include "Engine/Core/Collections/Array.h" /// @@ -138,6 +139,11 @@ public: _data.Add(*str, str.Length()); return *this; } + StringBuilder& Append(const StringView& str) + { + _data.Add(*str, str.Length()); + return *this; + } // Append int to the string // @param val Value to append diff --git a/Source/Engine/Core/Types/StringView.h b/Source/Engine/Core/Types/StringView.h index 27c63c999..3e2bbd5f3 100644 --- a/Source/Engine/Core/Types/StringView.h +++ b/Source/Engine/Core/Types/StringView.h @@ -327,6 +327,18 @@ public: bool operator!=(const String& other) const; public: + using StringViewBase::StartsWith; + FORCE_INLINE bool StartsWith(const StringView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::StartsWith(prefix, searchCase); + } + + using StringViewBase::EndsWith; + FORCE_INLINE bool EndsWith(const StringView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::EndsWith(suffix, searchCase); + } + /// /// Gets the left most given number of characters. /// @@ -511,6 +523,18 @@ public: bool operator!=(const StringAnsi& other) const; public: + using StringViewBase::StartsWith; + FORCE_INLINE bool StartsWith(const StringAnsiView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::StartsWith(prefix, searchCase); + } + + using StringViewBase::EndsWith; + FORCE_INLINE bool EndsWith(const StringAnsiView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::EndsWith(suffix, searchCase); + } + /// /// Retrieves substring created from characters starting from startIndex to the String end. /// diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index df044b299..952e648e6 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -2821,7 +2821,10 @@ void Variant::Inline() type = VariantType::Types::Vector4; } if (type != VariantType::Null) + { + ASSERT(sizeof(data) >= AsBlob.Length); Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length); + } } if (type != VariantType::Null) { @@ -2912,6 +2915,60 @@ void Variant::Inline() } } +void Variant::InvertInline() +{ + byte data[sizeof(Matrix)]; + switch (Type.Type) + { + case VariantType::Bool: + case VariantType::Int: + case VariantType::Uint: + case VariantType::Int64: + case VariantType::Uint64: + case VariantType::Float: + case VariantType::Double: + case VariantType::Pointer: + case VariantType::String: + case VariantType::Float2: + case VariantType::Float3: + case VariantType::Float4: + case VariantType::Color: +#if !USE_LARGE_WORLDS + case VariantType::BoundingSphere: + case VariantType::BoundingBox: + case VariantType::Ray: +#endif + case VariantType::Guid: + case VariantType::Quaternion: + case VariantType::Rectangle: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: + case VariantType::Int16: + case VariantType::Uint16: + case VariantType::Double2: + case VariantType::Double3: + case VariantType::Double4: + static_assert(sizeof(data) >= sizeof(AsData), "Invalid memory size."); + Platform::MemoryCopy(data, AsData, sizeof(AsData)); + break; +#if USE_LARGE_WORLDS + case VariantType::BoundingSphere: + case VariantType::BoundingBox: + case VariantType::Ray: +#endif + case VariantType::Transform: + case VariantType::Matrix: + ASSERT(sizeof(data) >= AsBlob.Length); + Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length); + break; + default: + return; // Not used + } + SetType(VariantType(VariantType::Structure, InBuiltTypesTypeNames[Type.Type])); + CopyStructure(data); +} + Variant Variant::NewValue(const StringAnsiView& typeName) { Variant v; diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 8cc1e133b..adb49249f 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -372,6 +372,9 @@ public: // Inlines potential value type into in-built format (eg. Vector3 stored as Structure, or String stored as ManagedObject). void Inline(); + // Inverts the inlined value from in-built format into generic storage (eg. Float3 from inlined format into Structure). + void InvertInline(); + // Allocates the Variant of the specific type (eg. structure or object or value). static Variant NewValue(const StringAnsiView& typeName); diff --git a/Source/Engine/Core/Types/Version.cpp b/Source/Engine/Core/Types/Version.cpp index 4a11a0af6..c0410d1cb 100644 --- a/Source/Engine/Core/Types/Version.cpp +++ b/Source/Engine/Core/Types/Version.cpp @@ -7,15 +7,15 @@ Version::Version(int32 major, int32 minor, int32 build, int32 revision) { _major = Math::Max(major, 0); _minor = Math::Max(minor, 0); - _build = Math::Max(build, 0); - _revision = Math::Max(revision, 0); + _build = Math::Max(build, -1); + _revision = Math::Max(revision, -1); } Version::Version(int32 major, int32 minor, int32 build) { _major = Math::Max(major, 0); _minor = Math::Max(minor, 0); - _build = Math::Max(build, 0); + _build = Math::Max(build, -1); _revision = -1; } diff --git a/Source/Engine/Core/Utilities.h b/Source/Engine/Core/Utilities.h index 36339baf5..737e423b2 100644 --- a/Source/Engine/Core/Utilities.h +++ b/Source/Engine/Core/Utilities.h @@ -51,10 +51,14 @@ namespace Utilities int32 i = 0; double dblSUnits = static_cast(units); for (; static_cast(units / static_cast(divider)) > 0; i++, units /= divider) - dblSUnits = units / static_cast(divider); + dblSUnits = (double)units / (double)divider; if (i >= sizes.Length()) i = 0; - return String::Format(TEXT("{0} {1}"), RoundTo2DecimalPlaces(dblSUnits), sizes[i]); + String text = String::Format(TEXT("{}"), RoundTo2DecimalPlaces(dblSUnits)); + const int32 dot = text.FindLast('.'); + if (dot != -1) + text = text.Left(dot + 3); + return String::Format(TEXT("{0} {1}"), text, sizes[i]); } // Converts size of the file (in bytes) to the best fitting string diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp index 48fa31c91..059ebbd5d 100644 --- a/Source/Engine/Debug/DebugDraw.cpp +++ b/Source/Engine/Debug/DebugDraw.cpp @@ -696,12 +696,15 @@ void* DebugDraw::AllocateContext() void DebugDraw::FreeContext(void* context) { + ASSERT(context); Memory::DestructItem((DebugDrawContext*)context); Allocator::Free(context); } void DebugDraw::UpdateContext(void* context, float deltaTime) { + if (!context) + context = &GlobalContext; ((DebugDrawContext*)context)->DebugDrawDefault.Update(deltaTime); ((DebugDrawContext*)context)->DebugDrawDepthTest.Update(deltaTime); } diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index 1a57c4f10..259649062 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -20,7 +20,6 @@ #include "Engine/Threading/MainThreadTask.h" #include "Engine/Threading/ThreadRegistry.h" #include "Engine/Graphics/GPUDevice.h" -#include "Engine/Scripting/ManagedCLR/MCore.h" #include "Engine/Scripting/ScriptingType.h" #include "Engine/Content/Content.h" #include "Engine/Content/JsonAsset.h" @@ -327,14 +326,6 @@ void Engine::OnUpdate() // Update services EngineService::OnUpdate(); - -#ifdef USE_NETCORE - // Force GC to run in background periodically to avoid large blocking collections causing hitches - if (Time::Update.TicksCount % 60 == 0) - { - MCore::GC::Collect(MCore::GC::MaxGeneration(), MGCCollectionMode::Forced, false, false); - } -#endif } void Engine::OnLateUpdate() @@ -531,7 +522,13 @@ void EngineImpl::InitLog() LOG(Info, "Compiled for Dev Environment"); #endif LOG(Info, "Version " FLAXENGINE_VERSION_TEXT); - LOG(Info, "Compiled: {0} {1}", TEXT(__DATE__), TEXT(__TIME__)); + const Char* cpp = TEXT("?"); + if (__cplusplus == 202101L) cpp = TEXT("C++23"); + else if (__cplusplus == 202002L) cpp = TEXT("C++20"); + else if (__cplusplus == 201703L) cpp = TEXT("C++17"); + else if (__cplusplus == 201402L) cpp = TEXT("C++14"); + else if (__cplusplus == 201103L) cpp = TEXT("C++11"); + LOG(Info, "Compiled: {0} {1} {2}", TEXT(__DATE__), TEXT(__TIME__), cpp); #ifdef _MSC_VER const String mcsVer = StringUtils::ToString(_MSC_FULL_VER); LOG(Info, "Compiled with Visual C++ {0}.{1}.{2}.{3:0^2d}", mcsVer.Substring(0, 2), mcsVer.Substring(2, 2), mcsVer.Substring(4, 5), _MSC_BUILD); diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 0391974b6..e99276a4e 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1022,6 +1022,8 @@ namespace FlaxEngine.Interop pair.Value.Free(); classAttributesCacheCollectible.Clear(); + FlaxEngine.Json.JsonSerializer.ResetCache(); + // Unload the ALC bool unloading = true; scriptingAssemblyLoadContext.Unloading += (alc) => { unloading = false; }; @@ -1269,6 +1271,9 @@ namespace FlaxEngine.Interop case Type _ when type == typeof(IntPtr): monoType = MTypes.Ptr; break; + case Type _ when type.IsPointer: + monoType = MTypes.Ptr; + break; case Type _ when type.IsEnum: monoType = MTypes.Enum; break; diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 2f74e421c..fc04d9668 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -278,44 +278,70 @@ namespace FlaxEngine.Interop if (typeCache.TryGetValue(typeName, out Type type)) return type; - type = Type.GetType(typeName, ResolveAssemblyByName, null); + type = Type.GetType(typeName, ResolveAssembly, null); if (type == null) - { - foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) - { - type = assembly.GetType(typeName); - if (type != null) - break; - } - } + type = ResolveSlow(typeName); if (type == null) { - string oldTypeName = typeName; + string fullTypeName = typeName; typeName = typeName.Substring(0, typeName.IndexOf(',')); - type = Type.GetType(typeName, ResolveAssemblyByName, null); + type = Type.GetType(typeName, ResolveAssembly, null); if (type == null) - { - foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) - { - type = assembly.GetType(typeName); - if (type != null) - break; - } - } - typeName = oldTypeName; + type = ResolveSlow(typeName); + + typeName = fullTypeName; } typeCache.Add(typeName, type); return type; + + static Type ResolveSlow(string typeName) + { + foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) + { + var type = assembly.GetType(typeName); + if (type != null) + return type; + } + return null; + } + + static Assembly ResolveAssembly(AssemblyName name) => ResolveScriptingAssemblyByName(name, allowPartial: false); } - private static Assembly ResolveAssemblyByName(AssemblyName assemblyName) + /// Find among the scripting assemblies. + /// The name to find + /// If true, partial names should be allowed to be resolved. + /// The resolved assembly, or null if none could be found. + internal static Assembly ResolveScriptingAssemblyByName(AssemblyName assemblyName, bool allowPartial = false) { - foreach (Assembly assembly in scriptingAssemblyLoadContext.Assemblies) - if (assembly.GetName() == assemblyName) + var lc = scriptingAssemblyLoadContext; + + if (lc is null) + return null; + + foreach (Assembly assembly in lc.Assemblies) + { + var curName = assembly.GetName(); + + if (curName == assemblyName) return assembly; + } + + if (allowPartial) // Check partial names if full name isn't found + { + string partialName = assemblyName.Name; + + foreach (Assembly assembly in lc.Assemblies) + { + var curName = assembly.GetName(); + + if (curName.Name == partialName) + return assembly; + } + } return null; } @@ -1109,7 +1135,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, fieldPtr, out int fieldSize); fieldPtr += fieldSize; } - Assert.IsTrue((fieldPtr - nativePtr) <= Unsafe.SizeOf()); + //Assert.IsTrue((fieldPtr - nativePtr) <= GetTypeSize(typeof(T))); } internal static void ToManaged(ref T managedValue, IntPtr nativePtr, bool byRef) @@ -1156,7 +1182,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, nativePtr, out int fieldSize); nativePtr += fieldSize; } - Assert.IsTrue((nativePtr - fieldPtr) <= Unsafe.SizeOf()); + //Assert.IsTrue((nativePtr - fieldPtr) <= GetTypeSize(typeof(T))); } internal static void ToNative(ref T managedValue, IntPtr nativePtr) @@ -1302,7 +1328,8 @@ namespace FlaxEngine.Interop #if !USE_AOT internal bool TryGetDelegate(out Invoker.MarshalAndInvokeDelegate outDeleg, out object outDelegInvoke) { - if (invokeDelegate == null) + // Skip using in-built delegate for value types (eg. Transform) to properly handle instance value passing to method + if (invokeDelegate == null && !method.DeclaringType.IsValueType) { List methodTypes = new List(); if (!method.IsStatic) @@ -1553,7 +1580,7 @@ namespace FlaxEngine.Interop private static IntPtr PinValue(T value) where T : struct { // Store the converted value in unmanaged memory so it will not be relocated by the garbage collector. - int size = Unsafe.SizeOf(); + int size = GetTypeSize(typeof(T)); uint index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length; ref (IntPtr ptr, int size) alloc = ref pinnedAllocations[index]; if (alloc.size < size) diff --git a/Source/Engine/Foliage/FoliageType.h b/Source/Engine/Foliage/FoliageType.h index 0bc556fc9..4251b6924 100644 --- a/Source/Engine/Foliage/FoliageType.h +++ b/Source/Engine/Foliage/FoliageType.h @@ -46,7 +46,7 @@ API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FoliageType : public ScriptingOb DECLARE_SCRIPTING_TYPE_NO_SPAWN(FoliageType); friend Foliage; private: - int8 _isReady : 1; + uint8 _isReady : 1; public: /// diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h index 86c723530..f57327fa4 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h @@ -26,12 +26,12 @@ public: , _srcResource(src) , _dstResource(dst) { - _srcResource.OnUnload.Bind(this); - _dstResource.OnUnload.Bind(this); + _srcResource.Released.Bind(this); + _dstResource.Released.Bind(this); } private: - void OnResourceUnload(GPUResourceReference* ref) + void OnResourceReleased() { Cancel(); } @@ -47,14 +47,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_srcResource.IsMissing() || _dstResource.IsMissing()) + if (!_srcResource || !_dstResource) return Result::MissingResources; - context->GPU->CopyResource(_dstResource, _srcResource); - return Result::Ok; } - void OnEnd() override { _srcResource.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h index ab8f1ffad..193eb965d 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h @@ -31,12 +31,12 @@ public: , _srcSubresource(srcSubresource) , _dstSubresource(dstSubresource) { - _srcResource.OnUnload.Bind(this); - _dstResource.OnUnload.Bind(this); + _srcResource.Released.Bind(this); + _dstResource.Released.Bind(this); } private: - void OnResourceUnload(GPUResourceReference* ref) + void OnResourceReleased() { Cancel(); } @@ -52,14 +52,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_srcResource.IsMissing() || _dstResource.IsMissing()) + if (!_srcResource || !_dstResource) return Result::MissingResources; - context->GPU->CopySubresource(_dstResource, _dstSubresource, _srcResource, _srcSubresource); - return Result::Ok; } - void OnEnd() override { _srcResource.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h index d2f20449c..3d38ce58b 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h @@ -31,7 +31,7 @@ public: , _buffer(buffer) , _offset(offset) { - _buffer.OnUnload.Bind(this); + _buffer.Released.Bind(this); if (copyData) _data.Copy(data); @@ -40,7 +40,7 @@ public: } private: - void OnResourceUnload(BufferReference* ref) + void OnResourceReleased() { Cancel(); } @@ -56,14 +56,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_buffer.IsMissing()) + if (!_buffer) return Result::MissingResources; - context->GPU->UpdateBuffer(_buffer, _data.Get(), _data.Length(), _offset); - return Result::Ok; } - void OnEnd() override { _buffer.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h index 6e9cca7fd..2aff3511b 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h @@ -35,7 +35,7 @@ public: , _rowPitch(rowPitch) , _slicePitch(slicePitch) { - _texture.OnUnload.Bind(this); + _texture.Released.Bind(this); if (copyData) _data.Copy(data); @@ -44,7 +44,7 @@ public: } private: - void OnResourceUnload(GPUTextureReference* ref) + void OnResourceReleased() { Cancel(); } diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index 4d1422f96..bbf9c0de8 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -591,37 +591,37 @@ API_ENUM() enum class Quality : byte API_ENUM() enum class MaterialPostFxLocation : byte { /// - /// The 'after' post processing pass using LDR input frame. + /// Render the material after the post processing pass using *LDR* input frame. /// AfterPostProcessingPass = 0, /// - /// The 'before' post processing pass using HDR input frame. + /// Render the material before the post processing pass using *HDR* input frame. /// BeforePostProcessingPass = 1, /// - /// The 'before' forward pass but after GBuffer with HDR input frame. + /// Render the material before the forward pass but after *GBuffer* with *HDR* input frame. /// BeforeForwardPass = 2, /// - /// The 'after' custom post effects. + /// Render the material after custom post effects (scripted). /// AfterCustomPostEffects = 3, /// - /// The 'before' Reflections pass. After the Light pass. Can be used to implement a custom light types that accumulate lighting to the light buffer. + /// Render the material before the reflections pass but after the lighting pass using *HDR* input frame. It can be used to implement a custom light types that accumulate lighting to the light buffer. /// BeforeReflectionsPass = 4, /// - /// The 'after' AA filter pass. Rendering is done to the output backbuffer. + /// Render the material after anti-aliasing into the output backbuffer. /// AfterAntiAliasingPass = 5, /// - /// The 'after' forward pass but before any post processing. + /// Render the material after the forward pass but before any post processing. /// AfterForwardPass = 6, diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp index 34d8e7661..2a54c7c4d 100644 --- a/Source/Engine/Graphics/GPUDevice.cpp +++ b/Source/Engine/Graphics/GPUDevice.cpp @@ -3,6 +3,7 @@ #include "GPUDevice.h" #include "RenderTargetPool.h" #include "GPUPipelineState.h" +#include "GPUResourceProperty.h" #include "GPUSwapChain.h" #include "RenderTask.h" #include "RenderTools.h" @@ -25,6 +26,39 @@ #include "Engine/Renderer/RenderList.h" #include "Engine/Scripting/Enums.h" +GPUResourcePropertyBase::~GPUResourcePropertyBase() +{ + const auto e = _resource; + if (e) + { + _resource = nullptr; + e->Releasing.Unbind(this); + } +} + +void GPUResourcePropertyBase::OnSet(GPUResource* resource) +{ + auto e = _resource; + if (e != resource) + { + if (e) + e->Releasing.Unbind(this); + _resource = e = resource; + if (e) + e->Releasing.Bind(this); + } +} + +void GPUResourcePropertyBase::OnReleased() +{ + auto e = _resource; + if (e) + { + _resource = nullptr; + e->Releasing.Unbind(this); + } +} + GPUPipelineState* GPUPipelineState::Spawn(const SpawnParams& params) { return GPUDevice::Instance->CreatePipelineState(); @@ -313,6 +347,8 @@ bool GPUDevice::Init() _res->TasksManager.SetExecutor(CreateTasksExecutor()); LOG(Info, "Total graphics memory: {0}", Utilities::BytesToText(TotalGraphicsMemory)); + if (!Limits.HasCompute) + LOG(Warning, "Compute Shaders are not supported"); return false; } diff --git a/Source/Engine/Graphics/GPUResourceProperty.h b/Source/Engine/Graphics/GPUResourceProperty.h index b3c56007d..0b5d73c40 100644 --- a/Source/Engine/Graphics/GPUResourceProperty.h +++ b/Source/Engine/Graphics/GPUResourceProperty.h @@ -8,28 +8,39 @@ /// /// GPU Resource container utility object. /// -template -class GPUResourceProperty +class FLAXENGINE_API GPUResourcePropertyBase { -private: - T* _resource; +protected: + GPUResource* _resource = nullptr; -private: - // Disable copy actions - GPUResourceProperty(const GPUResourceProperty& other) = delete; +public: + NON_COPYABLE(GPUResourcePropertyBase); + + GPUResourcePropertyBase() = default; + ~GPUResourcePropertyBase(); public: /// - /// Action fired when resource gets unloaded (reference gets cleared bu async tasks should stop execution). + /// Action fired when resource gets released (reference gets cleared bu async tasks should stop execution). /// - Delegate OnUnload; + Action Released; +protected: + void OnSet(GPUResource* resource); + void OnReleased(); +}; + +/// +/// GPU Resource container utility object. +/// +template +class GPUResourceProperty : public GPUResourcePropertyBase +{ public: /// /// Initializes a new instance of the class. /// GPUResourceProperty() - : _resource(nullptr) { } @@ -38,9 +49,37 @@ public: /// /// The resource. GPUResourceProperty(T* resource) - : _resource(nullptr) { - Set(resource); + OnSet(resource); + } + + /// + /// Initializes a new instance of the class. + /// + /// The other value. + GPUResourceProperty(const GPUResourceProperty& other) + { + OnSet(other.Get()); + } + + /// + /// Initializes a new instance of the class. + /// + /// The other value. + GPUResourceProperty(GPUResourceProperty&& other) + { + OnSet(other.Get()); + other.OnSet(nullptr); + } + + GPUResourceProperty& operator=(GPUResourceProperty&& other) + { + if (&other != this) + { + OnSet(other._resource); + other.OnSet(nullptr); + } + return *this; } /// @@ -48,13 +87,6 @@ public: /// ~GPUResourceProperty() { - // Check if object has been binded - if (_resource) - { - // Unlink - _resource->Releasing.template Unbind(this); - _resource = nullptr; - } } public: @@ -63,43 +95,34 @@ public: return Get() == other; } - FORCE_INLINE bool operator==(GPUResourceProperty& other) const + FORCE_INLINE bool operator==(const GPUResourceProperty& other) const { return Get() == other.Get(); } - GPUResourceProperty& operator=(const GPUResourceProperty& other) - { - if (this != &other) - Set(other.Get()); - return *this; - } - FORCE_INLINE GPUResourceProperty& operator=(T& other) { - Set(&other); + OnSet(&other); return *this; } FORCE_INLINE GPUResourceProperty& operator=(T* other) { - Set(other); + OnSet(other); return *this; } /// /// Implicit conversion to GPU Resource /// - /// Resource FORCE_INLINE operator T*() const { - return _resource; + return (T*)_resource; } /// /// Implicit conversion to resource /// - /// True if resource has been binded, otherwise false FORCE_INLINE operator bool() const { return _resource != nullptr; @@ -108,37 +131,17 @@ public: /// /// Implicit conversion to resource /// - /// Resource FORCE_INLINE T* operator->() const { - return _resource; + return (T*)_resource; } /// /// Gets linked resource /// - /// Resource FORCE_INLINE T* Get() const { - return _resource; - } - - /// - /// Checks if resource has been binded - /// - /// True if resource has been binded, otherwise false - FORCE_INLINE bool IsBinded() const - { - return _resource != nullptr; - } - - /// - /// Checks if resource is missing - /// - /// True if resource is missing, otherwise false - FORCE_INLINE bool IsMissing() const - { - return _resource == nullptr; + return (T*)_resource; } public: @@ -148,19 +151,7 @@ public: /// Value to assign void Set(T* value) { - if (_resource != value) - { - // Remove reference from the old one - if (_resource) - _resource->Releasing.template Unbind(this); - - // Change referenced object - _resource = value; - - // Add reference to the new one - if (_resource) - _resource->Releasing.template Bind(this); - } + OnSet(value); } /// @@ -168,22 +159,7 @@ public: /// void Unlink() { - if (_resource) - { - // Remove reference from the old one - _resource->Releasing.template Unbind(this); - _resource = nullptr; - } - } - -private: - void onResourceUnload() - { - if (_resource) - { - _resource = nullptr; - OnUnload(this); - } + OnSet(nullptr); } }; diff --git a/Source/Engine/Graphics/Graphics.cpp b/Source/Engine/Graphics/Graphics.cpp index fa03fb7cd..f91c58cea 100644 --- a/Source/Engine/Graphics/Graphics.cpp +++ b/Source/Engine/Graphics/Graphics.cpp @@ -67,7 +67,8 @@ void GraphicsSettings::Apply() Graphics::AllowCSMBlending = AllowCSMBlending; Graphics::GlobalSDFQuality = GlobalSDFQuality; Graphics::GIQuality = GIQuality; - Graphics::PostProcessSettings = PostProcessSettings; + Graphics::PostProcessSettings = ::PostProcessSettings(); + Graphics::PostProcessSettings.BlendWith(PostProcessSettings, 1.0f); } void Graphics::DisposeDevice() diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h index 86fab4548..afa23a6cc 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.h +++ b/Source/Engine/Graphics/Materials/MaterialInfo.h @@ -3,7 +3,6 @@ #pragma once #include "../Enums.h" -#include "Engine/Core/Math/Math.h" /// /// Material domain type. Material domain defines the target usage of the material shader. @@ -86,7 +85,7 @@ API_ENUM() enum class MaterialBlendMode : byte API_ENUM() enum class MaterialShadingModel : byte { /// - /// The unlit material. Emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline. + /// The unlit material. The emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline. /// Unlit = 0, @@ -96,7 +95,7 @@ API_ENUM() enum class MaterialShadingModel : byte Lit = 1, /// - /// The subsurface material. Intended for materials like vax or skin that need light scattering to transport simulation through the object. + /// The subsurface material. Intended for materials like wax or skin that need light scattering to transport simulation through the object. /// Subsurface = 2, @@ -366,12 +365,12 @@ API_ENUM() enum class MaterialDecalBlendingMode : byte API_ENUM() enum class MaterialTransparentLightingMode : byte { /// - /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting component active. + /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting components active. /// Surface = 0, /// - /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only diffuse lighting term is active (no specular highlights). + /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only the diffuse lighting term is active (no specular highlights). /// SurfaceNonDirectional = 1, }; diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index 32caa2d01..d7359d905 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -90,6 +90,21 @@ public: /// Array BlendShapes; + /// + /// Global translation for this mesh to be at it's local origin. + /// + Vector3 OriginTranslation = Vector3::Zero; + + /// + /// Orientation for this mesh at it's local origin. + /// + Quaternion OriginOrientation = Quaternion::Identity; + + /// + /// Meshes scaling. + /// + Vector3 Scaling = Vector3::One; + public: /// /// Determines whether this instance has any mesh data. diff --git a/Source/Engine/Graphics/PostProcessSettings.h b/Source/Engine/Graphics/PostProcessSettings.h index f3440c4a5..1d695f353 100644 --- a/Source/Engine/Graphics/PostProcessSettings.h +++ b/Source/Engine/Graphics/PostProcessSettings.h @@ -5,6 +5,7 @@ #include "Engine/Core/Math/Vector3.h" #include "Engine/Core/Math/Vector4.h" #include "Engine/Content/AssetReference.h" +#include "Engine/Content/SoftAssetReference.h" #include "Engine/Core/ISerializable.h" #include "Engine/Content/Assets/Texture.h" #include "Engine/Content/Assets/MaterialBase.h" @@ -446,13 +447,13 @@ API_STRUCT() struct FLAXENGINE_API BloomSettings : ISerializable bool Enabled = true; /// - /// Bloom effect strength. Value 0 disabled is, while higher values increase the effect. + /// Bloom effect strength. Set a value of 0 to disabled it, while higher values increase the effect. /// API_FIELD(Attributes="Limit(0, 20.0f, 0.01f), EditorOrder(1), PostProcessSetting((int)BloomSettingsOverride.Intensity)") float Intensity = 1.0f; /// - /// Minimum pixel brightness value to start blowing. Values below the threshold are skipped. + /// Minimum pixel brightness value to start blooming. Values below this threshold are skipped. /// API_FIELD(Attributes="Limit(0, 15.0f, 0.01f), EditorOrder(2), PostProcessSetting((int)BloomSettingsOverride.Threshold)") float Threshold = 3.0f; @@ -850,7 +851,7 @@ API_STRUCT() struct FLAXENGINE_API ColorGradingSettings : ISerializable /// The Lookup Table (LUT) used to perform color correction. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(22), PostProcessSetting((int)ColorGradingSettingsOverride.LutTexture)") - AssetReference LutTexture; + SoftAssetReference LutTexture; /// /// The LUT blending weight (normalized to range 0-1). Default is 1.0. @@ -986,13 +987,13 @@ API_STRUCT() struct FLAXENGINE_API EyeAdaptationSettings : ISerializable float MaxBrightness = 2.0f; /// - /// The lower bound for the luminance histogram of the scene color. Value is in percent and limits the pixels below this brightness. Use values from range 60-80. Used only in AutomaticHistogram mode. + /// The lower bound for the luminance histogram of the scene color. This value is in percent and limits the pixels below this brightness. Use values in the range of 60-80. Used only in AutomaticHistogram mode. /// API_FIELD(Attributes="Limit(1, 99, 0.001f), EditorOrder(3), PostProcessSetting((int)EyeAdaptationSettingsOverride.HistogramLowPercent)") float HistogramLowPercent = 70.0f; /// - /// The upper bound for the luminance histogram of the scene color. Value is in percent and limits the pixels above this brightness. Use values from range 80-95. Used only in AutomaticHistogram mode. + /// The upper bound for the luminance histogram of the scene color. This value is in percent and limits the pixels above this brightness. Use values in the range of 80-95. Used only in AutomaticHistogram mode. /// API_FIELD(Attributes="Limit(1, 99, 0.001f), EditorOrder(3), PostProcessSetting((int)EyeAdaptationSettingsOverride.HistogramHighPercent)") float HistogramHighPercent = 98.0f; @@ -1090,13 +1091,13 @@ API_STRUCT() struct FLAXENGINE_API CameraArtifactsSettings : ISerializable Float3 VignetteColor = Float3(0, 0, 0.001f); /// - /// Controls shape of the vignette. Values near 0 produce rectangle shape. Higher values result in round shape. The default value is 0.125. + /// Controls the shape of the vignette. Values near 0 produce a rectangular shape. Higher values result in a rounder shape. The default value is 0.125. /// API_FIELD(Attributes="Limit(0.0001f, 2.0f, 0.001f), EditorOrder(2), PostProcessSetting((int)CameraArtifactsSettingsOverride.VignetteShapeFactor)") float VignetteShapeFactor = 0.125f; /// - /// Intensity of the grain filter. Value 0 hides it. The default value is 0.005. + /// Intensity of the grain filter. A value of 0 hides it. The default value is 0.005. /// API_FIELD(Attributes="Limit(0.0f, 2.0f, 0.005f), EditorOrder(3), PostProcessSetting((int)CameraArtifactsSettingsOverride.GrainAmount)") float GrainAmount = 0.006f; @@ -1108,19 +1109,19 @@ API_STRUCT() struct FLAXENGINE_API CameraArtifactsSettings : ISerializable float GrainParticleSize = 1.6f; /// - /// Speed of the grain particles animation. + /// Speed of the grain particle animation. /// API_FIELD(Attributes="Limit(0.0f, 10.0f, 0.01f), EditorOrder(5), PostProcessSetting((int)CameraArtifactsSettingsOverride.GrainSpeed)") float GrainSpeed = 1.0f; /// - /// Controls chromatic aberration effect strength. Value 0 hides it. + /// Controls the chromatic aberration effect strength. A value of 0 hides it. /// API_FIELD(Attributes="Limit(0.0f, 1.0f, 0.01f), EditorOrder(6), PostProcessSetting((int)CameraArtifactsSettingsOverride.ChromaticDistortion)") float ChromaticDistortion = 0.0f; /// - /// Screen tint color (alpha channel defines the blending factor). + /// Screen tint color (the alpha channel defines the blending factor). /// API_FIELD(Attributes="DefaultValue(typeof(Color), \"0,0,0,0\"), EditorOrder(7), PostProcessSetting((int)CameraArtifactsSettingsOverride.ScreenFadeColor)") Color ScreenFadeColor = Color::Transparent; @@ -1226,7 +1227,7 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable LensFlaresSettingsOverride OverrideFlags = Override::None; /// - /// Strength of the effect. Value 0 disabled it. + /// Strength of the effect. A value of 0 disables it. /// API_FIELD(Attributes="Limit(0, 10.0f, 0.01f), EditorOrder(0), PostProcessSetting((int)LensFlaresSettingsOverride.Intensity)") float Intensity = 1.0f; @@ -1277,10 +1278,10 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable /// Fullscreen lens dirt texture. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(8), PostProcessSetting((int)LensFlaresSettingsOverride.LensDirt)") - AssetReference LensDirt; + SoftAssetReference LensDirt; /// - /// Fullscreen lens dirt intensity parameter. Allows to tune dirt visibility. + /// Fullscreen lens dirt intensity parameter. Allows tuning dirt visibility. /// API_FIELD(Attributes="Limit(0, 100, 0.01f), EditorOrder(9), PostProcessSetting((int)LensFlaresSettingsOverride.LensDirtIntensity)") float LensDirtIntensity = 1.0f; @@ -1289,13 +1290,13 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable /// Custom lens color texture (1D) used for lens color spectrum. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(10), PostProcessSetting((int)LensFlaresSettingsOverride.LensColor)") - AssetReference LensColor; + SoftAssetReference LensColor; /// /// Custom lens star texture sampled by lens flares. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)LensFlaresSettingsOverride.LensStar)") - AssetReference LensStar; + SoftAssetReference LensStar; public: /// @@ -1418,13 +1419,13 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable DepthOfFieldSettingsOverride OverrideFlags = Override::None; /// - /// If checked, depth of field effect will be visible. + /// If checked, the depth of field effect will be visible. /// API_FIELD(Attributes="EditorOrder(0), PostProcessSetting((int)DepthOfFieldSettingsOverride.Enabled)") bool Enabled = false; /// - /// The blur intensity in the out-of-focus areas. Allows reducing blur amount by scaling down the Gaussian Blur radius. Normalized to range 0-1. + /// The blur intensity in the out-of-focus areas. Allows reducing the blur amount by scaling down the Gaussian Blur radius. Normalized to range 0-1. /// API_FIELD(Attributes="Limit(0, 1, 0.01f), EditorOrder(1), PostProcessSetting((int)DepthOfFieldSettingsOverride.BlurStrength)") float BlurStrength = 1.0f; @@ -1478,7 +1479,7 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable float BokehBrightness = 1.0f; /// - /// Defines bokeh shapes type. + /// Defines the type of the bokeh shapes. /// API_FIELD(Attributes="EditorOrder(10), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehShape)") BokehShapeType BokehShape = BokehShapeType::Octagon; @@ -1487,22 +1488,22 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable /// If BokehShape is set to Custom, then this texture will be used for the bokeh shapes. For best performance, use small, compressed, grayscale textures (for instance 32px). /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehShapeCustom)") - AssetReference BokehShapeCustom; + SoftAssetReference BokehShapeCustom; /// - /// The minimum pixel brightness to create bokeh. Pixels with lower brightness will be skipped. + /// The minimum pixel brightness to create the bokeh. Pixels with lower brightness will be skipped. /// API_FIELD(Attributes="Limit(0, 10000.0f, 0.01f), EditorOrder(12), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehBrightnessThreshold)") float BokehBrightnessThreshold = 3.0f; /// - /// Depth of Field bokeh shapes blur threshold. + /// Depth of Field bokeh shape blur threshold. /// API_FIELD(Attributes="Limit(0, 1.0f, 0.001f), EditorOrder(13), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehBlurThreshold)") float BokehBlurThreshold = 0.05f; /// - /// Controls bokeh shapes brightness falloff. Higher values reduce bokeh visibility. + /// Controls bokeh shape brightness falloff. Higher values reduce bokeh visibility. /// API_FIELD(Attributes="Limit(0, 2.0f, 0.001f), EditorOrder(14), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehFalloff)") float BokehFalloff = 0.5f; @@ -1574,25 +1575,25 @@ API_STRUCT() struct FLAXENGINE_API MotionBlurSettings : ISerializable MotionBlurSettingsOverride OverrideFlags = Override::None; /// - /// If checked, motion blur effect will be rendered. + /// If checked, the motion blur effect will be rendered. /// API_FIELD(Attributes="EditorOrder(0), PostProcessSetting((int)MotionBlurSettingsOverride.Enabled)") bool Enabled = true; /// - /// The blur effect strength. Value 0 disabled is, while higher values increase the effect. + /// The blur effect strength. A value of 0 disables it, while higher values increase the effect. /// API_FIELD(Attributes="Limit(0, 5, 0.01f), EditorOrder(1), PostProcessSetting((int)MotionBlurSettingsOverride.Scale)") float Scale = 1.0f; /// - /// The amount of sample points used during motion blur rendering. It affects quality and performance. + /// The amount of sample points used during motion blur rendering. It affects blur quality and performance. /// API_FIELD(Attributes="Limit(4, 32, 0.1f), EditorOrder(2), PostProcessSetting((int)MotionBlurSettingsOverride.SampleCount)") int32 SampleCount = 10; /// - /// The motion vectors texture resolution. Motion blur uses per-pixel motion vectors buffer that contains objects movement information. Use lower resolution to improve performance. + /// The motion vectors texture resolution. Motion blur uses a per-pixel motion vector buffer that contains an objects movement information. Use a lower resolution to improve performance. /// API_FIELD(Attributes="EditorOrder(3), PostProcessSetting((int)MotionBlurSettingsOverride.MotionVectorsResolution)") ResolutionMode MotionVectorsResolution = ResolutionMode::Half; @@ -1897,13 +1898,13 @@ API_STRUCT() struct FLAXENGINE_API AntiAliasingSettings : ISerializable float TAA_Sharpness = 0.0f; /// - /// The blend coefficient for stationary fragments. Controls the percentage of history sample blended into final color for fragments with minimal active motion. + /// The blend coefficient for stationary fragments. Controls the percentage of history samples blended into the final color for fragments with minimal active motion. /// API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(3), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_StationaryBlending), EditorDisplay(null, \"TAA Stationary Blending\")") float TAA_StationaryBlending = 0.95f; /// - /// The blending coefficient for moving fragments. Controls the percentage of history sample blended into the final color for fragments with significant active motion. + /// The blending coefficient for moving fragments. Controls the percentage of history samples blended into the final color for fragments with significant active motion. /// API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(4), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_MotionBlending), EditorDisplay(null, \"TAA Motion Blending\")") float TAA_MotionBlending = 0.7f; diff --git a/Source/Engine/Graphics/RenderTask.cpp b/Source/Engine/Graphics/RenderTask.cpp index 48e89f59e..8a10a0444 100644 --- a/Source/Engine/Graphics/RenderTask.cpp +++ b/Source/Engine/Graphics/RenderTask.cpp @@ -8,11 +8,12 @@ #include "Engine/Core/Collections/Sorting.h" #include "Engine/Debug/DebugLog.h" #include "Engine/Level/Level.h" +#include "Engine/Level/Scene/Scene.h" #include "Engine/Level/Actors/Camera.h" +#include "Engine/Level/Actors/PostFxVolume.h" #include "Engine/Renderer/Renderer.h" #include "Engine/Render2D/Render2D.h" #include "Engine/Engine/Engine.h" -#include "Engine/Level/Actors/PostFxVolume.h" #include "Engine/Profiler/Profiler.h" #include "Engine/Renderer/RenderList.h" #include "Engine/Threading/Threading.h" @@ -202,15 +203,21 @@ void SceneRenderTask::CollectPostFxVolumes(RenderContext& renderContext) { Level::CollectPostFxVolumes(renderContext); } - if (EnumHasAllFlags(ActorsSource , ActorsSources::CustomActors)) + if (EnumHasAllFlags(ActorsSource, ActorsSources::CustomActors)) { for (Actor* a : CustomActors) { auto* postFxVolume = dynamic_cast(a); if (postFxVolume && a->GetIsActive()) - { postFxVolume->Collect(renderContext); - } + } + } + if (EnumHasAllFlags(ActorsSource, ActorsSources::CustomScenes)) + { + for (Scene* scene : CustomScenes) + { + if (scene && scene->IsActiveInHierarchy()) + scene->Rendering.CollectPostFxVolumes(renderContext); } } } @@ -282,6 +289,14 @@ void SceneRenderTask::OnCollectDrawCalls(RenderContextBatch& renderContextBatch, ASSERT_LOW_LAYER(_customActorsScene); _customActorsScene->Draw(renderContextBatch, (SceneRendering::DrawCategory)category); } + if (EnumHasAllFlags(ActorsSource, ActorsSources::CustomScenes)) + { + for (Scene* scene : CustomScenes) + { + if (scene && scene->IsActiveInHierarchy()) + scene->Rendering.Draw(renderContextBatch, (SceneRendering::DrawCategory)category); + } + } if (EnumHasAllFlags(ActorsSource, ActorsSources::Scenes)) { Level::DrawActors(renderContextBatch, category); diff --git a/Source/Engine/Graphics/RenderTask.h b/Source/Engine/Graphics/RenderTask.h index 9123751bc..864d95214 100644 --- a/Source/Engine/Graphics/RenderTask.h +++ b/Source/Engine/Graphics/RenderTask.h @@ -21,6 +21,7 @@ class PostProcessEffect; struct RenderContext; class Camera; class Actor; +class Scene; /// /// Allows to perform custom rendering using graphics pipeline. @@ -174,6 +175,11 @@ API_ENUM(Attributes="Flags") enum class ActorsSources /// CustomActors = 2, + /// + /// The scenes from the custom collection. + /// + CustomScenes = 4, + /// /// The actors from the loaded scenes and custom collection. /// @@ -267,9 +273,14 @@ public: public: /// - /// The custom set of actors to render. + /// The custom set of actors to render. Used when ActorsSources::CustomActors flag is active. /// - Array CustomActors; + API_FIELD() Array CustomActors; + + /// + /// The custom set of scenes to render. Used when ActorsSources::CustomScenes flag is active. + /// + API_FIELD() Array CustomScenes; /// /// Adds the custom actor to the rendering. diff --git a/Source/Engine/Graphics/Shaders/GPUShader.cpp b/Source/Engine/Graphics/Shaders/GPUShader.cpp index e76ee9996..86d983fd8 100644 --- a/Source/Engine/Graphics/Shaders/GPUShader.cpp +++ b/Source/Engine/Graphics/Shaders/GPUShader.cpp @@ -87,7 +87,11 @@ bool GPUShader::Create(MemoryReadStream& stream) GPUShaderProgramInitializer initializer; #if !BUILD_RELEASE initializer.Owner = this; + const StringView name = GetName(); +#else + const StringView name; #endif + const bool hasCompute = GPUDevice::Instance->Limits.HasCompute; for (int32 i = 0; i < shadersCount; i++) { const ShaderStage type = static_cast(stream.ReadByte()); @@ -117,10 +121,15 @@ bool GPUShader::Create(MemoryReadStream& stream) stream.ReadBytes(&initializer.Bindings, sizeof(ShaderBindings)); // Create shader program + if (type == ShaderStage::Compute && !hasCompute) + { + LOG(Warning, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name); + continue; + } GPUShaderProgram* shader = CreateGPUShaderProgram(type, initializer, cache, cacheSize, stream); if (shader == nullptr) { - LOG(Error, "Failed to create {} Shader program '{}'.", ::ToString(type), String(initializer.Name)); + LOG(Error, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name); return true; } diff --git a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h index 09c45506f..31f6638dc 100644 --- a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h +++ b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h @@ -122,6 +122,19 @@ public: /// class GPUShaderProgramVS : public GPUShaderProgram { +public: + // Input element run-time data (see VertexShaderMeta::InputElement for compile-time data) + PACK_STRUCT(struct InputElement + { + byte Type; // VertexShaderMeta::InputType + byte Index; + byte Format; // PixelFormat + byte InputSlot; + uint32 AlignedByteOffset; // Fixed value or INPUT_LAYOUT_ELEMENT_ALIGN if auto + byte InputSlotClass; // INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA or INPUT_LAYOUT_ELEMENT_PER_INSTANCE_DATA + uint32 InstanceDataStepRate; // 0 if per-vertex + }); + public: /// /// Gets input layout description handle (platform dependent). diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 409125838..015386fff 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -22,10 +22,10 @@ TextureHeader::TextureHeader() TextureGroup = -1; } -TextureHeader::TextureHeader(TextureHeader_Deprecated& old) +TextureHeader::TextureHeader(const TextureHeader_Deprecated& old) { Platform::MemoryClear(this, sizeof(*this)); - Width = old.Width;; + Width = old.Width; Height = old.Height; MipLevels = old.MipLevels; Format = old.Format; @@ -49,7 +49,7 @@ StreamingTexture::StreamingTexture(ITextureOwner* parent, const String& name) , _texture(nullptr) , _isBlockCompressed(false) { - ASSERT(_owner != nullptr); + ASSERT(parent != nullptr); // Always have created texture object ASSERT(GPUDevice::Instance); @@ -63,7 +63,6 @@ StreamingTexture::~StreamingTexture() { UnloadTexture(); SAFE_DELETE(_texture); - ASSERT(_streamingTasks.Count() == 0); } Float2 StreamingTexture::Size() const @@ -134,11 +133,9 @@ bool StreamingTexture::Create(const TextureHeader& header) void StreamingTexture::UnloadTexture() { ScopeLock lock(_owner->GetOwnerLocker()); - - // Release + CancelStreamingTasks(); _texture->ReleaseGPU(); _header.MipLevels = 0; - ASSERT(_streamingTasks.Count() == 0); } @@ -329,11 +326,11 @@ public: , _dataLock(_streamingTexture->GetOwner()->LockData()) { _streamingTexture->_streamingTasks.Add(this); - _texture.OnUnload.Bind(this); + _texture.Released.Bind(this); } private: - void onResourceUnload2(GPUTextureReference* ref) + void OnResourceReleased2() { // Unlink texture if (_streamingTexture) diff --git a/Source/Engine/Graphics/Textures/TextureBase.cpp b/Source/Engine/Graphics/Textures/TextureBase.cpp index 538b15a4a..181955fce 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.cpp +++ b/Source/Engine/Graphics/Textures/TextureBase.cpp @@ -660,6 +660,7 @@ uint64 TextureBase::GetMemoryUsage() const void TextureBase::CancelStreaming() { + Asset::CancelStreaming(); _texture.CancelStreamingTasks(); } diff --git a/Source/Engine/Graphics/Textures/Types.h b/Source/Engine/Graphics/Textures/Types.h index 9593cbbc2..31e0b1afc 100644 --- a/Source/Engine/Graphics/Textures/Types.h +++ b/Source/Engine/Graphics/Textures/Types.h @@ -106,5 +106,5 @@ struct FLAXENGINE_API TextureHeader byte CustomData[10]; TextureHeader(); - TextureHeader(TextureHeader_Deprecated& old); + TextureHeader(const TextureHeader_Deprecated& old); }; diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h index 6205a628d..c0e0d8fd5 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h @@ -4,6 +4,7 @@ #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/GPUResource.h" +#include "Engine/Core/Collections/Dictionary.h" #include "../GPUDeviceDX.h" #include "../IncludeDirectXHeaders.h" diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp index b58684a4e..52192c634 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp @@ -15,32 +15,21 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const { case ShaderStage::Vertex: { - D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; - - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - - // Load Input Layout (it may be empty) + // Load Input Layout byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS); + D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); // Get semantic name const char* semanticName = nullptr; // TODO: maybe use enum+mapping ? - switch (Type) + switch (inputElement.Type) { case 1: semanticName = "POSITION"; @@ -70,7 +59,7 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const semanticName = "BLENDWEIGHT"; break; default: - LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type); + LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type); break; } @@ -78,12 +67,12 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const inputLayoutDesc[a] = { semanticName, - static_cast(Index), - static_cast(Format), - static_cast(InputSlot), - static_cast(AlignedByteOffset), - static_cast(InputSlotClass), - static_cast(InstanceDataStepRate) + static_cast(inputElement.Index), + static_cast(inputElement.Format), + static_cast(inputElement.InputSlot), + static_cast(inputElement.AlignedByteOffset), + static_cast(inputElement.InputSlotClass), + static_cast(inputElement.InstanceDataStepRate) }; } diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h index 2764ce235..27aba0c4b 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h @@ -7,6 +7,7 @@ #include "Engine/Graphics/GPUPipelineState.h" #include "GPUDeviceDX12.h" #include "Types.h" +#include "Engine/Core/Collections/Dictionary.h" #include "../IncludeDirectXHeaders.h" class GPUTextureViewDX12; diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp index e1e716853..07352b674 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp @@ -20,32 +20,21 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const { case ShaderStage::Vertex: { - D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; - - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - // Load Input Layout (it may be empty) byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS); + D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); // Get semantic name const char* semanticName = nullptr; // TODO: maybe use enum+mapping ? - switch (Type) + switch (inputElement.Type) { case 1: semanticName = "POSITION"; @@ -75,7 +64,7 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const semanticName = "BLENDWEIGHT"; break; default: - LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type); + LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type); break; } @@ -83,12 +72,12 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const inputLayout[a] = { semanticName, - static_cast(Index), - static_cast(Format), - static_cast(InputSlot), - static_cast(AlignedByteOffset), - static_cast(InputSlotClass), - static_cast(InstanceDataStepRate) + static_cast(inputElement.Index), + static_cast(inputElement.Format), + static_cast(inputElement.InputSlot), + static_cast(inputElement.AlignedByteOffset), + static_cast(inputElement.InputSlotClass), + static_cast(inputElement.InstanceDataStepRate) }; } diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp index 5e583d713..852ce5bad 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp @@ -14,9 +14,9 @@ #include "Engine/Graphics/PixelFormatExtensions.h" #if PLATFORM_DESKTOP -#define VULKAN_UNIFORM_RING_BUFFER_SIZE 24 * 1024 * 1024 +#define VULKAN_UNIFORM_RING_BUFFER_SIZE (24 * 1024 * 1024) #else -#define VULKAN_UNIFORM_RING_BUFFER_SIZE 8 * 1024 * 1024 +#define VULKAN_UNIFORM_RING_BUFFER_SIZE (8 * 1024 * 1024) #endif UniformBufferUploaderVulkan::UniformBufferUploaderVulkan(GPUDeviceVulkan* device) @@ -153,10 +153,6 @@ GPUShaderProgram* GPUShaderVulkan::CreateGPUShaderProgram(ShaderStage type, cons vertexBindingDescriptions[i].inputRate = VK_VERTEX_INPUT_RATE_VERTEX; } - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - // Load Input Layout (it may be empty) byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); @@ -167,32 +163,26 @@ GPUShaderProgram* GPUShaderVulkan::CreateGPUShaderProgram(ShaderStage type, cons for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); - const auto size = PixelFormatExtensions::SizeInBytes((PixelFormat)Format); - if (AlignedByteOffset != INPUT_LAYOUT_ELEMENT_ALIGN) - offset = AlignedByteOffset; + const auto size = PixelFormatExtensions::SizeInBytes((PixelFormat)inputElement.Format); + if (inputElement.AlignedByteOffset != INPUT_LAYOUT_ELEMENT_ALIGN) + offset = inputElement.AlignedByteOffset; - auto& vertexBindingDescription = vertexBindingDescriptions[InputSlot]; - vertexBindingDescription.binding = InputSlot; + auto& vertexBindingDescription = vertexBindingDescriptions[inputElement.InputSlot]; + vertexBindingDescription.binding = inputElement.InputSlot; vertexBindingDescription.stride = Math::Max(vertexBindingDescription.stride, (uint32_t)(offset + size)); - vertexBindingDescription.inputRate = InputSlotClass == INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; - ASSERT(InstanceDataStepRate == 0 || InstanceDataStepRate == 1); + vertexBindingDescription.inputRate = inputElement.InputSlotClass == INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; + ASSERT(inputElement.InstanceDataStepRate == 0 || inputElement.InstanceDataStepRate == 1); auto& vertexAttributeDescription = vertexAttributeDescriptions[a]; vertexAttributeDescription.location = a; - vertexAttributeDescription.binding = InputSlot; - vertexAttributeDescription.format = RenderToolsVulkan::ToVulkanFormat((PixelFormat)Format); + vertexAttributeDescription.binding = inputElement.InputSlot; + vertexAttributeDescription.format = RenderToolsVulkan::ToVulkanFormat((PixelFormat)inputElement.Format); vertexAttributeDescription.offset = offset; - bindingsCount = Math::Max(bindingsCount, (uint32)InputSlot + 1); + bindingsCount = Math::Max(bindingsCount, (uint32)inputElement.InputSlot + 1); offset += size; } diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 75e3c12d4..addb4861e 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1406,7 +1406,7 @@ Script* Actor::FindScript(const MClass* type) const CHECK_RETURN(type, nullptr); for (auto script : Scripts) { - if (script->GetClass()->IsSubClassOf(type)) + if (script->GetClass()->IsSubClassOf(type) || script->GetClass()->HasInterface(type)) return script; } for (auto child : Children) diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h index a1f2dc78f..0ce9a0dbc 100644 --- a/Source/Engine/Level/Actor.h +++ b/Source/Engine/Level/Actor.h @@ -33,12 +33,12 @@ API_CLASS(Abstract) class FLAXENGINE_API Actor : public SceneObject friend Prefab; friend PrefabInstanceData; protected: - int16 _isActive : 1; - int16 _isActiveInHierarchy : 1; - int16 _isPrefabRoot : 1; - int16 _isEnabled : 1; - int16 _drawNoCulling : 1; - int16 _drawCategory : 4; + uint16 _isActive : 1; + uint16 _isActiveInHierarchy : 1; + uint16 _isPrefabRoot : 1; + uint16 _isEnabled : 1; + uint16 _drawNoCulling : 1; + uint16 _drawCategory : 4; byte _layer; StaticFlags _staticFlags; Transform _localTransform; diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 88f7e6876..c1b5af398 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -172,6 +172,28 @@ void AnimatedModel::GetNodeTransformation(const StringView& nodeName, Matrix& no GetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace); } +void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace) +{ + if (GraphInstance.NodesPose.IsEmpty()) + const_cast(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return + CHECK(nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count()); + GraphInstance.NodesPose[nodeIndex] = nodeTransformation; + if (worldSpace) + { + Matrix world; + _transform.GetWorld(world); + Matrix invWorld; + Matrix::Invert(world, invWorld); + GraphInstance.NodesPose[nodeIndex] = GraphInstance.NodesPose[nodeIndex] * invWorld; + } + OnAnimationUpdated(); +} + +void AnimatedModel::SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace) +{ + SetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace); +} + int32 AnimatedModel::FindClosestNode(const Vector3& location, bool worldSpace) const { if (GraphInstance.NodesPose.IsEmpty()) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index 0c5c4d73b..029e17b62 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -229,6 +229,22 @@ public: /// True if convert matrices into world-space, otherwise returned values will be in local-space of the actor. API_FUNCTION() void GetNodeTransformation(const StringView& nodeName, API_PARAM(Out) Matrix& nodeTransformation, bool worldSpace = false) const; + /// + /// Sets the node final transformation. If multiple nodes are to be set within a frame, do not use set worldSpace to true, and do the conversion yourself to avoid recalculation of inv matrices. + /// + /// The index of the skinned model skeleton node. + /// The final node transformation matrix. + /// True if convert matrices from world-space, otherwise values will be in local-space of the actor. + API_FUNCTION() void SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace = false); + + /// + /// Sets the node final transformation. If multiple nodes are to be set within a frame, do not use set worldSpace to true, and do the conversion yourself to avoid recalculation of inv matrices. + /// + /// The name of the skinned model skeleton node. + /// The final node transformation matrix. + /// True if convert matrices from world-space, otherwise values will be in local-space of the actor. + API_FUNCTION() void SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace = false); + /// /// Finds the closest node to a given location. /// diff --git a/Source/Engine/Level/Actors/Camera.h b/Source/Engine/Level/Actors/Camera.h index 22d950dd4..c63a5dcf6 100644 --- a/Source/Engine/Level/Actors/Camera.h +++ b/Source/Engine/Level/Actors/Camera.h @@ -66,7 +66,7 @@ public: /// /// Gets the value indicating if camera should use perspective rendering mode, otherwise it will use orthographic projection. /// - API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Camera\"), Tooltip(\"Enables perspective projection mode, otherwise uses orthographic.\")") + API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(true), EditorDisplay(\"Camera\")") bool GetUsePerspective() const; /// @@ -77,7 +77,7 @@ public: /// /// Gets the camera's field of view (in degrees). /// - API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), Tooltip(\"Field of view angle in degrees.\")") + API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), VisibleIf(nameof(UsePerspective))") float GetFieldOfView() const; /// @@ -88,7 +88,7 @@ public: /// /// Gets the custom aspect ratio. 0 if not use custom value. /// - API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Custom aspect ratio to use. Set to 0 to disable.\")") + API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective))") float GetCustomAspectRatio() const; /// @@ -99,7 +99,7 @@ public: /// /// Gets camera's near plane distance. /// - API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\"), Tooltip(\"Near clipping plane distance\")") + API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\")") float GetNearPlane() const; /// @@ -110,7 +110,7 @@ public: /// /// Gets camera's far plane distance. /// - API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\"), Tooltip(\"Far clipping plane distance\")") + API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\")") float GetFarPlane() const; /// @@ -121,7 +121,7 @@ public: /// /// Gets the orthographic projection scale. /// - API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Orthographic projection scale\")") + API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective), true)") float GetOrthographicScale() const; /// diff --git a/Source/Engine/Level/Actors/DirectionalLight.h b/Source/Engine/Level/Actors/DirectionalLight.h index 3f57e74bb..d5f31324a 100644 --- a/Source/Engine/Level/Actors/DirectionalLight.h +++ b/Source/Engine/Level/Actors/DirectionalLight.h @@ -13,7 +13,7 @@ class FLAXENGINE_API DirectionalLight : public LightWithShadow DECLARE_SCENE_OBJECT(DirectionalLight); public: /// - /// The number of cascades used for slicing the range of depth covered by the light. Values are 1, 2 or 4 cascades; a typical scene uses 4 cascades. + /// The number of cascades used for slicing the range of depth covered by the light during shadow rendering. Values are 1, 2 or 4 cascades; a typical scene uses 4 cascades. /// API_FIELD(Attributes="EditorOrder(65), DefaultValue(4), Limit(1, 4), EditorDisplay(\"Shadow\")") int32 CascadeCount = 4; diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.h b/Source/Engine/Level/Actors/ExponentialHeightFog.h index b0e5751d9..7b400fe4e 100644 --- a/Source/Engine/Level/Actors/ExponentialHeightFog.h +++ b/Source/Engine/Level/Actors/ExponentialHeightFog.h @@ -29,7 +29,7 @@ public: float FogDensity = 0.02f; /// - /// The fog height density factor that controls how the density increases as height decreases. The smaller values produce more visible transition larger. + /// The fog height density factor that controls how the density increases as height decreases. Smaller values produce a more visible transition layer. /// API_FIELD(Attributes="EditorOrder(20), DefaultValue(0.2f), Limit(0.0001f, 10.0f, 0.001f), EditorDisplay(\"Exponential Height Fog\")") float FogHeightFalloff = 0.2f; @@ -55,7 +55,7 @@ public: float StartDistance = 0.0f; /// - /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in. + /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in. Setting this value to 0 disables it. /// API_FIELD(Attributes="EditorOrder(60), DefaultValue(0.0f), Limit(0), EditorDisplay(\"Exponential Height Fog\")") float FogCutoffDistance = 0.0f; @@ -111,7 +111,7 @@ public: Color VolumetricFogAlbedo = Color::White; /// - /// Light emitted by height fog. This is a density so more light is emitted the further you are looking through the fog. + /// Light emitted by height fog. This is a density value so more light is emitted the further you are looking through the fog. /// In most cases using a Skylight is a better choice, however, it may be useful in certain scenarios. /// API_FIELD(Attributes="EditorOrder(330), DefaultValue(typeof(Color), \"0,0,0,1\"), EditorDisplay(\"Volumetric Fog\", \"Emissive\")") diff --git a/Source/Engine/Level/Actors/Light.h b/Source/Engine/Level/Actors/Light.h index 86ca59ef5..efb137b60 100644 --- a/Source/Engine/Level/Actors/Light.h +++ b/Source/Engine/Level/Actors/Light.h @@ -29,7 +29,7 @@ public: float Brightness = 3.14f; /// - /// Controls light visibility range. The distance at which the light be completely faded. Use value 0 to always draw light. + /// Controls light visibility range. The distance at which the light becomes completely faded. Use a value of 0 to always draw light. /// API_FIELD(Attributes="EditorOrder(35), Limit(0, float.MaxValue, 10.0f), EditorDisplay(\"Light\")") float ViewDistance = 0.0f; @@ -87,19 +87,19 @@ public: float MinRoughness = 0.04f; /// - /// The light shadows casting distance from view. + /// Shadows casting distance from view. /// API_FIELD(Attributes="EditorOrder(80), EditorDisplay(\"Shadow\", \"Distance\"), Limit(0, 1000000)") float ShadowsDistance = 5000.0f; /// - /// The light shadows fade off distance + /// Shadows fade off distance. /// API_FIELD(Attributes="EditorOrder(90), EditorDisplay(\"Shadow\", \"Fade Distance\"), Limit(0.0f, 10000.0f, 0.1f)") float ShadowsFadeDistance = 500.0f; /// - /// The light shadows edges sharpness + /// TheShadows edges sharpness. /// API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Shadow\", \"Sharpness\"), Limit(1.0f, 10.0f, 0.001f)") float ShadowsSharpness = 1.0f; @@ -129,7 +129,7 @@ public: float ContactShadowsLength = 0.0f; /// - /// Shadows casting mode by this visual element + /// Describes how a visual element casts shadows. /// API_FIELD(Attributes="EditorOrder(60), EditorDisplay(\"Shadow\", \"Mode\")") ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All; diff --git a/Source/Engine/Level/Actors/Sky.h b/Source/Engine/Level/Actors/Sky.h index 81aca3653..6d0935f78 100644 --- a/Source/Engine/Level/Actors/Sky.h +++ b/Source/Engine/Level/Actors/Sky.h @@ -31,19 +31,19 @@ public: /// /// Directional light that is used to simulate the sun. /// - API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Sun\")") + API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Sky\")") ScriptingObjectReference SunLight; /// /// The sun disc scale. /// - API_FIELD(Attributes="EditorOrder(20), DefaultValue(2.0f), EditorDisplay(\"Sun\"), Limit(0, 100, 0.01f)") + API_FIELD(Attributes="EditorOrder(20), DefaultValue(2.0f), EditorDisplay(\"Sky\"), Limit(0, 100, 0.01f)") float SunDiscScale = 2.0f; /// /// The sun power. /// - API_FIELD(Attributes="EditorOrder(30), DefaultValue(8.0f), EditorDisplay(\"Sun\"), Limit(0, 1000, 0.01f)") + API_FIELD(Attributes="EditorOrder(30), DefaultValue(8.0f), EditorDisplay(\"Sky\"), Limit(0, 1000, 0.01f)") float SunPower = 8.0f; private: diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index 194dbd9a9..dfe7c2be8 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -149,9 +149,14 @@ float Spline::GetSplineDuration() const float Spline::GetSplineLength() const { float sum = 0.0f; - const int32 slices = 20; - const float step = 1.0f / (float)slices; + constexpr int32 slices = 20; + constexpr float step = 1.0f / (float)slices; Vector3 prevPoint = Vector3::Zero; + if (Curve.GetKeyframes().Count() != 0) + { + const auto& a = Curve[0]; + prevPoint = a.Value.Translation * _transform.Scale; + } for (int32 i = 1; i < Curve.GetKeyframes().Count(); i++) { const auto& a = Curve[i - 1]; @@ -176,6 +181,37 @@ float Spline::GetSplineLength() const return Math::Sqrt(sum); } +float Spline::GetSplineSegmentLength(int32 index) const +{ + if (index == 0) + return 0.0f; + CHECK_RETURN(index > 0 && index < GetSplinePointsCount(), 0.0f); + float sum = 0.0f; + constexpr int32 slices = 20; + constexpr float step = 1.0f / (float)slices; + const auto& a = Curve[index - 1]; + const auto& b = Curve[index]; + Vector3 startPoint = a.Value.Translation * _transform.Scale; + { + const float length = Math::Abs(b.Time - a.Time); + Vector3 leftTangent, rightTangent; + AnimationUtils::GetTangent(a.Value.Translation, a.TangentOut.Translation, length, leftTangent); + AnimationUtils::GetTangent(b.Value.Translation, b.TangentIn.Translation, length, rightTangent); + + // TODO: implement sth more analytical than brute-force solution + for (int32 slice = 0; slice < slices; slice++) + { + const float t = (float)slice * step; + Vector3 pos; + AnimationUtils::Bezier(a.Value.Translation, leftTangent, rightTangent, b.Value.Translation, t, pos); + pos *= _transform.Scale; + sum += (float)Vector3::DistanceSquared(pos, startPoint); + startPoint = pos; + } + } + return Math::Sqrt(sum); +} + float Spline::GetSplineTime(int32 index) const { CHECK_RETURN(index >= 0 && index < GetSplinePointsCount(), 0.0f) diff --git a/Source/Engine/Level/Actors/Spline.h b/Source/Engine/Level/Actors/Spline.h index cd022890b..4a81ee186 100644 --- a/Source/Engine/Level/Actors/Spline.h +++ b/Source/Engine/Level/Actors/Spline.h @@ -167,6 +167,13 @@ public: /// API_PROPERTY() float GetSplineLength() const; + /// + /// Gets the length of the spline segment (distance between pair of two points). + /// + /// The index of the segment end index. Zero-based, smaller than GetSplinePointsCount(). + /// The spline segment length. + API_FUNCTION() float GetSplineSegmentLength(int32 index) const; + /// /// Gets the time of the spline keyframe. /// diff --git a/Source/Engine/Level/Actors/SpotLight.h b/Source/Engine/Level/Actors/SpotLight.h index 37735692a..2c16d38e4 100644 --- a/Source/Engine/Level/Actors/SpotLight.h +++ b/Source/Engine/Level/Actors/SpotLight.h @@ -24,7 +24,7 @@ private: public: /// - /// Light source bulb radius + /// Light source bulb radius. /// API_FIELD(Attributes="EditorOrder(2), DefaultValue(0.0f), EditorDisplay(\"Light\"), Limit(0, 1000, 0.01f)") float SourceRadius = 0.0f; @@ -42,54 +42,51 @@ public: float FallOffExponent = 8.0f; /// - /// IES texture (light profiles from real world measured data) + /// IES texture (light profiles from real world measured data). /// API_FIELD(Attributes="EditorOrder(211), DefaultValue(null), EditorDisplay(\"IES Profile\", \"IES Texture\")") AssetReference IESTexture; /// - /// Enable/disable using light brightness from IES profile + /// Enable/disable using light brightness from IES profile. /// API_FIELD(Attributes="EditorOrder(212), DefaultValue(false), EditorDisplay(\"IES Profile\", \"Use IES Brightness\")") bool UseIESBrightness = false; /// - /// Global scale for IES brightness contribution + /// Global scale for IES brightness contribution. /// API_FIELD(Attributes="EditorOrder(213), DefaultValue(1.0f), Limit(0, 10000, 0.01f), EditorDisplay(\"IES Profile\", \"Brightness Scale\")") float IESBrightnessScale = 1.0f; public: /// - /// Computes light brightness value + /// Computes light brightness value. /// - /// Brightness float ComputeBrightness() const; /// - /// Gets scaled light radius + /// Gets scaled light radius. /// float GetScaledRadius() const; /// - /// Gets light radius + /// Gets light radius. /// - API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Tooltip(\"Light radius\"), Limit(0, 10000, 0.1f)") + API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Limit(0, 10000, 0.1f)") FORCE_INLINE float GetRadius() const { return _radius; } /// - /// Sets light radius + /// Sets light radius. /// - /// New radius API_PROPERTY() void SetRadius(float value); /// - /// Gets the spot light's outer cone angle (in degrees) + /// Gets the spot light's outer cone angle (in degrees). /// - /// Outer angle (in degrees) API_PROPERTY(Attributes="EditorOrder(22), DefaultValue(43.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)") FORCE_INLINE float GetOuterConeAngle() const { @@ -97,15 +94,13 @@ public: } /// - /// Sets the spot light's outer cone angle (in degrees) + /// Sets the spot light's outer cone angle (in degrees). /// - /// Value to assign API_PROPERTY() void SetOuterConeAngle(float value); /// - /// Sets the spot light's inner cone angle (in degrees) + /// Sets the spot light's inner cone angle (in degrees). /// - /// Inner angle (in degrees) API_PROPERTY(Attributes="EditorOrder(21), DefaultValue(10.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)") FORCE_INLINE float GetInnerConeAngle() const { @@ -113,9 +108,8 @@ public: } /// - /// Sets the spot light's inner cone angle (in degrees) + /// Sets the spot light's inner cone angle (in degrees). /// - /// Value to assign API_PROPERTY() void SetInnerConeAngle(float value); private: diff --git a/Source/Engine/Level/Components/MissingScript.h b/Source/Engine/Level/Components/MissingScript.h index bfb73498f..7b351bd82 100644 --- a/Source/Engine/Level/Components/MissingScript.h +++ b/Source/Engine/Level/Components/MissingScript.h @@ -42,24 +42,7 @@ public: /// /// Field for assigning new script to transfer data to. /// - API_PROPERTY() void SetReferenceScript(const ScriptingObjectReference