Merge remote-tracking branch 'upstream/master' into FocusSelectedVJControls

This commit is contained in:
Saas
2026-02-09 15:52:27 +01:00
908 changed files with 72117 additions and 9506 deletions

View File

@@ -174,7 +174,9 @@ void EditorAnalytics::StartSession()
// Bind events
GameCooker::OnEvent.Bind<RegisterGameCookingStart>();
ShadowsOfMordor::Builder::Instance()->OnBuildStarted.Bind<RegisterLightmapsBuildingStart>();
#if LOG_ENABLE
Log::Logger::OnError.Bind<RegisterError>();
#endif
}
void EditorAnalytics::EndSession()
@@ -187,7 +189,9 @@ void EditorAnalytics::EndSession()
// Unbind events
GameCooker::OnEvent.Unbind<RegisterGameCookingStart>();
ShadowsOfMordor::Builder::Instance()->OnBuildStarted.Unbind<RegisterLightmapsBuildingStart>();
#if LOG_ENABLE
Log::Logger::OnError.Unbind<RegisterError>();
#endif
// End session
{

View File

@@ -117,7 +117,8 @@ namespace FlaxEditor.Content.Create
private static bool IsValid(Type type)
{
return (type.IsPublic || type.IsNestedPublic) && !type.IsAbstract && !type.IsGenericType;
var controlTypes = Editor.Instance.CodeEditing.Controls.Get();
return (type.IsPublic || type.IsNestedPublic) && !type.IsAbstract && !type.IsGenericType && controlTypes.Contains(new ScriptType(type));
}
}

View File

@@ -281,6 +281,13 @@ namespace FlaxEditor.Content
private void CacheData()
{
if (!_asset)
{
_parameters = Utils.GetEmptyArray<ScriptMemberInfo>();
_methods = Utils.GetEmptyArray<ScriptMemberInfo>();
_attributes = Utils.GetEmptyArray<Attribute>();
return;
}
if (_parameters != null)
return;
if (_asset.WaitForLoaded())
@@ -344,13 +351,13 @@ namespace FlaxEditor.Content
}
/// <inheritdoc />
public string Name => Path.GetFileNameWithoutExtension(_asset.Path);
public string Name => _asset ? Path.GetFileNameWithoutExtension(_asset.Path) : null;
/// <inheritdoc />
public string Namespace => string.Empty;
/// <inheritdoc />
public string TypeName => JsonSerializer.GetStringID(_asset.ID);
public string TypeName => _asset ? JsonSerializer.GetStringID(_asset.ID) : null;
/// <inheritdoc />
public bool IsPublic => true;

View File

@@ -130,6 +130,11 @@ namespace FlaxEditor.Content
eyeAdaptation.Mode = EyeAdaptationMode.None;
eyeAdaptation.OverrideFlags |= EyeAdaptationSettingsOverride.Mode;
preview.PostFxVolume.EyeAdaptation = eyeAdaptation;
var antiAliasing = preview.PostFxVolume.AntiAliasing;
antiAliasing.Mode = AntialiasingMode.FastApproximateAntialiasing;
antiAliasing.OverrideFlags |= AntiAliasingSettingsOverride.Mode;
preview.PostFxVolume.AntiAliasing = antiAliasing;
}
}
}

View File

@@ -20,13 +20,6 @@ class PlatformTools;
#define GAME_BUILD_DOTNET_RUNTIME_MAX_VER 9
#endif
#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=" MACRO_TO_STR(GAME_BUILD_DOTNET_RUNTIME_MIN_VER))
#else
#define GAME_BUILD_DOTNET_VER TEXT("")
#endif
/// <summary>
/// Game building options. Used as flags.
/// </summary>
@@ -374,6 +367,8 @@ public:
/// </summary>
void GetBuildPlatformName(const Char*& platform, const Char*& architecture) const;
String GetDotnetCommandArg() const;
public:
/// <summary>

View File

@@ -30,6 +30,7 @@
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Content/JsonAsset.h"
#include "Engine/Content/AssetReference.h"
#include "Engine/Profiler/ProfilerMemory.h"
#if PLATFORM_TOOLS_WINDOWS
#include "Platform/Windows/WindowsPlatformTools.h"
#include "Engine/Platform/Windows/WindowsPlatformSettings.h"
@@ -311,6 +312,14 @@ void CookingData::GetBuildPlatformName(const Char*& platform, const Char*& archi
}
}
String CookingData::GetDotnetCommandArg() const
{
int32 version = Tools->GetDotnetVersion();
if (version == 0)
return String::Empty;
return String::Format(TEXT("-dotnet={}"), version);
}
void CookingData::StepProgress(const String& info, const float stepProgress) const
{
const float singleStepProgress = 1.0f / (StepsCount + 1);
@@ -380,6 +389,7 @@ bool GameCooker::IsCancelRequested()
PlatformTools* GameCooker::GetTools(BuildPlatform platform)
{
PROFILE_MEM(Editor);
PlatformTools* result = nullptr;
if (!Tools.TryGet(platform, result))
{
@@ -471,6 +481,7 @@ bool GameCooker::Build(BuildPlatform platform, BuildConfiguration configuration,
LOG(Error, "Build platform {0} is not supported.", ::ToString(platform));
return true;
}
PROFILE_MEM(Editor);
// Setup
CancelFlag = 0;
@@ -624,6 +635,7 @@ void GameCookerImpl::ReportProgress(const String& info, float totalProgress)
void GameCookerImpl::OnCollectAssets(HashSet<Guid>& assets)
{
PROFILE_MEM(Editor);
if (Internal_OnCollectAssets == nullptr)
{
auto c = GameCooker::GetStaticClass();
@@ -651,6 +663,7 @@ void GameCookerImpl::OnCollectAssets(HashSet<Guid>& assets)
bool GameCookerImpl::Build()
{
PROFILE_MEM(Editor);
CookingData& data = *Data;
LOG(Info, "Starting Game Cooker...");
LOG(Info, "Platform: {0}, Configuration: {2}, Options: {1}", ::ToString(data.Platform), (int32)data.Options, ::ToString(data.Configuration));
@@ -670,8 +683,7 @@ bool GameCookerImpl::Build()
MCore::Thread::Attach();
// Build Started
if (!EnumHasAnyFlags(data.Options, BuildOptions::NoCook))
// Build start
{
CallEvent(GameCooker::EventType::BuildStarted);
data.Tools->OnBuildStarted(data);
@@ -744,8 +756,8 @@ bool GameCookerImpl::Build()
}
IsRunning = false;
CancelFlag = 0;
if (!EnumHasAnyFlags(data.Options, BuildOptions::NoCook))
{
// Build end
for (int32 stepIndex = 0; stepIndex < Steps.Count(); stepIndex++)
Steps[stepIndex]->OnBuildEnded(data, failed);
data.Tools->OnBuildEnded(data, failed);
@@ -778,6 +790,8 @@ int32 GameCookerImpl::ThreadFunction()
bool GameCookerService::Init()
{
PROFILE_MEM(Editor);
auto editorAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly;
editorAssembly->Unloading.Bind(OnEditorAssemblyUnloading);
GameCooker::OnCollectAssets.Bind(OnCollectAssets);
@@ -789,6 +803,7 @@ void GameCookerService::Update()
{
if (IsRunning)
{
PROFILE_MEM(Editor);
ScopeLock lock(ProgressLocker);
if (ProgressMsg.HasChars())

View File

@@ -15,26 +15,32 @@
#include "Editor/ProjectInfo.h"
#include "Editor/Utilities/EditorUtilities.h"
GDKPlatformTools::GDKPlatformTools()
String GetGDK()
{
// Find GDK
Platform::GetEnvironmentVariable(TEXT("GameDKLatest"), _gdkPath);
if (_gdkPath.IsEmpty() || !FileSystem::DirectoryExists(_gdkPath))
String gdk;
Platform::GetEnvironmentVariable(TEXT("GameDKLatest"), gdk);
if (gdk.IsEmpty() || !FileSystem::DirectoryExists(gdk))
{
_gdkPath.Clear();
Platform::GetEnvironmentVariable(TEXT("GRDKLatest"), _gdkPath);
if (_gdkPath.IsEmpty() || !FileSystem::DirectoryExists(_gdkPath))
gdk.Clear();
Platform::GetEnvironmentVariable(TEXT("GRDKLatest"), gdk);
if (gdk.IsEmpty() || !FileSystem::DirectoryExists(gdk))
{
_gdkPath.Clear();
gdk.Clear();
}
else
{
if (_gdkPath.EndsWith(TEXT("GRDK\\")))
_gdkPath.Remove(_gdkPath.Length() - 6);
else if (_gdkPath.EndsWith(TEXT("GRDK")))
_gdkPath.Remove(_gdkPath.Length() - 5);
if (gdk.EndsWith(TEXT("GRDK\\")))
gdk.Remove(gdk.Length() - 6);
else if (gdk.EndsWith(TEXT("GRDK")))
gdk.Remove(gdk.Length() - 5);
}
}
return gdk;
}
GDKPlatformTools::GDKPlatformTools()
{
_gdkPath = GetGDK();
}
DotNetAOTModes GDKPlatformTools::UseAOT() const
@@ -121,7 +127,7 @@ bool GDKPlatformTools::OnPostProcess(CookingData& data, GDKPlatformSettings* pla
validName.Add('\0');
sb.Append(TEXT("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"));
sb.Append(TEXT("<Game configVersion=\"0\">\n"));
sb.Append(TEXT("<Game configVersion=\"1\">\n"));
sb.AppendFormat(TEXT(" <Identity Name=\"{0}\" Publisher=\"{1}\" Version=\"{2}\"/>\n"),
validName.Get(),
platformSettings->PublisherName.HasChars() ? platformSettings->PublisherName : TEXT("CN=") + gameSettings->CompanyName,
@@ -195,4 +201,9 @@ bool GDKPlatformTools::OnPostProcess(CookingData& data, GDKPlatformSettings* pla
return false;
}
int32 GDKPlatformTools::GetDotnetVersion() const
{
return GAME_BUILD_DOTNET_RUNTIME_MIN_VER;
}
#endif

View File

@@ -26,6 +26,7 @@ public:
public:
// [PlatformTools]
int32 GetDotnetVersion() const override;
DotNetAOTModes UseAOT() const override;
bool OnDeployBinaries(CookingData& data) override;
};

View File

@@ -186,7 +186,7 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
ADD_ENTRY("CFBundlePackageType", "APPL");
ADD_ENTRY("NSPrincipalClass", "NSApplication");
ADD_ENTRY("LSApplicationCategoryType", "public.app-category.games");
ADD_ENTRY("LSMinimumSystemVersion", "10.15");
ADD_ENTRY("LSMinimumSystemVersion", "13");
ADD_ENTRY("CFBundleIconFile", "icon.icns");
ADD_ENTRY_STR("CFBundleExecutable", executableName);
ADD_ENTRY_STR("CFBundleIdentifier", appIdentifier);
@@ -231,6 +231,8 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
LOG(Info, "Building app package...");
{
const String dmgPath = data.OriginalOutputPath / appName + TEXT(".dmg");
if (FileSystem::FileExists(dmgPath))
FileSystem::DeleteFile(dmgPath);
CreateProcessSettings procSettings;
procSettings.HiddenWindow = true;
procSettings.WorkingDirectory = data.OriginalOutputPath;

View File

@@ -528,6 +528,9 @@ bool WindowsPlatformTools::OnDeployBinaries(CookingData& data)
void WindowsPlatformTools::OnBuildStarted(CookingData& data)
{
if (EnumHasAllFlags(data.Options, BuildOptions::NoCook))
return;
// Remove old executable
Array<String> files;
FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*.exe"), DirectorySearchOption::TopDirectoryOnly);

View File

@@ -70,6 +70,20 @@ public:
/// </summary>
virtual ArchitectureType GetArchitecture() const = 0;
/// <summary>
/// Gets the .Net version to use for the cooked game.
/// </summary>
virtual int32 GetDotnetVersion() const
{
#if OFFICIAL_BUILD
// Use the fixed .NET SDK version in packaged builds for compatibility (FlaxGame is precompiled with it)
return GAME_BUILD_DOTNET_RUNTIME_MIN_VER;
#else
// Use the highest version found on a system (Flax.Build will decide)
return 0;
#endif
}
/// <summary>
/// Gets the value indicating whenever platform requires AOT (needs C# assemblies to be precompiled).
/// </summary>

View File

@@ -10,9 +10,10 @@
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Editor/Cooker/PlatformTools.h"
#include "Engine/Engine/Globals.h"
#include "Editor/Editor.h"
#include "Editor/ProjectInfo.h"
#include "Engine/Engine/Globals.h"
#include "Editor/Utilities/EditorUtilities.h"
#if PLATFORM_MAC
#include <sys/stat.h>
#endif
@@ -127,7 +128,7 @@ bool CompileScriptsStep::DeployBinaries(CookingData& data, const String& path, c
const String dst = dstPath / StringUtils::GetFileName(file);
if (dst == file)
continue;
if (FileSystem::CopyFile(dst, file))
if (EditorUtilities::CopyFileIfNewer(dst, file))
{
data.Error(String::Format(TEXT("Failed to copy file from {0} to {1}."), file, dst));
return true;
@@ -189,7 +190,7 @@ bool CompileScriptsStep::Perform(CookingData& data)
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} {6}"),
target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT()), GAME_BUILD_DOTNET_VER);
target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT()), data.GetDotnetCommandArg());
#if PLATFORM_WINDOWS
if (data.Platform == BuildPlatform::LinuxX64)
#elif PLATFORM_LINUX

View File

@@ -36,6 +36,7 @@
#include "Engine/Engine/Base/GameBase.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Scripting/Enums.h"
#if PLATFORM_TOOLS_WINDOWS
@@ -525,6 +526,7 @@ bool ProcessShaderBase(CookAssetsStep::AssetCookData& data, ShaderAssetBase* ass
#if PLATFORM_TOOLS_XBOX_SCARLETT
case BuildPlatform::XboxScarlett:
{
options.Platform = PlatformType::XboxScarlett;
const char* platformDefineName = "PLATFORM_XBOX_SCARLETT";
COMPILE_PROFILE(DirectX_SM6, SHADER_FILE_CHUNK_INTERNAL_D3D_SM6_CACHE);
break;
@@ -1366,7 +1368,10 @@ bool CookAssetsStep::Perform(CookingData& data)
{
typeName = e.TypeName;
}
LOG(Info, "{0}: {1:>4} assets of total size {2}", typeName, e.Count, Utilities::BytesToText(e.ContentSize));
if (e.Count == 1)
LOG(Info, "{0}: 1 asset of total size {1}", typeName, Utilities::BytesToText(e.ContentSize));
else
LOG(Info, "{0}: {1:>4} assets of total size {2}", typeName, e.Count, Utilities::BytesToText(e.ContentSize));
}
LOG(Info, "");
}

View File

@@ -88,7 +88,7 @@ bool DeployDataStep::Perform(CookingData& data)
{
// Ask Flax.Build to provide .NET SDK location for the current platform
String sdks;
bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), GAME_BUILD_DOTNET_VER), data.CacheDirectory);
bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), data.GetDotnetCommandArg()), data.CacheDirectory);
failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks);
int32 idx = sdks.Find(TEXT("DotNetSdk, "), StringSearchCase::CaseSensitive);
if (idx != -1)
@@ -200,7 +200,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, GAME_BUILD_DOTNET_VER);
String args = String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printDotNetRuntime -platform={} -arch={} {}"), platformName, archName, data.GetDotnetCommandArg());
bool failed = ScriptsBuilder::RunBuildTool(args, data.CacheDirectory);
failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks);
Array<String> parts;
@@ -244,10 +244,13 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (version.IsEmpty())
{
int32 minVer = GAME_BUILD_DOTNET_RUNTIME_MIN_VER, maxVer = GAME_BUILD_DOTNET_RUNTIME_MAX_VER;
if (srcDotnetFromEngine)
{
// Detect version from runtime files inside Engine Platform folder
for (int32 i = GAME_BUILD_DOTNET_RUNTIME_MAX_VER; i >= GAME_BUILD_DOTNET_RUNTIME_MIN_VER; i--)
if (data.Tools->GetDotnetVersion() != 0)
minVer = maxVer = data.Tools->GetDotnetVersion();
for (int32 i = maxVer; i >= minVer; i--)
{
// Check runtime files inside Engine Platform folder
String testPath1 = srcDotnet / String::Format(TEXT("lib/net{}.0"), i);
@@ -262,7 +265,7 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (version.IsEmpty())
{
data.Error(String::Format(TEXT("Failed to find supported .NET {} version for the current host platform."), GAME_BUILD_DOTNET_RUNTIME_MIN_VER));
data.Error(String::Format(TEXT("Failed to find supported .NET {} version (min {}) for {} platform."), maxVer, minVer, platformName));
return true;
}
}
@@ -364,7 +367,7 @@ bool DeployDataStep::Perform(CookingData& data)
const String logFile = data.CacheDirectory / TEXT("StripDotnetLibs.txt");
String args = String::Format(
TEXT("-log -logfile=\"{}\" -runDotNetClassLibStripping -mutex -binaries=\"{}\" {}"),
logFile, data.DataOutputPath, GAME_BUILD_DOTNET_VER);
logFile, data.DataOutputPath, data.GetDotnetCommandArg());
for (const String& define : data.CustomDefines)
{
args += TEXT(" -D");

View File

@@ -12,7 +12,7 @@
void PrecompileAssembliesStep::OnBuildStarted(CookingData& data)
{
const DotNetAOTModes aotMode = data.Tools->UseAOT();
if (aotMode == DotNetAOTModes::None)
if (aotMode == DotNetAOTModes::None || EnumHasAllFlags(data.Options, BuildOptions::NoCook))
return;
const auto& buildSettings = *BuildSettings::Get();
@@ -59,6 +59,7 @@ bool PrecompileAssembliesStep::Perform(CookingData& data)
data.StepProgress(infoMsg, 0);
// Override Newtonsoft.Json with AOT-version (one that doesn't use System.Reflection.Emit)
// TODO: remove it since EngineModule does properly reference AOT lib now
EditorUtilities::CopyFileIfNewer(data.ManagedCodeOutputPath / TEXT("Newtonsoft.Json.dll"), Globals::StartupFolder / TEXT("Source/Platforms/DotNet/AOT/Newtonsoft.Json.dll"));
FileSystem::DeleteFile(data.ManagedCodeOutputPath / TEXT("Newtonsoft.Json.xml"));
FileSystem::DeleteFile(data.ManagedCodeOutputPath / TEXT("Newtonsoft.Json.pdb"));
@@ -69,7 +70,7 @@ bool PrecompileAssembliesStep::Perform(CookingData& data)
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, GAME_BUILD_DOTNET_VER);
logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath, data.GetDotnetCommandArg());
if (!buildSettings.SkipUnusedDotnetLibsPackaging)
args += TEXT(" -skipUnusedDotnetLibs=false"); // Run AOT on whole class library (not just used libs)
for (const String& define : data.CustomDefines)

View File

@@ -6,6 +6,8 @@
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Types/Stopwatch.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/BinaryModule.h"
@@ -69,6 +71,7 @@ MTypeObject* CustomEditorsUtil::GetCustomEditor(MTypeObject* refType)
bool CustomEditorsUtilService::Init()
{
PROFILE_MEM(Editor);
TRACK_ASSEMBLY(((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly);
Scripting::BinaryModuleLoaded.Bind(&OnBinaryModuleLoaded);
@@ -77,6 +80,8 @@ bool CustomEditorsUtilService::Init()
void OnAssemblyLoaded(MAssembly* assembly)
{
PROFILE_CPU_NAMED("CustomEditors.OnAssemblyLoaded");
PROFILE_MEM(Editor);
Stopwatch stopwatch;
// Prepare FlaxEngine

View File

@@ -13,6 +13,8 @@ namespace FlaxEditor.CustomEditors.Dedicated
public class AudioSourceEditor : ActorEditor
{
private Label _infoLabel;
private Slider _slider;
private AudioSource.States _slideStartState;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
@@ -28,6 +30,13 @@ namespace FlaxEditor.CustomEditors.Dedicated
_infoLabel = playbackGroup.Label(string.Empty).Label;
_infoLabel.AutoHeight = true;
// Play back slider
var sliderElement = playbackGroup.CustomContainer<Slider>();
_slider = sliderElement.CustomControl;
_slider.ThumbSize = new Float2(_slider.ThumbSize.X * 0.5f, _slider.ThumbSize.Y);
_slider.SlidingStart += OnSlidingStart;
_slider.SlidingEnd += OnSlidingEnd;
var grid = playbackGroup.UniformGrid();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
@@ -40,6 +49,38 @@ namespace FlaxEditor.CustomEditors.Dedicated
}
}
private void OnSlidingEnd()
{
foreach (var value in Values)
{
if (value is AudioSource audioSource && audioSource.Clip)
{
switch (_slideStartState)
{
case AudioSource.States.Playing:
audioSource.Play();
break;
case AudioSource.States.Paused:
case AudioSource.States.Stopped:
audioSource.Pause();
break;
default: break;
}
}
}
}
private void OnSlidingStart()
{
foreach (var value in Values)
{
if (value is AudioSource audioSource && audioSource.Clip)
{
_slideStartState = audioSource.State;
}
}
}
/// <inheritdoc />
public override void Refresh()
{
@@ -51,7 +92,29 @@ namespace FlaxEditor.CustomEditors.Dedicated
foreach (var value in Values)
{
if (value is AudioSource audioSource && audioSource.Clip)
{
text += $"Time: {audioSource.Time:##0.0}s / {audioSource.Clip.Length:##0.0}s\n";
_slider.Maximum = audioSource.Clip.Length;
_slider.Minimum = 0;
if (_slider.IsSliding)
{
if (audioSource.State != AudioSource.States.Playing)
{
// Play to move slider correctly
audioSource.Play();
audioSource.Time = _slider.Value;
}
else
{
audioSource.Time = _slider.Value;
}
}
else
{
_slider.Value = audioSource.Time;
}
}
}
_infoLabel.Text = text;
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Dedicated
{
@@ -11,7 +12,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
[CustomEditor(typeof(EnvironmentProbe)), DefaultEditor]
public class EnvironmentProbeEditor : ActorEditor
{
private FlaxEngine.GUI.Button _bake;
private Button _bake;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
@@ -20,8 +21,9 @@ namespace FlaxEditor.CustomEditors.Dedicated
if (Values.HasDifferentTypes == false)
{
layout.Space(10);
_bake = layout.Button("Bake").Button;
var group = layout.Group("Bake");
group.Panel.ItemsMargin = new Margin(Utilities.Constants.UIMargin * 2);
_bake = group.Button("Bake").Button;
_bake.Clicked += BakeButtonClicked;
}
}

View File

@@ -32,7 +32,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
private void OnImageClicked(Image image, MouseButton button)
{
var texture = Values[0] as GPUTexture;
if (!texture)
if (!texture || button != MouseButton.Right)
return;
var menu = new ContextMenu();
menu.AddButton("Save...", () => Screenshot.Capture(Values[0] as GPUTexture));

View File

@@ -0,0 +1,38 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="NavMeshBoundsVolume"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(NavMeshBoundsVolume)), DefaultEditor]
internal class NavMeshBoundsVolumeEditor : ActorEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
if (Values.HasDifferentTypes == false)
{
var button = layout.Button("Build");
button.Button.Clicked += OnBuildClicked;
}
}
private void OnBuildClicked()
{
foreach (var value in Values)
{
if (value is NavMeshBoundsVolume volume)
{
Navigation.BuildNavMesh(volume.Box, volume.Scene);
Editor.Instance.Scene.MarkSceneEdited(volume.Scene);
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.Reflection.Emit;
using FlaxEditor.CustomEditors.GUI;
using FlaxEngine;
using FlaxEngine.GUI;

View File

@@ -36,6 +36,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
{
ScriptName = scriptName;
TooltipText = "Create a new script";
DrawHighlights = false;
}
}
@@ -70,7 +71,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
var buttonHeight = (textSize.Y < 18) ? 18 : textSize.Y + 4;
_addScriptsButton = new Button
{
TooltipText = "Add new scripts to the actor",
TooltipText = "Add new scripts to the actor.",
AnchorPreset = AnchorPresets.MiddleCenter,
Text = buttonText,
Parent = this,
@@ -114,7 +115,16 @@ namespace FlaxEditor.CustomEditors.Dedicated
cm.TextChanged += text =>
{
if (!IsValidScriptName(text))
{
// Remove NewScriptItems
List<Control> newScriptItems = cm.ItemsPanel.Children.FindAll(c => c is NewScriptItem);
foreach (var item in newScriptItems)
{
cm.ItemsPanel.RemoveChild(item);
}
return;
}
if (!cm.ItemsPanel.Children.Any(x => x.Visible && x is not NewScriptItem))
{
// If there are no visible items, that means the search failed so we can find the create script button or create one if it's the first time
@@ -876,7 +886,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
// Add drag button to the group
var scriptDrag = new DragImage
{
TooltipText = "Script reference",
TooltipText = "Script reference.",
AutoFocus = true,
IsScrollable = false,
Color = FlaxEngine.GUI.Style.Current.ForegroundGrey,
@@ -899,14 +909,17 @@ namespace FlaxEditor.CustomEditors.Dedicated
settingsButton.Tag = script;
settingsButton.Clicked += OnSettingsButtonClicked;
group.Panel.HeaderTextMargin = new Margin(scriptDrag.Right - 12, 15, 2, 2);
// Adjust margin to not overlap with other ui elements in the header
group.Panel.HeaderTextMargin = group.Panel.HeaderTextMargin with { Left = scriptDrag.Right - 12, Right = settingsButton.Width + Utilities.Constants.UIMargin };
group.Object(values, editor);
// Remove drop down arrows and containment lines if no objects in the group
if (group.Children.Count == 0)
{
group.Panel.Close();
group.Panel.ArrowImageOpened = null;
group.Panel.ArrowImageClosed = null;
group.Panel.EnableContainmentLines = false;
group.Panel.CanOpenClose = false;
}
// Scripts arrange bar

View File

@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Dedicated
{
@@ -19,8 +20,9 @@ namespace FlaxEditor.CustomEditors.Dedicated
if (Values.HasDifferentTypes == false)
{
// Add 'Bake' button
layout.Space(10);
var button = layout.Button("Bake");
var group = layout.Group("Bake");
group.Panel.ItemsMargin = new Margin(Utilities.Constants.UIMargin * 2);
var button = group.Button("Bake");
button.Button.Clicked += BakeButtonClicked;
}
}

View File

@@ -123,6 +123,8 @@ namespace FlaxEditor.CustomEditors.Editors
{
base.Refresh();
if (Picker == null)
return;
var differentValues = HasDifferentValues;
Picker.DifferentValues = differentValues;
if (!differentValues)

View File

@@ -71,7 +71,7 @@ namespace FlaxEditor.CustomEditors.Editors
menu.AddButton("Copy", linkedEditor.Copy);
var b = menu.AddButton("Duplicate", () => Editor.Duplicate(Index));
b.Enabled = linkedEditor.CanPaste && !Editor._readOnly;
b.Enabled = !Editor._readOnly && Editor._canResize;
b = menu.AddButton("Paste", linkedEditor.Paste);
b.Enabled = linkedEditor.CanPaste && !Editor._readOnly;
@@ -407,7 +407,7 @@ namespace FlaxEditor.CustomEditors.Editors
menu.AddButton("Copy", linkedEditor.Copy);
var b = menu.AddButton("Duplicate", () => Editor.Duplicate(Index));
b.Enabled = linkedEditor.CanPaste && !Editor._readOnly;
b.Enabled = !Editor._readOnly && Editor._canResize;
var paste = menu.AddButton("Paste", linkedEditor.Paste);
paste.Enabled = linkedEditor.CanPaste && !Editor._readOnly;
@@ -422,7 +422,8 @@ namespace FlaxEditor.CustomEditors.Editors
moveDownButton.Enabled = Index + 1 < Editor.Count;
}
menu.AddButton("Remove", OnRemoveClicked);
b = menu.AddButton("Remove", OnRemoveClicked);
b.Enabled = !Editor._readOnly && Editor._canResize;
menu.Show(panel, location);
}
@@ -449,6 +450,7 @@ namespace FlaxEditor.CustomEditors.Editors
protected bool NotNullItems;
private IntValueBox _sizeBox;
private Label _label;
private Color _background;
private int _elementsCount, _minCount, _maxCount;
private bool _readOnly;
@@ -565,7 +567,7 @@ namespace FlaxEditor.CustomEditors.Editors
Parent = dropPanel,
};
var label = new Label
_label = new Label
{
Text = "Size",
AnchorPreset = AnchorPresets.TopRight,
@@ -591,11 +593,12 @@ namespace FlaxEditor.CustomEditors.Editors
panel.Panel.Offsets = new Margin(7, 7, 0, 0);
panel.Panel.BackgroundColor = _background;
var elementType = ElementType;
var bindingAttr = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public;
bool single = elementType.IsPrimitive ||
elementType.Equals(new ScriptType(typeof(string))) ||
elementType.IsEnum ||
(elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) ||
(elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) ||
(elementType.GetFields(bindingAttr).Length == 1 && elementType.GetProperties(bindingAttr).Length == 0) ||
(elementType.GetProperties(bindingAttr).Length == 1 && elementType.GetFields(bindingAttr).Length == 0) ||
elementType.Equals(new ScriptType(typeof(JsonAsset))) ||
elementType.Equals(new ScriptType(typeof(SettingsBase)));
if (_cachedDropPanels == null)
@@ -649,7 +652,7 @@ namespace FlaxEditor.CustomEditors.Editors
panel.Panel.Size = new Float2(0, 18);
panel.Panel.Margin = new Margin(0, 0, Utilities.Constants.UIMargin, 0);
var removeButton = panel.Button("-", "Remove the last item");
var removeButton = panel.Button("-", "Remove the last item.");
removeButton.Button.Size = new Float2(16, 16);
removeButton.Button.Enabled = size > _minCount;
removeButton.Button.AnchorPreset = AnchorPresets.TopRight;
@@ -660,7 +663,7 @@ namespace FlaxEditor.CustomEditors.Editors
Resize(Count - 1);
};
var addButton = panel.Button("+", "Add a new item");
var addButton = panel.Button("+", "Add a new item.");
addButton.Button.Size = new Float2(16, 16);
addButton.Button.Enabled = (!NotNullItems || size > 0) && size < _maxCount;
addButton.Button.AnchorPreset = AnchorPresets.TopRight;
@@ -671,8 +674,10 @@ namespace FlaxEditor.CustomEditors.Editors
Resize(Count + 1);
};
}
}
Layout.ContainerControl.SizeChanged += OnLayoutSizeChanged;
}
private void OnSetupContextMenu(ContextMenu menu, DropPanel panel)
{
if (menu.Items.Any(x => x is ContextMenuButton b && b.Text.Equals("Open All", StringComparison.Ordinal)))
@@ -695,10 +700,24 @@ namespace FlaxEditor.CustomEditors.Editors
});
}
private void OnLayoutSizeChanged(Control control)
{
if (Layout.ContainerControl is DropPanel dropPanel)
{
// Hide "Size" text when array editor title overlaps
var headerTextSize = dropPanel.HeaderTextFont.GetFont().MeasureText(dropPanel.HeaderText);
if (headerTextSize.X + DropPanel.DropDownIconSize >= _label.Left)
_label.TextColor = _label.TextColorHighlighted = Color.Transparent;
else
_label.TextColor = _label.TextColorHighlighted = FlaxEngine.GUI.Style.Current.Foreground;
}
}
/// <inheritdoc />
protected override void Deinitialize()
{
_sizeBox = null;
Layout.ContainerControl.SizeChanged -= OnLayoutSizeChanged;
base.Deinitialize();
}

View File

@@ -22,7 +22,7 @@ internal class UIControlRefPickerControl : FlaxObjectRefPickerControl
/// <inheritdoc />
protected override bool IsValid(Object obj)
{
return obj == null || (obj is UIControl control && control.Control.GetType() == ControlType);
return obj == null || (obj is UIControl control && ControlType.IsAssignableFrom(control.Control.GetType()));
}
}

View File

@@ -180,7 +180,7 @@ namespace FlaxEditor.CustomEditors.Editors
// Show prefab diff when reference value type is different
var color = Color.Transparent;
if (Values.HasReferenceValue && CanRevertReferenceValue && Values[0].GetType() != Values.ReferenceValue.GetType())
if (Values.HasReferenceValue && CanRevertReferenceValue && Values[0]?.GetType() != Values.ReferenceValue?.GetType())
color = FlaxEngine.GUI.Style.Current.BackgroundSelected;
_typeItem.Labels[0].HighlightStripColor = color;

View File

@@ -104,7 +104,7 @@ namespace FlaxEditor.CustomEditors.Editors
public event Action<TypePickerControl> TypePickerValueChanged;
/// <summary>
/// The custom callback for types validation. Cane be used to implement a rule for types to pick.
/// The custom callback for types validation. Can be used to implement a rule for types to pick.
/// </summary>
public Func<ScriptType, bool> CheckValid;
@@ -353,7 +353,13 @@ namespace FlaxEditor.CustomEditors.Editors
}
if (!string.IsNullOrEmpty(typeReference.CheckMethod))
{
var parentType = ParentEditor.Values[0].GetType();
var parentEditor = ParentEditor;
// Find actual parent editor if parent editor is collection editor
while (parentEditor.GetType().IsAssignableTo(typeof(CollectionEditor)))
parentEditor = parentEditor.ParentEditor;
var parentType = parentEditor.Values[0].GetType();
var method = parentType.GetMethod(typeReference.CheckMethod, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{

View File

@@ -44,7 +44,8 @@ namespace FlaxEditor.CustomEditors.Elements
{
var style = Style.Current;
var settingsButtonSize = Panel.HeaderHeight;
return new Image
Panel.HeaderTextMargin = Panel.HeaderTextMargin with { Right = settingsButtonSize + Utilities.Constants.UIMargin };
; return new Image
{
TooltipText = "Settings",
AutoFocus = true,

View File

@@ -20,6 +20,7 @@
#include "Engine/Engine/Engine.h"
#include "Engine/ShadowsOfMordor/Builder.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "FlaxEngine.Gen.h"
#if PLATFORM_LINUX
#include "Engine/Tools/TextureTool/TextureTool.h"
@@ -47,6 +48,7 @@ void Editor::CloseSplashScreen()
bool Editor::CheckProjectUpgrade()
{
PROFILE_MEM(Editor);
const auto versionFilePath = Globals::ProjectCacheFolder / TEXT("version");
// Load version cache file
@@ -266,8 +268,8 @@ bool Editor::CheckProjectUpgrade()
// Check if last version was older
else if (lastVersion.Major < FLAXENGINE_VERSION_MAJOR || (lastVersion.Major == FLAXENGINE_VERSION_MAJOR && lastVersion.Minor < FLAXENGINE_VERSION_MINOR))
{
LOG(Warning, "The project was opened with the older editor version last time");
const auto result = MessageBox::Show(TEXT("The project was opened with the older editor version last time. Loading it may modify existing data so older editor version won't open it. Do you want to perform a backup before or cancel operation?"), TEXT("Project upgrade"), MessageBoxButtons::YesNoCancel, MessageBoxIcon::Question);
LOG(Warning, "The project was last opened with an older editor version");
const auto result = MessageBox::Show(TEXT("The project was last opened with an older editor version.\nLoading it may modify existing data, which can result in older editor versions being unable to open it.\n\nDo you want to perform a backup before or cancel the operation?"), TEXT("Project upgrade"), MessageBoxButtons::YesNoCancel, MessageBoxIcon::Question);
if (result == DialogResult::Yes)
{
if (BackupProject())
@@ -289,8 +291,8 @@ bool Editor::CheckProjectUpgrade()
// Check if last version was newer
else if (lastVersion.Major > FLAXENGINE_VERSION_MAJOR || (lastVersion.Major == FLAXENGINE_VERSION_MAJOR && lastVersion.Minor > FLAXENGINE_VERSION_MINOR))
{
LOG(Warning, "The project was opened with the newer editor version last time");
const auto result = MessageBox::Show(TEXT("The project was opened with the newer editor version last time. Loading it may fail and corrupt existing data. Do you want to perform a backup before or cancel operation?"), TEXT("Project upgrade"), MessageBoxButtons::YesNoCancel, MessageBoxIcon::Warning);
LOG(Warning, "The project was last opened with a newer editor version");
const auto result = MessageBox::Show(TEXT("The project was last opened with a newer editor version.\nLoading it may fail and corrupt existing data.\n\nDo you want to perform a backup before loading or cancel the operation?"), TEXT("Project upgrade"), MessageBoxButtons::YesNoCancel, MessageBoxIcon::Warning);
if (result == DialogResult::Yes)
{
if (BackupProject())
@@ -366,6 +368,8 @@ bool Editor::BackupProject()
int32 Editor::LoadProduct()
{
PROFILE_MEM(Editor);
// Flax Editor product
Globals::ProductName = TEXT("Flax Editor");
Globals::CompanyName = TEXT("Flax");
@@ -626,6 +630,7 @@ int32 Editor::LoadProduct()
Window* Editor::CreateMainWindow()
{
PROFILE_MEM(Editor);
Window* window = Managed->GetMainWindow();
#if PLATFORM_LINUX
@@ -662,6 +667,7 @@ bool Editor::Init()
return true;
}
PROFILE_CPU();
PROFILE_MEM(Editor);
// If during last lightmaps baking engine crashed we could try to restore the progress
ShadowsOfMordor::Builder::Instance()->CheckIfRestoreState();
@@ -693,11 +699,13 @@ bool Editor::Init()
void Editor::BeforeRun()
{
PROFILE_MEM(Editor);
Managed->BeforeRun();
}
void Editor::BeforeExit()
{
PROFILE_MEM(Editor);
CloseSplashScreen();
Managed->Exit();
@@ -708,6 +716,8 @@ void Editor::BeforeExit()
void EditorImpl::OnUpdate()
{
PROFILE_MEM(Editor);
// Update c# editor
Editor::Managed->Update();

View File

@@ -23,6 +23,7 @@ using FlaxEngine.Assertions;
using FlaxEngine.GUI;
using FlaxEngine.Interop;
using FlaxEngine.Json;
using FlaxEngine.Utilities;
#pragma warning disable CS1591
@@ -51,6 +52,7 @@ namespace FlaxEditor
private readonly List<EditorModule> _modules = new List<EditorModule>(16);
private bool _isAfterInit, _areModulesInited, _areModulesAfterInitEnd, _isHeadlessMode, _autoExit;
private string _projectToOpen;
private bool _projectIsNew;
private float _lastAutoSaveTimer, _autoExitTimeout = 0.1f;
private Button _saveNowButton;
private Button _cancelSaveButton;
@@ -737,11 +739,12 @@ namespace FlaxEditor
var procSettings = new CreateProcessSettings
{
FileName = Platform.ExecutableFilePath,
Arguments = string.Format("-project \"{0}\"", _projectToOpen),
Arguments = string.Format("-project \"{0}\"" + (_projectIsNew ? " -new" : string.Empty), _projectToOpen),
ShellExecute = true,
WaitForEnd = false,
HiddenWindow = false,
};
_projectIsNew = false;
_projectToOpen = null;
Platform.CreateProcess(ref procSettings);
}
@@ -790,6 +793,24 @@ namespace FlaxEditor
}
}
/// <summary>
/// Creates the given project. Afterwards closes this project with running editor and opens the given project.
/// </summary>
/// <param name="projectFilePath">The project file path.</param>
public void NewProject(string projectFilePath)
{
if (projectFilePath == null)
{
MessageBox.Show("Missing project");
return;
}
// Cache project path and start editor exit (it will open new instance on valid closing)
_projectToOpen = StringUtils.NormalizePath(Path.GetDirectoryName(projectFilePath));
_projectIsNew = true;
Windows.MainWindow.Close(ClosingReason.User);
}
/// <summary>
/// Closes this project with running editor and opens the given project.
/// </summary>
@@ -1350,7 +1371,7 @@ namespace FlaxEditor
public void BuildCSG()
{
var scenes = Level.Scenes;
scenes.ToList().ForEach(x => x.BuildCSG(0));
scenes.ForEach(x => x.BuildCSG(0));
Scene.MarkSceneEdited(scenes);
}
@@ -1360,7 +1381,7 @@ namespace FlaxEditor
public void BuildNavMesh()
{
var scenes = Level.Scenes;
scenes.ToList().ForEach(x => Navigation.BuildNavMesh(x, 0));
Navigation.BuildNavMesh();
Scene.MarkSceneEdited(scenes);
}
@@ -1370,6 +1391,7 @@ namespace FlaxEditor
public void BuildAllMeshesSDF()
{
var models = new List<Model>();
var forceRebuild = Input.GetKey(KeyboardKeys.F);
Scene.ExecuteOnGraph(node =>
{
if (node is StaticModelNode staticModelNode && staticModelNode.Actor is StaticModel staticModel)
@@ -1379,7 +1401,7 @@ namespace FlaxEditor
model != null &&
!models.Contains(model) &&
!model.IsVirtual &&
model.SDF.Texture == null)
(forceRebuild || model.SDF.Texture == null))
{
models.Add(model);
}
@@ -1392,7 +1414,17 @@ namespace FlaxEditor
{
var model = models[i];
Log($"[{i}/{models.Count}] Generating SDF for {model}");
if (!model.GenerateSDF())
float resolutionScale = 1.0f, backfacesThreshold = 0.6f;
int lodIndex = 6;
bool useGPU = true;
var sdf = model.SDF;
if (sdf.Texture != null)
{
// Preserve options set on this model
resolutionScale = sdf.ResolutionScale;
lodIndex = sdf.LOD;
}
if (!model.GenerateSDF(resolutionScale, lodIndex, true, backfacesThreshold, useGPU))
model.Save();
}
});
@@ -1567,7 +1599,7 @@ namespace FlaxEditor
if (dockedTo != null && dockedTo.SelectedTab != gameWin && dockedTo.SelectedTab != null)
result = dockedTo.SelectedTab.Size;
else
result = gameWin.Viewport.Size;
result = gameWin.Viewport.ContentSize;
result *= root.DpiScale;
result = Float2.Round(result);

View File

@@ -51,7 +51,7 @@ namespace FlaxEditor.GUI
/// <summary>
/// The column title horizontal text alignment
/// </summary>
public TextAlignment TitleAlignment = TextAlignment.Near;
public TextAlignment TitleAlignment = TextAlignment.Center;
/// <summary>
/// The column title margin.

View File

@@ -502,6 +502,7 @@ namespace FlaxEditor.GUI.ContextMenu
if (base.OnKeyDown(key))
return true;
// Keyboard navigation around the menu
switch (key)
{
case KeyboardKeys.ArrowDown:
@@ -526,6 +527,20 @@ namespace FlaxEditor.GUI.ContextMenu
}
}
break;
case KeyboardKeys.ArrowRight:
for (int i = 0; i < _panel.Children.Count; i++)
{
if (_panel.Children[i] is ContextMenuChildMenu item && item.Visible && item.IsFocused && !item.ContextMenu.IsOpened)
{
item.ShowChild(this);
item.ContextMenu._panel.Children.FirstOrDefault(x => x is ContextMenuButton && x.Visible)?.Focus();
break;
}
}
break;
case KeyboardKeys.ArrowLeft:
ParentCM?.RootWindow.Focus();
break;
}
return false;

View File

@@ -72,6 +72,11 @@ namespace FlaxEditor.GUI.ContextMenu
/// </summary>
public bool HasChildCMOpened => _childCM != null;
/// <summary>
/// Gets the parent context menu (if exists).
/// </summary>
public ContextMenuBase ParentCM => _parentCM;
/// <summary>
/// Gets the topmost context menu.
/// </summary>
@@ -81,9 +86,7 @@ namespace FlaxEditor.GUI.ContextMenu
{
var cm = this;
while (cm._parentCM != null && cm._isSubMenu)
{
cm = cm._parentCM;
}
return cm;
}
}
@@ -108,15 +111,21 @@ namespace FlaxEditor.GUI.ContextMenu
/// </summary>
public bool UseInput = true;
/// <summary>
/// Optional flag that can disable UI navigation (tab/enter).
/// </summary>
public bool UseNavigation = true;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenuBase"/> class.
/// </summary>
public ContextMenuBase()
: base(0, 0, 120, 32)
{
_direction = ContextMenuDirection.RightDown;
Visible = false;
AutoFocus = true;
_direction = ContextMenuDirection.RightDown;
_isSubMenu = true;
}
@@ -593,6 +602,21 @@ namespace FlaxEditor.GUI.ContextMenu
case KeyboardKeys.Escape:
Hide();
return true;
case KeyboardKeys.Return:
if (UseNavigation && Root?.FocusedControl != null)
{
Root.SubmitFocused();
return true;
}
break;
case KeyboardKeys.Tab:
if (UseNavigation && Root != null)
{
bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next);
return true;
}
break;
}
return false;
}

View File

@@ -29,7 +29,7 @@ namespace FlaxEditor.GUI.ContextMenu
CloseMenuOnClick = false;
}
private void ShowChild(ContextMenu parentContextMenu)
internal void ShowChild(ContextMenu parentContextMenu)
{
// Hide parent CM popups and set itself as child
var vAlign = parentContextMenu.ItemsAreaMargin.Top;

View File

@@ -522,6 +522,16 @@ namespace FlaxEditor.GUI
cm.AddButton("Show whole curve", _editor.ShowWholeCurve);
cm.AddButton("Reset view", _editor.ResetView);
}
cm.AddSeparator();
var presetCm = cm.AddChildMenu("Apply preset");
foreach (var value in Enum.GetValues(typeof(CurvePreset)))
{
CurvePreset preset = (CurvePreset)value;
string name = Utilities.Utils.GetPropertyNameUI(preset.ToString());
var b = presetCm.ContextMenu.AddButton(name, () => _editor.ApplyPreset(preset));
b.Enabled = !(_editor is LinearCurveEditor<T> && (preset != CurvePreset.Constant && preset != CurvePreset.Linear));
}
_editor.OnShowContextMenu(cm, selectionCount);
cm.Show(this, location);
}
@@ -619,6 +629,33 @@ namespace FlaxEditor.GUI
}
}
/// <summary>
/// A list of avaliable curve presets for the <see cref="CurveEditor{T}"/>.
/// </summary>
public enum CurvePreset
{
/// <summary>
/// A curve where every point has the same value.
/// </summary>
Constant,
/// <summary>
/// A curve linear curve.
/// </summary>
Linear,
/// <summary>
/// A curve that starts a slowly and then accelerates until the end.
/// </summary>
EaseIn,
/// <summary>
/// A curve that starts a steep and then flattens until the end.
/// </summary>
EaseOut,
/// <summary>
/// A combination of the <see cref="CurvePreset.EaseIn"/> and <see cref="CurvePreset.EaseOut"/> preset.
/// </summary>
Smoothstep
}
/// <inheritdoc />
public override void OnKeyframesDeselect(IKeyframesEditor editor)
{

View File

@@ -19,6 +19,48 @@ namespace FlaxEditor.GUI
/// <seealso cref="CurveEditorBase" />
public abstract partial class CurveEditor<T> : CurveEditorBase where T : new()
{
/// <summary>
/// Represents a single point in a <see cref="CurveEditorPreset"/>.
/// </summary>
protected struct CurvePresetPoint
{
/// <summary>
/// The time.
/// </summary>
public float Time;
/// <summary>
/// The value.
/// </summary>
public float Value;
/// <summary>
/// The in tangent. Will be ignored in <see cref="LinearCurveEditor{T}"/>
/// </summary>
public float TangentIn;
/// <summary>
/// The out tangent. Will be ignored in <see cref="LinearCurveEditor{T}"/>
/// </summary>
public float TangentOut;
}
/// <summary>
/// A curve preset.
/// </summary>
protected struct CurveEditorPreset()
{
/// <summary>
/// If the tangents will be linear or smooth.
/// </summary>
public bool LinearTangents;
/// <summary>
/// The points of the preset.
/// </summary>
public List<CurvePresetPoint> Points;
}
private class Popup : ContextMenuBase
{
private CustomEditorPresenter _presenter;
@@ -26,11 +68,12 @@ namespace FlaxEditor.GUI
private List<int> _keyframeIndices;
private bool _isDirty;
public Popup(CurveEditor<T> editor, object[] selection, List<int> keyframeIndices = null, float height = 140.0f)
: this(editor, height)
public Popup(CurveEditor<T> editor, object[] selection, List<int> keyframeIndices = null, float maxHeight = 140.0f)
: this(editor, maxHeight)
{
_presenter.Select(selection);
_presenter.OpenAllGroups();
Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight));
_keyframeIndices = keyframeIndices;
if (keyframeIndices != null && selection.Length != keyframeIndices.Count)
throw new Exception();
@@ -169,7 +212,7 @@ namespace FlaxEditor.GUI
if (IsSelected)
color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f);
if (IsMouseOver)
color *= 1.1f;
color *= 1.5f;
Render2D.FillRectangle(rect, color);
}
@@ -285,7 +328,7 @@ namespace FlaxEditor.GUI
/// <summary>
/// The keyframes size.
/// </summary>
protected static readonly Float2 KeyframesSize = new Float2(7.0f);
protected static readonly Float2 KeyframesSize = new Float2(8.0f);
/// <summary>
/// The colors for the keyframe points.
@@ -326,6 +369,63 @@ namespace FlaxEditor.GUI
private Color _labelsColor;
private Font _labelsFont;
/// <summary>
/// Preset values for <see cref="CurvePreset"/> to be applied to a <see cref="CurveEditor{T}"/>.
/// </summary>
protected Dictionary<CurvePreset, CurveEditorPreset> Presets = new Dictionary<CurvePreset, CurveEditorPreset>
{
{ CurvePreset.Constant, new CurveEditorPreset
{
LinearTangents = true,
Points = new List<CurvePresetPoint>
{
new CurvePresetPoint { Time = 0f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f },
new CurvePresetPoint { Time = 1f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f },
}
}
},
{ CurvePreset.EaseIn, new CurveEditorPreset
{
LinearTangents = false,
Points = new List<CurvePresetPoint>
{
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = -1.4f, TangentOut = 0f },
}
}
},
{ CurvePreset.EaseOut, new CurveEditorPreset
{
LinearTangents = false,
Points = new List<CurvePresetPoint>
{
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 1.4f },
}
}
},
{ CurvePreset.Linear, new CurveEditorPreset
{
LinearTangents = true,
Points = new List<CurvePresetPoint>
{
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
}
}
},
{ CurvePreset.Smoothstep, new CurveEditorPreset
{
LinearTangents = false,
Points = new List<CurvePresetPoint>
{
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
}
}
},
};
/// <summary>
/// The keyframe UI points.
/// </summary>
@@ -568,6 +668,28 @@ namespace FlaxEditor.GUI
/// <param name="indicesToRemove">The list of indices of the keyframes to remove.</param>
protected abstract void RemoveKeyframesInternal(HashSet<int> indicesToRemove);
/// <summary>
/// Tries to convert a float to the type of the type wildcard of the curve editor.
/// </summary>
/// <param name="value">The float.</param>
/// <returns>The converted value.</returns>
public static object ConvertCurvePresetValueToCurveEditorType(float value)
{
if (typeof(T) == typeof(Float2))
return new Float2(value);
if (typeof(T) == typeof(Float3))
return new Float3(value);
if (typeof(T) == typeof(Float4))
return new Float4(value);
if (typeof(T) == typeof(Vector2))
return new Vector2(value);
if (typeof(T) == typeof(Vector3))
return new Vector3(value);
if (typeof(T) == typeof(Vector4))
return new Vector4(value);
return value;
}
/// <summary>
/// Called when showing a context menu. Can be used to add custom buttons with actions.
/// </summary>
@@ -752,6 +874,17 @@ namespace FlaxEditor.GUI
ShowCurve(false);
}
/// <summary>
/// Applies a <see cref="CurvePreset"/> to the curve editor.
/// </summary>
/// <param name="preset">The preset.</param>
public virtual void ApplyPreset(CurvePreset preset)
{
// Remove existing keyframes
SelectAll();
RemoveKeyframes();
}
/// <inheritdoc />
public override void Evaluate(out object result, float time, bool loop = false)
{
@@ -1028,6 +1161,31 @@ namespace FlaxEditor.GUI
return true;
}
bool left = key == KeyboardKeys.ArrowLeft;
bool right = key == KeyboardKeys.ArrowRight;
bool up = key == KeyboardKeys.ArrowUp;
bool down = key == KeyboardKeys.ArrowDown;
if (left || right || up || down)
{
bool shift = Root.GetKey(KeyboardKeys.Shift);
bool alt = Root.GetKey(KeyboardKeys.Alt);
float deltaValue = 10f;
if (shift || alt)
deltaValue = shift ? 2.5f : 5f;
Float2 moveDelta = Float2.Zero;
if (left || right)
moveDelta.X = left ? -deltaValue : deltaValue;
if (up || down)
moveDelta.Y = up ? -deltaValue : deltaValue;
_contents.OnMoveStart(Float2.Zero);
_contents.OnMove(moveDelta);
_contents.OnMoveEnd(Float2.Zero);
return true;
}
return false;
}
@@ -1526,6 +1684,22 @@ namespace FlaxEditor.GUI
_tangents[i].Visible = false;
}
/// <inheritdoc />
public override void ApplyPreset(CurvePreset preset)
{
base.ApplyPreset(preset);
CurveEditorPreset data = Presets[preset];
foreach (var point in data.Points)
{
float time = point.Time;
object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value);
AddKeyframe(time, value);
}
ShowWholeCurve();
}
/// <inheritdoc />
protected override void DrawCurve(ref Rectangle viewRect)
{
@@ -2312,6 +2486,30 @@ namespace FlaxEditor.GUI
}
}
/// <inheritdoc />
public override void ApplyPreset(CurvePreset preset)
{
base.ApplyPreset(preset);
CurveEditorPreset data = Presets[preset];
foreach (var point in data.Points)
{
float time = point.Time;
object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value);
object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)point.TangentIn);
object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)point.TangentOut);
AddKeyframe(time, value, tangentIn, tangentOut);
}
SelectAll();
if (data.LinearTangents)
SetTangentsLinear();
ShowWholeCurve();
}
/// <inheritdoc />
protected override void SetScaleInternal(ref Float2 scale)
{

View File

@@ -76,6 +76,8 @@ namespace FlaxEditor.GUI.Dialogs
public ColorSelector(float wheelSize)
: base(0, 0, wheelSize, wheelSize)
{
AutoFocus = true;
_colorWheelSprite = Editor.Instance.Icons.ColorWheel128;
_wheelRect = new Rectangle(0, 0, wheelSize, wheelSize);
}

View File

@@ -469,7 +469,7 @@ namespace FlaxEditor.GUI.Docking
var childPanels = _childPanels.ToArray();
if (childPanels.Length != 0)
{
// Move tabs from child panels into this one
// Fallback: move tabs from child panels into this one.
DockWindow selectedTab = null;
foreach (var childPanel in childPanels)
{
@@ -490,7 +490,8 @@ namespace FlaxEditor.GUI.Docking
{
// Unlink splitter
var splitterParent = splitter.Parent;
Assert.IsNotNull(splitterParent);
if (splitterParent == null)
return;
splitter.Parent = null;
// Move controls from second split panel to the split panel parent
@@ -507,17 +508,63 @@ namespace FlaxEditor.GUI.Docking
splitter.Dispose();
}
}
else if (IsMaster && _childPanels.Count != 0)
{
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
return;
}
else if (!IsMaster)
{
throw new InvalidOperationException();
}
}
else if (_childPanels.Count != 0)
{
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
return;
}
else if (!IsMaster)
{
throw new InvalidOperationException();
}
}
internal bool CollapseEmptyTabsProxy()
{
if (TabsCount == 0 && ChildPanelsCount > 0)
{
return TryCollapseSplitter(_tabsProxy?.Parent as Panel);
}
return false;
}
private bool TryCollapseSplitter(Panel removedPanelParent)
{
if (removedPanelParent == null)
return false;
if (!(removedPanelParent.Parent is SplitPanel tabsSplitter))
return false;
var splitterParent = tabsSplitter.Parent;
if (splitterParent == null)
return false;
tabsSplitter.Parent = null;
var scrPanel = removedPanelParent == tabsSplitter.Panel2 ? tabsSplitter.Panel1 : tabsSplitter.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);
tabsSplitter.Dispose();
if (_tabsProxy != null && _tabsProxy.Parent == removedPanelParent)
_tabsProxy = null;
return true;
}
internal virtual void DockWindowInternal(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null)
{
DockWindow(state, window, autoSelect, splitterValue);

View File

@@ -70,8 +70,6 @@ namespace FlaxEditor.GUI.Docking
internal DockPanelProxy(DockPanel panel)
: base(0, 0, 64, 64)
{
AutoFocus = false;
_panel = panel;
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;

View File

@@ -129,11 +129,39 @@ namespace FlaxEditor.GUI.Input
{
base.Draw();
var style = Style.Current;
var r = new Rectangle(0, 0, Width, Height);
bool isTransparent = _value.A < 1;
Render2D.FillRectangle(r, _value);
Render2D.DrawRectangle(r, IsMouseOver || IsNavFocused ? style.BackgroundSelected : Color.Black);
var style = Style.Current;
var fullRect = new Rectangle(0, 0, Width, Height);
var colorRect = new Rectangle(0, 0, isTransparent ? Width * 0.7f : Width, Height);
if (isTransparent)
{
var alphaRect = new Rectangle(colorRect.Right, 0, Width - colorRect.Right, Height);
// Draw checkerboard pattern to part of the color value box
Render2D.FillRectangle(alphaRect, Color.White);
var smallRectSize = 7.9f;
var numHor = Mathf.CeilToInt(alphaRect.Width / smallRectSize);
var numVer = Mathf.CeilToInt(alphaRect.Height / smallRectSize);
for (int i = 0; i < numHor; i++)
{
for (int j = 0; j < numVer; j++)
{
if ((i + j) % 2 == 0)
{
var rect = new Rectangle(alphaRect.X + smallRectSize * i, alphaRect.Y + smallRectSize * j, new Float2(smallRectSize));
Render2D.PushClip(alphaRect);
Render2D.FillRectangle(rect, Color.Gray);
Render2D.PopClip();
}
}
}
Render2D.FillRectangle(alphaRect, _value);
}
Render2D.FillRectangle(colorRect, _value with { A = 1 });
Render2D.DrawRectangle(fullRect, IsMouseOver || IsNavFocused ? style.BackgroundSelected : Color.Black);
}
/// <inheritdoc />

View File

@@ -368,6 +368,8 @@ namespace FlaxEditor.GUI.Input
public SliderControl(float value, float x = 0, float y = 0, float width = 120, float min = float.MinValue, float max = float.MaxValue)
: base(x, y, width, TextBox.DefaultHeight)
{
AutoFocus = true;
_min = min;
_max = max;
_value = Mathf.Clamp(value, min, max);

View File

@@ -99,6 +99,11 @@ namespace FlaxEditor.GUI.Input
/// </summary>
public event Action SlidingEnd;
/// <summary>
/// If enabled, pressing the arrow up or down key increments/ decrements the value.
/// </summary>
public bool ArrowKeysIncrement = true;
/// <summary>
/// Gets or sets the slider speed. Use value 0 to disable and hide slider UI.
/// </summary>
@@ -239,6 +244,27 @@ namespace FlaxEditor.GUI.Input
ResetViewOffset();
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (ArrowKeysIncrement && (key == KeyboardKeys.ArrowUp || key == KeyboardKeys.ArrowDown))
{
bool altDown = Root.GetKey(KeyboardKeys.Alt);
bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
bool controlDown = Root.GetKey(KeyboardKeys.Control);
float deltaValue = altDown ? 0.1f : (shiftDown ? 10f : (controlDown ? 100f : 1f));
float slideDelta = key == KeyboardKeys.ArrowUp ? deltaValue : -deltaValue;
_startSlideValue = Value;
ApplySliding(slideDelta);
EndSliding();
Focus();
return true;
}
return base.OnKeyDown(key);
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{

View File

@@ -51,6 +51,11 @@ namespace FlaxEditor.GUI
/// </summary>
public float SortScore;
/// <summary>
/// Wether the query highlights should be draw.
/// </summary>
public bool DrawHighlights = true;
/// <summary>
/// Occurs when items gets clicked by the user.
/// </summary>
@@ -165,7 +170,7 @@ namespace FlaxEditor.GUI
Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), style.BackgroundHighlighted);
// Draw all highlights
if (_highlights != null)
if (DrawHighlights && _highlights != null)
{
var color = style.ProgressNormal * 0.6f;
for (int i = 0; i < _highlights.Count; i++)

View File

@@ -10,6 +10,7 @@ using System.Text;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
using FlaxEngine.Utilities;
namespace FlaxEditor.GUI.Timeline.Tracks
@@ -54,7 +55,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks
var paramTypeName = LoadName(stream);
e.EventParamsTypes[i] = TypeUtils.GetManagedType(paramTypeName);
if (e.EventParamsTypes[i] == null)
{
Editor.LogError($"Unknown type {paramTypeName}.");
isInvalid = true;
}
}
if (isInvalid)
@@ -82,7 +86,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
for (int j = 0; j < paramsCount; j++)
{
stream.Read(dataBuffer, 0, e.EventParamsSizes[j]);
key.Parameters[j] = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), e.EventParamsTypes[j]);
key.Parameters[j] = Utilities.Utils.ByteArrayToStructure(handle.AddrOfPinnedObject(), e.EventParamsTypes[j], e.EventParamsSizes[j]);
}
events[i] = new KeyframesEditor.Keyframe
@@ -125,8 +129,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
for (int j = 0; j < paramsCount; j++)
{
Marshal.StructureToPtr(key.Parameters[j], ptr, true);
Marshal.Copy(ptr, dataBuffer, 0, e.EventParamsSizes[j]);
Utilities.Utils.StructureToByteArray(key.Parameters[j], e.EventParamsSizes[j], ptr, dataBuffer);
stream.Write(dataBuffer, 0, e.EventParamsSizes[j]);
}
}
@@ -153,7 +156,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// <summary>
/// The event key data.
/// </summary>
public struct EventKey
public struct EventKey : ICloneable
{
/// <summary>
/// The parameters values.
@@ -178,6 +181,26 @@ namespace FlaxEditor.GUI.Timeline.Tracks
sb.Append(')');
return sb.ToString();
}
/// <inheritdoc />
public object Clone()
{
if (Parameters == null)
return new EventKey();
// Deep clone parameter values (especially boxed value types need to be duplicated to avoid referencing the same ones)
var parameters = new object[Parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
var p = Parameters[i];
if (p == null || p is FlaxEngine.Object)
parameters[i] = Parameters[i];
else
parameters[i] = JsonSerializer.Deserialize(JsonSerializer.Serialize(p), p.GetType());
}
return new EventKey { Parameters = parameters };
}
}
/// <inheritdoc />
@@ -234,6 +257,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
var time = Timeline.CurrentTime;
if (!TryGetValue(out var value))
value = Events.Evaluate(time);
value = ((ICloneable)value).Clone();
// Find event at the current location
for (int i = Events.Keyframes.Count - 1; i >= 0; i--)

View File

@@ -77,7 +77,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{
var time = stream.ReadSingle();
stream.Read(dataBuffer, 0, e.ValueSize);
var value = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), propertyType);
var value = Utilities.Utils.ByteArrayToStructure(handle.AddrOfPinnedObject(), propertyType, e.ValueSize);
keyframes[i] = new KeyframesEditor.Keyframe
{
@@ -142,8 +142,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
for (int i = 0; i < keyframes.Count; i++)
{
var keyframe = keyframes[i];
Marshal.StructureToPtr(keyframe.Value, ptr, true);
Marshal.Copy(ptr, dataBuffer, 0, e.ValueSize);
Utilities.Utils.StructureToByteArray(keyframe.Value, e.ValueSize, ptr, dataBuffer);
stream.Write(keyframe.Time);
stream.Write(dataBuffer);
}

View File

@@ -447,8 +447,8 @@ namespace FlaxEditor.GUI.Tree
// Select previous parent child
var select = nodeParent.GetChild(myIndex - 1) as TreeNode;
// Select last child if is valid and expanded and has any children
if (select != null && select.IsExpanded && select.HasAnyVisibleChild)
// Get bottom most child node
while (select != null && select.IsExpanded && select.HasAnyVisibleChild)
{
select = select.GetChild(select.ChildrenCount - 1) as TreeNode;
}

View File

@@ -319,6 +319,8 @@ namespace FlaxEditor.GUI.Tree
public TreeNode(bool canChangeOrder, SpriteHandle iconCollapsed, SpriteHandle iconOpened)
: base(0, 0, 64, 16)
{
AutoFocus = true;
_canChangeOrder = canChangeOrder;
_animationProgress = 1.0f;
_cachedHeight = _headerHeight;
@@ -1138,8 +1140,11 @@ namespace FlaxEditor.GUI.Tree
ParentTree.DraggedOverNode = this;
// Expand node if mouse goes over arrow
if (ArrowRect.Contains(location) && HasAnyVisibleChild)
if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
{
Expand(true);
ParentTree?.FlushPendingPerformLayout();
}
result = OnDragEnterHeader(data);
}
@@ -1170,8 +1175,11 @@ namespace FlaxEditor.GUI.Tree
ParentTree.DraggedOverNode = this;
// Expand node if mouse goes over arrow
if (ArrowRect.Contains(location) && HasAnyVisibleChild)
if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed)
{
Expand(true);
ParentTree?.FlushPendingPerformLayout();
}
if (!_isDragOverHeader)
result = OnDragEnterHeader(data);

View File

@@ -13,6 +13,7 @@ namespace FlaxEditor.Gizmo
public abstract class GizmoBase
{
private IGizmoOwner _owner;
private bool _visible = true;
/// <summary>
/// Gets the gizmo owner.
@@ -34,6 +35,11 @@ namespace FlaxEditor.Gizmo
/// </summary>
public virtual BoundingSphere FocusBounds => BoundingSphere.Empty;
/// <summary>
/// Gets or sets a value indicating whether this gizmo is visible.
/// </summary>
public bool Visible { get { return _visible; } set { _visible = value; } }
/// <summary>
/// Initializes a new instance of the <see cref="GizmoBase"/> class.
/// </summary>

View File

@@ -155,6 +155,7 @@ namespace FlaxEditor.Gizmo
// Ensure player is not moving objects
if (ActiveAxis != Axis.None)
return;
Profiler.BeginEvent("Pick");
// Get mouse ray and try to hit any object
var ray = Owner.MouseRay;
@@ -243,6 +244,8 @@ namespace FlaxEditor.Gizmo
{
sceneEditing.Deselect();
}
Profiler.EndEvent();
}
/// <inheritdoc />

View File

@@ -36,11 +36,12 @@ public sealed class ViewportRubberBandSelector
/// Triggers the start of a rubber band selection.
/// </summary>
/// <returns>True if selection started, otherwise false.</returns>
public bool TryStartingRubberBandSelection()
public bool TryStartingRubberBandSelection(Float2 mousePosition)
{
if (!_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
{
_tryStartRubberBand = true;
_cachedStartingMousePosition = mousePosition;
return true;
}
return false;
@@ -82,12 +83,15 @@ public sealed class ViewportRubberBandSelector
return;
}
if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart)
if (_tryStartRubberBand && canStart)
{
_isRubberBandSpanning = true;
_cachedStartingMousePosition = mousePosition;
_rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
_tryStartRubberBand = false;
var delta = mousePosition - _cachedStartingMousePosition;
if (Mathf.Abs(delta.X) > 0.1f || Mathf.Abs(delta.Y) > 0.1f)
{
_isRubberBandSpanning = true;
_rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
_tryStartRubberBand = false;
}
}
else if (_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
{

View File

@@ -14,6 +14,7 @@
#include "Engine/Platform/WindowsManager.h"
#include "Engine/Content/Assets/VisualScript.h"
#include "Engine/Content/Content.h"
#include "Engine/Level/Actor.h"
#include "Engine/CSG/CSGBuilder.h"
#include "Engine/Engine/CommandLine.h"
#include "Engine/Renderer/ProbesRenderer.h"
@@ -75,7 +76,7 @@ void OnLightmapsBuildFinished(bool failed)
OnLightmapsBake(ShadowsOfMordor::BuildProgressStep::GenerateLightmapCharts, 0, 0, false);
}
void OnBakeEvent(bool started, const ProbesRenderer::Entry& e)
void OnBakeEvent(bool started, Actor* e)
{
if (Internal_EnvProbeBake == nullptr)
{
@@ -83,7 +84,7 @@ void OnBakeEvent(bool started, const ProbesRenderer::Entry& e)
ASSERT(Internal_EnvProbeBake);
}
MObject* probeObj = e.Actor ? e.Actor->GetManagedInstance() : nullptr;
MObject* probeObj = e ? e->GetManagedInstance() : nullptr;
MainThreadManagedInvokeAction::ParamsBuilder params;
params.AddParam(started);
@@ -91,12 +92,12 @@ void OnBakeEvent(bool started, const ProbesRenderer::Entry& e)
MainThreadManagedInvokeAction::Invoke(Internal_EnvProbeBake, params);
}
void OnRegisterBake(const ProbesRenderer::Entry& e)
void OnRegisterBake(Actor* e)
{
OnBakeEvent(true, e);
}
void OnFinishBake(const ProbesRenderer::Entry& e)
void OnFinishBake(Actor* e)
{
OnBakeEvent(false, e);
}
@@ -157,7 +158,9 @@ ManagedEditor::ManagedEditor()
lightmapsBuilder->OnBuildProgress.Bind<OnLightmapsBuildProgress>();
lightmapsBuilder->OnBuildFinished.Bind<OnLightmapsBuildFinished>();
CSG::Builder::OnBrushModified.Bind<OnBrushModified>();
#if LOG_ENABLE
Log::Logger::OnMessage.Bind<OnLogMessage>();
#endif
VisualScripting::DebugFlow.Bind<OnVisualScriptingDebugFlow>();
}
@@ -173,7 +176,9 @@ ManagedEditor::~ManagedEditor()
lightmapsBuilder->OnBuildProgress.Unbind<OnLightmapsBuildProgress>();
lightmapsBuilder->OnBuildFinished.Unbind<OnLightmapsBuildFinished>();
CSG::Builder::OnBrushModified.Unbind<OnBrushModified>();
#if LOG_ENABLE
Log::Logger::OnMessage.Unbind<OnLogMessage>();
#endif
VisualScripting::DebugFlow.Unbind<OnVisualScriptingDebugFlow>();
}

View File

@@ -229,7 +229,7 @@ namespace FlaxEditor.Modules
if (!isPlayMode && options.General.AutoRebuildNavMesh && actor.Scene && node.AffectsNavigationWithChildren)
{
var bounds = actor.BoxWithChildren;
Navigation.BuildNavMesh(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
@@ -673,6 +673,7 @@ namespace FlaxEditor.Modules
pasteAction.Do(out _, out var nodeParents);
// Select spawned objects (parents only)
newSelection.Clear();
newSelection.AddRange(nodeParents);
var selectAction = new SelectionChangeAction(Selection.ToArray(), newSelection.ToArray(), OnSelectionUndo);
selectAction.Do();
@@ -711,7 +712,11 @@ namespace FlaxEditor.Modules
private void OnActorChildNodesDispose(ActorNode node)
{
if (Selection.Count == 0)
return;
// TODO: cache if selection contains any actor child node and skip this loop if no need to iterate
// TODO: or build a hash set with selected nodes for quick O(1) checks (cached until selection changes)
// Deselect child nodes
for (int i = 0; i < node.ChildNodes.Count; i++)

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEngine;
@@ -662,6 +663,48 @@ namespace FlaxEditor.Modules
//node?.TreeNode.OnActiveChanged();
}
private void OnActorDestroyChildren(Actor actor)
{
// Instead of doing OnActorParentChanged for every child lets remove all of them at once from that actor
ActorNode node = GetActorNode(actor);
if (node != null)
{
if (Editor.SceneEditing.HasSthSelected)
{
// Clear selection if one of the removed actors is selected
var selection = new HashSet<Actor>();
foreach (var e in Editor.SceneEditing.Selection)
{
if (e is ActorNode q && q.Actor)
selection.Add(q.Actor);
}
var count = actor.ChildrenCount;
for (int i = 0; i < count; i++)
{
var child = actor.GetChild(i);
if (selection.Contains(child))
{
Editor.SceneEditing.Deselect();
break;
}
}
}
// Remove all child nodes (upfront remove all nodes to run faster)
for (int i = 0; i < node.ChildNodes.Count; i++)
{
if (node.ChildNodes[i] is ActorNode child)
child.parentNode = null;
}
node.TreeNode.DisposeChildren();
for (int i = 0; i < node.ChildNodes.Count; i++)
{
node.ChildNodes[i].Dispose();
}
node.ChildNodes.Clear();
}
}
/// <summary>
/// Gets the actor node.
/// </summary>
@@ -713,6 +756,7 @@ namespace FlaxEditor.Modules
Level.ActorOrderInParentChanged += OnActorOrderInParentChanged;
Level.ActorNameChanged += OnActorNameChanged;
Level.ActorActiveChanged += OnActorActiveChanged;
Level.ActorDestroyChildren += OnActorDestroyChildren;
}
/// <inheritdoc />
@@ -730,6 +774,7 @@ namespace FlaxEditor.Modules
Level.ActorOrderInParentChanged -= OnActorOrderInParentChanged;
Level.ActorNameChanged -= OnActorNameChanged;
Level.ActorActiveChanged -= OnActorActiveChanged;
Level.ActorDestroyChildren -= OnActorDestroyChildren;
// Cleanup graph
Root.Dispose();

View File

@@ -125,6 +125,7 @@ namespace FlaxEditor.Modules
private ContextMenuButton _menuToolsProfilerWindow;
private ContextMenuButton _menuToolsSetTheCurrentSceneViewAsDefault;
private ContextMenuButton _menuToolsTakeScreenshot;
private ContextMenuButton _menuToolsOpenLocalFolder;
private ContextMenuChildMenu _menuWindowApplyWindowLayout;
private ToolStripButton _toolStripSaveAll;
@@ -638,6 +639,7 @@ namespace FlaxEditor.Modules
_menuFileGenerateScriptsProjectFiles = cm.AddButton("Generate scripts project files", inputOptions.GenerateScriptsProject, Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync);
_menuFileRecompileScripts = cm.AddButton("Recompile scripts", inputOptions.RecompileScripts, ScriptsBuilder.Compile);
cm.AddSeparator();
cm.AddButton("New project", NewProject);
cm.AddButton("Open project...", OpenProject);
cm.AddButton("Reload project", ReloadProject);
cm.AddButton("Open project folder", () => FileSystem.ShowFileExplorer(Editor.Instance.GameProject.ProjectFolderPath));
@@ -668,7 +670,7 @@ namespace FlaxEditor.Modules
if (item != null)
Editor.ContentEditing.Open(item);
});
cm.AddButton("Editor Options", () => Editor.Windows.EditorOptionsWin.Show());
cm.AddButton("Editor Options", inputOptions.EditorOptionsWindow, () => Editor.Windows.EditorOptionsWin.Show());
// Scene
MenuScene = MainMenu.AddButton("Scene");
@@ -713,6 +715,7 @@ namespace FlaxEditor.Modules
_menuToolsBuildCSGMesh = cm.AddButton("Build CSG mesh", inputOptions.BuildCSG, Editor.BuildCSG);
_menuToolsBuildNavMesh = cm.AddButton("Build Nav Mesh", inputOptions.BuildNav, Editor.BuildNavMesh);
_menuToolsBuildAllMeshesSDF = cm.AddButton("Build all meshes SDF", inputOptions.BuildSDF, Editor.BuildAllMeshesSDF);
_menuToolsBuildAllMeshesSDF.LinkTooltip("Generates Sign Distance Field texture for all meshes used in loaded scenes. Use with 'F' key pressed to force rebuild SDF for meshes with existing one.");
cm.AddSeparator();
cm.AddButton("Game Cooker", Editor.Windows.GameCookerWin.FocusOrShow);
_menuToolsCancelBuilding = cm.AddButton("Cancel building game", () => GameCooker.Cancel());
@@ -723,6 +726,16 @@ namespace FlaxEditor.Modules
_menuToolsTakeScreenshot = cm.AddButton("Take screenshot", inputOptions.TakeScreenshot, Editor.Windows.TakeScreenshot);
cm.AddSeparator();
cm.AddButton("Plugins", () => Editor.Windows.PluginsWin.Show());
cm.AddSeparator();
var childMenu = cm.AddChildMenu("Open Product Local folder");
childMenu.ContextMenu.AddButton("Editor", () => FileSystem.ShowFileExplorer(Globals.ProductLocalFolder));
_menuToolsOpenLocalFolder = childMenu.ContextMenu.AddButton("Game", () =>
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
GameSettings settings = GameSettings.Load<GameSettings>();
string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
FileSystem.ShowFileExplorer(path);
});
// Window
MenuWindow = MainMenu.AddButton("Window");
@@ -929,6 +942,17 @@ namespace FlaxEditor.Modules
MasterPanel.Offsets = new Margin(0, 0, ToolStrip.Bottom, StatusBar.Height);
}
private void NewProject()
{
// Ask user to create project file
if (FileSystem.ShowSaveFileDialog(Editor.Windows.MainWindow, null, "Project files (*.flaxproj)\0*.flaxproj\0All files (*.*)\0*.*\0", false, "Create project file", out var files))
return;
if (files != null && files.Length > 0)
{
Editor.NewProject(files[0]);
}
}
private void OpenProject()
{
// Ask user to select project file
@@ -1049,6 +1073,10 @@ namespace FlaxEditor.Modules
_menuToolsBuildNavMesh.Enabled = canEdit;
_menuToolsCancelBuilding.Enabled = GameCooker.IsRunning;
_menuToolsSetTheCurrentSceneViewAsDefault.Enabled = Level.ScenesCount > 0;
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
GameSettings settings = GameSettings.Load<GameSettings>();
string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
_menuToolsOpenLocalFolder.Enabled = Directory.Exists(path);
c.PerformLayout();
}

View File

@@ -491,10 +491,15 @@ namespace FlaxEditor.Modules
Editor.LogWarning("Empty panel inside layout.");
p.RemoveIt();
}
else
{
p.CollapseEmptyTabsProxy();
}
}
}
panel.SelectTab(selectedTab);
panel.CollapseEmptyTabsProxy();
}
private static void SaveBounds(XmlWriter writer, Window win)
@@ -896,9 +901,11 @@ namespace FlaxEditor.Modules
if (type.IsAssignableTo(typeof(AssetEditorWindow)))
{
var ctor = type.GetConstructor(new Type[] { typeof(Editor), typeof(AssetItem) });
var assetItem = Editor.ContentDatabase.FindAsset(winData.AssetItemID);
var assetType = assetItem.GetType();
var ctor = type.GetConstructor(new Type[] { typeof(Editor), assetType });
var win = (AssetEditorWindow)ctor.Invoke(new object[] { Editor.Instance, assetItem });
win.Show(winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue);
if (winData.DockState == DockState.Float)
{

View File

@@ -387,6 +387,14 @@ namespace FlaxEditor.Options
[EditorDisplay("Viewport"), EditorOrder(1760)]
public InputBinding ToggleOrthographic = new InputBinding(KeyboardKeys.NumpadDecimal);
[DefaultValue(typeof(InputBinding), "G")]
[EditorDisplay("Viewport"), EditorOrder(1770)]
public InputBinding ToggleGameView = new InputBinding(KeyboardKeys.G);
[DefaultValue(typeof(InputBinding), "P")]
[EditorDisplay("Viewport"), EditorOrder(1770)]
public InputBinding ToggleNavMeshVisibility = new InputBinding(KeyboardKeys.P);
#endregion
#region Debug Views
@@ -571,6 +579,10 @@ namespace FlaxEditor.Options
[EditorDisplay("View Flags"), EditorOrder(3260)]
public InputBinding DebugDraw = new InputBinding(KeyboardKeys.Alpha4, KeyboardKeys.Control, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "None")]
[EditorDisplay("View Flags"), EditorOrder(3270)]
public InputBinding Particles = new InputBinding(KeyboardKeys.None);
#endregion
#region Interface
@@ -650,6 +662,10 @@ namespace FlaxEditor.Options
[EditorDisplay("Windows"), EditorOrder(4020)]
public InputBinding VisualScriptDebuggerWindow = new InputBinding(KeyboardKeys.None);
[DefaultValue(typeof(InputBinding), "Control+Comma")]
[EditorDisplay("Windows"), EditorOrder(4030)]
public InputBinding EditorOptionsWindow = new InputBinding(KeyboardKeys.Comma, KeyboardKeys.Control);
#endregion
#region Node Editors

View File

@@ -150,5 +150,26 @@ namespace FlaxEditor.Options
[DefaultValue(typeof(Color), "0.5,0.5,0.5,1.0")]
[EditorDisplay("Grid"), EditorOrder(310), Tooltip("The color for the viewport grid.")]
public Color ViewportGridColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 1.0f);
/// <summary>
/// Gets or sets the minimum size used for viewport icons.
/// </summary>
[DefaultValue(7.0f), Limit(1.0f, 1000.0f, 5.0f)]
[EditorDisplay("Viewport Icons"), EditorOrder(400)]
public float IconsMinimumSize { get; set; } = 7.0f;
/// <summary>
/// Gets or sets the maximum size used for viewport icons.
/// </summary>
[DefaultValue(30.0f), Limit(1.0f, 1000.0f, 5.0f)]
[EditorDisplay("Viewport Icons"), EditorOrder(410)]
public float IconsMaximumSize { get; set; } = 30.0f;
/// <summary>
/// Gets or sets the distance towards the camera at which the max icon scale will be applied. Set to 0 to disable scaling the icons based on the distance to the camera.
/// </summary>
[DefaultValue(1000.0f), Limit(0.0f, 20000.0f, 5.0f)]
[EditorDisplay("Viewport Icons"), EditorOrder(410)]
public float MaxSizeDistance { get; set; } = 1000.0f;
}
}

View File

@@ -6,6 +6,7 @@
#include "Engine/Core/Log.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Core/Math/Quaternion.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Serialization/JsonTools.h"
#include <ThirdParty/pugixml/pugixml.hpp>
@@ -327,6 +328,7 @@ ProjectInfo* ProjectInfo::Load(const String& path)
}
// Load
PROFILE_MEM(Editor);
auto project = New<ProjectInfo>();
if (project->LoadProject(path))
{

View File

@@ -42,6 +42,7 @@ namespace FlaxEditor.SceneGraph.Actors
if (value is BoxCollider collider)
collider.AutoResize(!_keepLocalOrientation);
}
Presenter.OnModified();
}
}

View File

@@ -555,7 +555,7 @@ namespace FlaxEditor.SceneGraph.Actors
var options = Editor.Instance.Options.Options.General;
if (options.AutoRebuildNavMesh)
{
Navigation.BuildNavMesh(collider.Scene, collider.Box, options.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(collider.Box, options.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -47,6 +47,11 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
/// <summary>
/// Gets the model used by this actor.
/// </summary>
public Model Model => ((StaticModel)Actor).Model;
/// <inheritdoc />
public StaticModelNode(Actor actor)
: base(actor)
@@ -120,12 +125,12 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.OnContextMenu(contextMenu, window);
// Check if every selected node is a primitive
// Check if every selected node is a primitive or has collision asset
var selection = GetSelection(window);
bool autoOptionEnabled = true;
foreach (var node in selection)
{
if (node is StaticModelNode staticModelNode && !staticModelNode.IsPrimitive)
if (node is StaticModelNode staticModelNode && (!staticModelNode.IsPrimitive && GetCollisionData(staticModelNode.Model) == null))
{
autoOptionEnabled = false;
break;
@@ -201,6 +206,54 @@ namespace FlaxEditor.SceneGraph.Actors
return Array.Empty<SceneGraphNode>();
}
private static bool TryCollisionData(Model model, BinaryAssetItem assetItem, out CollisionData collisionData)
{
collisionData = FlaxEngine.Content.Load<CollisionData>(assetItem.ID);
if (collisionData)
{
var options = collisionData.Options;
if (options.Model == model.ID || options.Model == Guid.Empty)
return true;
}
return false;
}
private CollisionData GetCollisionData(Model model)
{
if (model == null)
return null;
// Check if there already is collision data for that model to reuse
var modelItem = (AssetItem)Editor.Instance.ContentDatabase.Find(model.ID);
if (modelItem?.ParentFolder != null)
{
foreach (var child in modelItem.ParentFolder.Children)
{
// Check if there is collision that was made with this model
if (child is BinaryAssetItem b && b.IsOfType<CollisionData>())
{
if (TryCollisionData(model, b, out var collisionData))
return collisionData;
}
// Check if there is an auto-imported collision
if (child is ContentFolder childFolder && childFolder.ShortName == modelItem.ShortName)
{
foreach (var childFolderChild in childFolder.Children)
{
if (childFolderChild is BinaryAssetItem c && c.IsOfType<CollisionData>())
{
if (TryCollisionData(model, c, out var collisionData))
return collisionData;
}
}
}
}
}
return null;
}
private void CreateAuto(StaticModel actor, Spawner spawner, bool singleNode)
{
// Special case for in-built Editor models that can use analytical collision
@@ -243,6 +296,15 @@ namespace FlaxEditor.SceneGraph.Actors
collider.LocalPosition = new Vector3(0, 50.0f, 0);
collider.LocalOrientation = Quaternion.Euler(0, 0, 90.0f);
}
else
{
var collider = new MeshCollider
{
Transform = actor.Transform,
CollisionData = GetCollisionData(model),
};
spawner(collider);
}
}
private void CreateBox(StaticModel actor, Spawner spawner, bool singleNode)

View File

@@ -97,13 +97,16 @@ namespace FlaxEditor.SceneGraph
/// <returns>Hit object or null if there is no intersection at all.</returns>
public SceneGraphNode RayCast(ref Ray ray, ref Ray view, out Real distance, RayCastData.FlagTypes flags = RayCastData.FlagTypes.None)
{
Profiler.BeginEvent("RayCastScene");
var data = new RayCastData
{
Ray = ray,
View = view,
Flags = flags
};
return RayCast(ref data, out distance, out _);
var result = RayCast(ref data, out distance, out _);
Profiler.EndEvent();
return result;
}
/// <summary>
@@ -117,13 +120,16 @@ namespace FlaxEditor.SceneGraph
/// <returns>Hit object or null if there is no intersection at all.</returns>
public SceneGraphNode RayCast(ref Ray ray, ref Ray view, out Real distance, out Vector3 normal, RayCastData.FlagTypes flags = RayCastData.FlagTypes.None)
{
Profiler.BeginEvent("RayCastScene");
var data = new RayCastData
{
Ray = ray,
View = view,
Flags = flags
};
return RayCast(ref data, out distance, out normal);
var result = RayCast(ref data, out distance, out normal);
Profiler.EndEvent();
return result;
}
internal static Quaternion RaycastNormalRotation(ref Vector3 normal)

View File

@@ -27,7 +27,7 @@ namespace FlaxEditor.SceneGraph
/// <summary>
/// The parent node.
/// </summary>
protected SceneGraphNode parentNode;
internal SceneGraphNode parentNode;
/// <summary>
/// Gets the children list.

View File

@@ -13,6 +13,7 @@
#include "Engine/Engine/EngineService.h"
#include "Engine/Platform/Thread.h"
#include "Engine/Threading/IRunnable.h"
#include "Engine/Profiler/ProfilerMemory.h"
void OnAsyncBegin(Thread* thread);
void OnAsyncEnd();
@@ -232,6 +233,8 @@ void OnAsyncEnd()
bool CodeEditingManagerService::Init()
{
PROFILE_MEM(Editor);
// Try get editors
#if USE_VISUAL_STUDIO_DTE
VisualStudioEditor::FindEditors(&CodeEditors);

View File

@@ -23,6 +23,7 @@
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/Script.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Level/Level.h"
#include "FlaxEngine.Gen.h"
@@ -77,7 +78,7 @@ namespace ScriptsBuilderImpl
void onScriptsReloadEnd();
void onScriptsLoaded();
void GetClassName(const StringAnsi& fullname, StringAnsi& className);
void GetClassName(const StringAnsiView fullname, StringAnsi& className);
void onCodeEditorAsyncOpenBegin()
{
@@ -272,7 +273,7 @@ bool ScriptsBuilder::GenerateProject(const StringView& customArgs)
return RunBuildTool(args);
}
void ScriptsBuilderImpl::GetClassName(const StringAnsi& fullname, StringAnsi& className)
void ScriptsBuilderImpl::GetClassName(const StringAnsiView fullname, StringAnsi& className)
{
const auto lastDotIndex = fullname.FindLast('.');
if (lastDotIndex != -1)
@@ -413,6 +414,7 @@ void ScriptsBuilder::GetBinariesConfiguration(const Char*& target, const Char*&
bool ScriptsBuilderImpl::compileGameScriptsAsyncInner()
{
PROFILE_MEM(Editor);
LOG(Info, "Starting scripts compilation...");
CallEvent(EventType::CompileStarted);
@@ -519,6 +521,8 @@ void ScriptsBuilderImpl::onEditorAssemblyUnloading(MAssembly* assembly)
bool ScriptsBuilderImpl::compileGameScriptsAsync()
{
PROFILE_MEM(Editor);
// Start
{
ScopeLock scopeLock(_locker);
@@ -562,6 +566,7 @@ bool ScriptsBuilderService::Init()
// Check flag
if (_isInited)
return false;
PROFILE_MEM(Editor);
_isInited = true;
// Link for Editor assembly unload event to clear cached Internal_OnCompilationEnd to prevent errors
@@ -659,6 +664,9 @@ bool ScriptsBuilderService::Init()
void ScriptsBuilderService::Update()
{
PROFILE_CPU();
PROFILE_MEM(Editor);
// Send compilation events
{
ScopeLock scopeLock(_compileEventsLocker);

View File

@@ -406,6 +406,8 @@ namespace FlaxEngine.Utilities
{
if (type == ScriptType.Null)
return null;
if (type.BaseType == null)
return type.Type;
while (type.Type == null)
type = type.BaseType;
return type.Type;

View File

@@ -229,20 +229,20 @@ namespace FlaxEditor.Surface
}
/// <inheritdoc />
protected override void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, Box startBox)
protected override void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, List<Box> startBoxes)
{
// Check if show additional nodes in the current surface context
if (activeCM != _cmStateMachineMenu)
{
_nodesCache.Get(activeCM);
base.OnShowPrimaryMenu(activeCM, location, startBox);
base.OnShowPrimaryMenu(activeCM, location, startBoxes);
activeCM.VisibleChanged += OnActiveContextMenuVisibleChanged;
}
else
{
base.OnShowPrimaryMenu(activeCM, location, startBox);
base.OnShowPrimaryMenu(activeCM, location, startBoxes);
}
}

View File

@@ -233,6 +233,8 @@ namespace FlaxEditor.Surface.Archetypes
public BlendPointsEditor(Animation.MultiBlend node, bool is2D, float x, float y, float width, float height)
: base(x, y, width, height)
{
AutoFocus = true;
_node = node;
_is2D = is2D;
}

View File

@@ -726,7 +726,7 @@ namespace FlaxEditor.Surface.Archetypes
private void OnSurfaceMouseUp(ref Float2 mouse, MouseButton buttons, ref bool handled)
{
if (handled)
if (handled || Surface.Context != Context)
return;
// Check click over the connection
@@ -751,7 +751,7 @@ namespace FlaxEditor.Surface.Archetypes
private void OnSurfaceMouseDoubleClick(ref Float2 mouse, MouseButton buttons, ref bool handled)
{
if (handled)
if (handled || Surface.Context != Context)
return;
// Check double click over the connection

View File

@@ -6,6 +6,7 @@ using FlaxEditor.Scripting;
using FlaxEditor.Surface.Elements;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Surface.Archetypes
{
@@ -123,7 +124,8 @@ namespace FlaxEditor.Surface.Archetypes
case MaterialDomain.Particle:
case MaterialDomain.Deformable:
{
bool isNotUnlit = info.ShadingModel != MaterialShadingModel.Unlit;
bool isNotUnlit = info.ShadingModel != MaterialShadingModel.Unlit && info.ShadingModel != MaterialShadingModel.CustomLit;
bool isOpaque = info.BlendMode == MaterialBlendMode.Opaque;
bool withTess = info.TessellationMode != TessellationMethod.None;
GetBox(MaterialNodeBoxes.Color).IsActive = isNotUnlit;
@@ -134,8 +136,8 @@ namespace FlaxEditor.Surface.Archetypes
GetBox(MaterialNodeBoxes.Roughness).IsActive = isNotUnlit;
GetBox(MaterialNodeBoxes.AmbientOcclusion).IsActive = isNotUnlit;
GetBox(MaterialNodeBoxes.Normal).IsActive = isNotUnlit;
GetBox(MaterialNodeBoxes.Opacity).IsActive = info.ShadingModel == MaterialShadingModel.Subsurface || info.ShadingModel == MaterialShadingModel.Foliage || info.BlendMode != MaterialBlendMode.Opaque;
GetBox(MaterialNodeBoxes.Refraction).IsActive = info.BlendMode != MaterialBlendMode.Opaque;
GetBox(MaterialNodeBoxes.Opacity).IsActive = info.ShadingModel == MaterialShadingModel.Subsurface || info.ShadingModel == MaterialShadingModel.Foliage || !isOpaque;
GetBox(MaterialNodeBoxes.Refraction).IsActive = !isOpaque;
GetBox(MaterialNodeBoxes.PositionOffset).IsActive = true;
GetBox(MaterialNodeBoxes.TessellationMultiplier).IsActive = withTess;
GetBox(MaterialNodeBoxes.WorldDisplacement).IsActive = withTess;
@@ -260,6 +262,93 @@ namespace FlaxEditor.Surface.Archetypes
}
}
#if false // TODO: finish code editor based on RichTextBoxBase with text block parsing for custom styling
internal sealed class CustomCodeTextBox : RichTextBoxBase
{
protected override void OnParseTextBlocks()
{
base.OnParseTextBlocks();
// Single block for a whole text
// TODO: implement code parsing with HLSL syntax
var font = Style.Current.FontMedium;
var style = new TextBlockStyle
{
Font = new FontReference(font),
Color = Style.Current.Foreground,
BackgroundSelectedBrush = new SolidColorBrush(Style.Current.BackgroundSelected),
};
_textBlocks.Clear();
_textBlocks.Add(new TextBlock
{
Range = new TextRange
{
StartIndex = 0,
EndIndex = TextLength,
},
Style = style,
Bounds = new Rectangle(Float2.Zero, font.MeasureText(Text)),
});
}
#else
internal sealed class CustomCodeTextBox : TextBox
{
#endif
public override void Draw()
{
base.Draw();
// Draw border
if (!IsFocused)
Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.BorderNormal);
}
}
internal sealed class CustomCodeNode : ResizableSurfaceNode
{
private CustomCodeTextBox _textBox;
public CustomCodeNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
_sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array
Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize), size;
if (nodeArch.TypeID == 8)
{
pos += new Float2(60, 0);
size = new Float2(172, 200);
}
else
{
pos += new Float2(0, 40);
size = new Float2(300, 200);
}
_textBox = new CustomCodeTextBox
{
IsMultiline = true,
Location = pos,
Size = size,
Parent = this,
AnchorMax = Float2.One,
};
_textBox.EditEnd += () => SetValue(0, _textBox.Text);
}
public override void OnSurfaceLoaded(SurfaceNodeActions action)
{
base.OnSurfaceLoaded(action);
_textBox.Text = (string)Values[0];
}
public override void OnValuesChanged()
{
base.OnValuesChanged();
_textBox.Text = (string)Values[0];
}
}
internal enum MaterialTemplateInputsMapping
{
/// <summary>
@@ -410,13 +499,15 @@ namespace FlaxEditor.Surface.Archetypes
new NodeArchetype
{
TypeID = 8,
Create = (id, context, arch, groupArch) => new CustomCodeNode(id, context, arch, groupArch),
Title = "Custom Code",
Description = "Custom HLSL shader code expression",
Flags = NodeFlags.MaterialGraph,
Size = new Float2(300, 200),
DefaultValues = new object[]
{
"// Here you can add HLSL code\nOutput0 = Input0;"
"// Here you can add HLSL code\nOutput0 = Input0;",
new Float2(300, 200),
},
Elements = new[]
{
@@ -433,8 +524,6 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Output(1, "Output1", typeof(Float4), 9),
NodeElementArchetype.Factory.Output(2, "Output2", typeof(Float4), 10),
NodeElementArchetype.Factory.Output(3, "Output3", typeof(Float4), 11),
NodeElementArchetype.Factory.TextBox(60, 0, 175, 200, 0),
}
},
new NodeArchetype
@@ -874,6 +963,7 @@ namespace FlaxEditor.Surface.Archetypes
new NodeArchetype
{
TypeID = 38,
Create = (id, context, arch, groupArch) => new CustomCodeNode(id, context, arch, groupArch),
Title = "Custom Global Code",
Description = "Custom global HLSL shader code expression (placed before material shader code). Can contain includes to shader utilities or declare functions to reuse later.",
Flags = NodeFlags.MaterialGraph,
@@ -883,6 +973,7 @@ namespace FlaxEditor.Surface.Archetypes
"// Here you can add HLSL code\nfloat4 GetCustomColor()\n{\n\treturn float4(1, 0, 0, 1);\n}",
true,
(int)MaterialTemplateInputsMapping.Utilities,
new Float2(300, 240),
},
Elements = new[]
{
@@ -890,7 +981,6 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Text(20, 0, "Enabled"),
NodeElementArchetype.Factory.Text(0, 20, "Location"),
NodeElementArchetype.Factory.Enum(50, 20, 120, 2, typeof(MaterialTemplateInputsMapping)),
NodeElementArchetype.Factory.TextBox(0, 40, 300, 200, 0),
}
},
new NodeArchetype

View File

@@ -1100,6 +1100,27 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116)
}
},
new NodeArchetype
{
TypeID = 5,
Create = (id, context, arch, groupArch) => new SurfaceNodeParamsSet(id, context, arch, groupArch),
Title = "Set Parameter",
Description = "Parameter value setter invoked when the animation pose is evaluated (output pose comes from input)",
Flags = NodeFlags.AnimGraph,
Size = new Float2(140, 40),
DefaultValues = new object[]
{
Guid.Empty,
null
},
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 0),
NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 2),
NodeElementArchetype.Factory.Input(1, string.Empty, true, ScriptType.Null, 1, 1),
NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116)
}
},
};
}
}

View File

@@ -23,11 +23,14 @@ namespace FlaxEditor.Surface.Archetypes
TextureGroup = 4,
}
internal class SampleTextureNode : SurfaceNode
internal class TextureSamplerNode : SurfaceNode
{
private ComboBox _textureGroupPicker;
protected int _samplerTypeValueIndex = -1;
protected int _textureGroupValueIndex = -1;
protected int _level = 5;
public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
protected TextureSamplerNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
}
@@ -48,13 +51,13 @@ namespace FlaxEditor.Surface.Archetypes
private void UpdateUI()
{
if ((int)Values[0] == (int)CommonSamplerType.TextureGroup)
if ((int)Values[_samplerTypeValueIndex] == (int)CommonSamplerType.TextureGroup)
{
if (_textureGroupPicker == null)
{
_textureGroupPicker = new ComboBox
{
Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * 5),
Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * _level),
Width = 100,
Parent = this,
};
@@ -71,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes
_textureGroupPicker.Visible = true;
}
_textureGroupPicker.SelectedIndexChanged -= OnSelectedTextureGroupChanged;
_textureGroupPicker.SelectedIndex = (int)Values[2];
_textureGroupPicker.SelectedIndex = (int)Values[_textureGroupValueIndex];
_textureGroupPicker.SelectedIndexChanged += OnSelectedTextureGroupChanged;
}
else if (_textureGroupPicker != null)
@@ -83,7 +86,39 @@ namespace FlaxEditor.Surface.Archetypes
private void OnSelectedTextureGroupChanged(ComboBox comboBox)
{
SetValue(2, _textureGroupPicker.SelectedIndex);
SetValue(_textureGroupValueIndex, _textureGroupPicker.SelectedIndex);
}
}
internal class SampleTextureNode : TextureSamplerNode
{
public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
_samplerTypeValueIndex = 0;
_textureGroupValueIndex = 2;
}
}
internal class TriplanarSampleTextureNode : TextureSamplerNode
{
public TriplanarSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
_samplerTypeValueIndex = 3;
_textureGroupValueIndex = 5;
_level = 5;
}
}
internal class ProceduralSampleTextureNode : TextureSamplerNode
{
public ProceduralSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
_samplerTypeValueIndex = 0;
_textureGroupValueIndex = 2;
_level = 4;
}
}
@@ -280,9 +315,9 @@ namespace FlaxEditor.Surface.Archetypes
ConnectionsHints = ConnectionsHint.Vector,
DefaultValues = new object[]
{
0,
-1.0f,
0,
(int)CommonSamplerType.LinearClamp, // Sampler
-1.0f, // Level
0, // Texture Group
},
Elements = new[]
{
@@ -402,6 +437,7 @@ namespace FlaxEditor.Surface.Archetypes
new NodeArchetype
{
TypeID = 16,
Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch),
Title = "Triplanar Texture",
Description = "Projects a texture using world-space coordinates with triplanar mapping.",
Flags = NodeFlags.MaterialGraph,
@@ -411,8 +447,9 @@ namespace FlaxEditor.Surface.Archetypes
Float3.One, // Scale
1.0f, // Blend
Float2.Zero, // Offset
2, // Sampler
(int)CommonSamplerType.LinearWrap, // Sampler
false, // Local
0, // Texture Group
},
Elements = new[]
{
@@ -430,17 +467,17 @@ namespace FlaxEditor.Surface.Archetypes
new NodeArchetype
{
TypeID = 17,
Create = (id, context, arch, groupArch) => new SampleTextureNode(id, context, arch, groupArch),
Create = (id, context, arch, groupArch) => new ProceduralSampleTextureNode(id, context, arch, groupArch),
Title = "Procedural Sample Texture",
Description = "Samples a texture to create a more natural look with less obvious tiling.",
Flags = NodeFlags.MaterialGraph,
Size = new Float2(240, 110),
Size = new Float2(240, 130),
ConnectionsHints = ConnectionsHint.Vector,
DefaultValues = new object[]
{
2,
-1.0f,
0,
(int)CommonSamplerType.LinearWrap, // Sampler
-1.0f, // Level
0, // Texture Group
},
Elements = new[]
{
@@ -448,8 +485,8 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Input(1, "UVs", true, null, 1),
NodeElementArchetype.Factory.Input(2, "Offset", true, typeof(Float2), 3),
NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 4),
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"),
NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4, 100, 0, typeof(CommonSamplerType))
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 3, "Sampler"),
NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 3, 100, 0, typeof(CommonSamplerType))
}
},
new NodeArchetype
@@ -469,6 +506,7 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 23,
Title = "Triplanar Normal Map",
Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch),
Description = "Projects a normal map texture using world-space coordinates with triplanar mapping.",
Flags = NodeFlags.MaterialGraph,
Size = new Float2(280, 100),
@@ -477,8 +515,9 @@ namespace FlaxEditor.Surface.Archetypes
Float3.One, // Scale
1.0f, // Blend
Float2.Zero, // Offset
2, // Sampler
(int)CommonSamplerType.LinearWrap, // Sampler
false, // Local
0, // Texture Group
},
Elements = new[]
{

View File

@@ -453,7 +453,7 @@ namespace FlaxEditor.Surface.Archetypes
}
}
private class CurveNode<T> : SurfaceNode where T : struct
private class CurveNode<T> : ResizableSurfaceNode where T : struct
{
private BezierCurveEditor<T> _curve;
private bool _isSavingCurve;
@@ -467,7 +467,7 @@ namespace FlaxEditor.Surface.Archetypes
Create = (id, context, arch, groupArch) => new CurveNode<T>(id, context, arch, groupArch),
Description = "An animation spline represented by a set of keyframes, each representing an endpoint of a Bezier curve.",
Flags = NodeFlags.AllGraphs,
Size = new Float2(400, 180.0f),
Size = new Float2(400, 180),
DefaultValues = new object[]
{
// Keyframes count
@@ -491,6 +491,8 @@ namespace FlaxEditor.Surface.Archetypes
0.0f, zero, zero, zero,
0.0f, zero, zero, zero,
0.0f, zero, zero, zero,
new Float2(400, 180),
},
Elements = new[]
{
@@ -504,30 +506,52 @@ namespace FlaxEditor.Surface.Archetypes
public CurveNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
_sizeValueIndex = 29; // Index of the Size stored in Values array
}
/// <inheritdoc />
public override void OnLoaded(SurfaceNodeActions action)
{
base.OnLoaded(action);
// Create curve editor
var upperLeft = GetBox(0).BottomLeft;
var upperRight = GetBox(1).BottomRight;
float curveMargin = 20.0f;
_curve = new BezierCurveEditor<T>
{
MaxKeyframes = 7,
Bounds = new Rectangle(upperLeft + new Float2(curveMargin, 10.0f), upperRight.X - upperLeft.X - curveMargin * 2.0f, 140.0f),
Parent = this
Parent = this,
AnchorMax = Float2.One,
};
_curve.Edited += OnCurveEdited;
_curve.UnlockChildrenRecursive();
_curve.PerformLayout();
// Sync keyframes
UpdateCurveKeyframes();
}
/// <inheritdoc />
public override void OnSurfaceLoaded(SurfaceNodeActions action)
{
base.OnSurfaceLoaded(action);
// Ensure the whole curve is shown
_curve.ShowWholeCurve();
}
public override void OnValuesChanged()
{
base.OnValuesChanged();
if (!_isSavingCurve)
{
UpdateCurveKeyframes();
}
}
private void OnCurveEdited()
{
if (_isSavingCurve)
@@ -553,17 +577,6 @@ namespace FlaxEditor.Surface.Archetypes
_isSavingCurve = false;
}
/// <inheritdoc />
public override void OnValuesChanged()
{
base.OnValuesChanged();
if (!_isSavingCurve)
{
UpdateCurveKeyframes();
}
}
private void UpdateCurveKeyframes()
{
var count = (int)Values[0];
@@ -1390,7 +1403,7 @@ namespace FlaxEditor.Surface.Archetypes
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, "Time", typeof(float), 0),
NodeElementArchetype.Factory.Output(1, "Unscaled Time", typeof(float), 1),
NodeElementArchetype.Factory.Output(1, "Scaled Time", typeof(float), 1),
}
},
new NodeArchetype
@@ -1575,7 +1588,7 @@ namespace FlaxEditor.Surface.Archetypes
DefaultValues = new object[]
{
Guid.Empty,
string.Empty
string.Empty,
},
Elements = new[]
{

View File

@@ -2,11 +2,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Runtime.Serialization.Formatters.Binary;
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.GUI.ContextMenu;
@@ -18,6 +15,7 @@ namespace FlaxEditor.Surface
class AttributesEditor : ContextMenuBase
{
private CustomEditorPresenter _presenter;
private Proxy _proxy;
private byte[] _oldData;
private class Proxy
@@ -72,11 +70,11 @@ namespace FlaxEditor.Surface
/// Initializes a new instance of the <see cref="AttributesEditor"/> class.
/// </summary>
/// <param name="attributes">The attributes list to edit.</param>
/// <param name="attributeType">The allowed attribute types to use.</param>
public AttributesEditor(Attribute[] attributes, IList<Type> attributeType)
/// <param name="attributeTypes">The allowed attribute types to use.</param>
public AttributesEditor(Attribute[] attributes, IList<Type> attributeTypes)
{
// Context menu dimensions
const float width = 340.0f;
const float width = 375.0f;
const float height = 370.0f;
Size = new Float2(width, height);
@@ -88,61 +86,68 @@ namespace FlaxEditor.Surface
Parent = this
};
// Buttons
float buttonsWidth = (width - 16.0f) * 0.5f;
// Ok and Cancel Buttons
float buttonsWidth = (width - 12.0f) * 0.5f;
float buttonsHeight = 20.0f;
var cancelButton = new Button(4.0f, title.Bottom + 4.0f, buttonsWidth, buttonsHeight)
var okButton = new Button(4.0f, Bottom - 4.0f - buttonsHeight, buttonsWidth, buttonsHeight)
{
Text = "Ok",
Parent = this
};
okButton.Clicked += OnOkButtonClicked;
var cancelButton = new Button(okButton.Right + 4.0f, okButton.Y, buttonsWidth, buttonsHeight)
{
Text = "Cancel",
Parent = this
};
cancelButton.Clicked += Hide;
var okButton = new Button(cancelButton.Right + 4.0f, cancelButton.Y, buttonsWidth, buttonsHeight)
{
Text = "OK",
Parent = this
};
okButton.Clicked += OnOkButtonClicked;
// Actual panel
// Actual panel used to display attributes
var panel1 = new Panel(ScrollBars.Vertical)
{
Bounds = new Rectangle(0, okButton.Bottom + 4.0f, width, height - okButton.Bottom - 2.0f),
Bounds = new Rectangle(0, title.Bottom + 4.0f, width, height - buttonsHeight - title.Height - 14.0f),
Parent = this
};
var editor = new CustomEditorPresenter(null);
editor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
editor.Panel.IsScrollable = true;
editor.Panel.Parent = panel1;
editor.Panel.Tag = attributeType;
editor.Panel.Tag = attributeTypes;
_presenter = editor;
// Cache 'previous' state to check if attributes were edited after operation
_oldData = SurfaceMeta.GetAttributesData(attributes);
editor.Select(new Proxy
_proxy = new Proxy
{
Value = attributes,
});
};
editor.Select(_proxy);
_presenter.Modified += OnPresenterModified;
OnPresenterModified();
}
private void OnPresenterModified()
{
if (_proxy.Value.Length == 0)
{
var label = _presenter.Label("No attributes.\nPress the \"+\" button to add a new one and then select an attribute type using the \"Type\" dropdown.", TextAlignment.Center);
label.Label.Wrapping = TextWrapping.WrapWords;
label.Control.Height = 35f;
label.Label.Margin = new Margin(10f);
label.Label.TextColor = label.Label.TextColorHighlighted = Style.Current.ForegroundGrey;
}
}
private void OnOkButtonClicked()
{
var newValue = ((Proxy)_presenter.Selection[0]).Value;
for (int i = 0; i < newValue.Length; i++)
{
if (newValue[i] == null)
{
MessageBox.Show("One of the attributes is null. Please set it to the valid object.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
newValue = newValue.Where(v => v != null).ToArray();
var newData = SurfaceMeta.GetAttributesData(newValue);
if (!_oldData.SequenceEqual(newData))
{
Edited?.Invoke(newValue);
}
Hide();
}
@@ -183,7 +188,9 @@ namespace FlaxEditor.Surface
{
_presenter = null;
_oldData = null;
_proxy = null;
Edited = null;
_presenter.Modified -= OnPresenterModified;
base.OnDestroy();
}

View File

@@ -101,12 +101,12 @@ namespace FlaxEditor.Surface
}
/// <inheritdoc />
protected override void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, Box startBox)
protected override void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, List<Box> startBoxes)
{
activeCM.ShowExpanded = true;
_nodesCache.Get(activeCM);
base.OnShowPrimaryMenu(activeCM, location, startBox);
base.OnShowPrimaryMenu(activeCM, location, startBoxes);
activeCM.VisibleChanged += OnActiveContextMenuVisibleChanged;
}

View File

@@ -24,8 +24,8 @@ namespace FlaxEditor.Surface.ContextMenu
/// Visject context menu item clicked delegate.
/// </summary>
/// <param name="clickedItem">The item that was clicked</param>
/// <param name="selectedBox">The currently user-selected box. Can be null.</param>
public delegate void ItemClickedDelegate(VisjectCMItem clickedItem, Elements.Box selectedBox);
/// <param name="selectedBoxes">The currently user-selected boxes. Can be empty/ null.</param>
public delegate void ItemClickedDelegate(VisjectCMItem clickedItem, List<Elements.Box> selectedBoxes);
/// <summary>
/// Visject Surface node archetype spawn ability checking delegate.
@@ -53,7 +53,7 @@ namespace FlaxEditor.Surface.ContextMenu
private Panel _panel1;
private VerticalPanel _groupsPanel;
private readonly ParameterGetterDelegate _parametersGetter;
private Elements.Box _selectedBox;
private List<Elements.Box> _selectedBoxes = new List<Elements.Box>();
private NodeArchetype _parameterGetNodeArchetype;
private NodeArchetype _parameterSetNodeArchetype;
@@ -411,7 +411,8 @@ namespace FlaxEditor.Surface.ContextMenu
if (!IsLayoutLocked)
{
group.UnlockChildrenRecursive();
if (_contextSensitiveSearchEnabled && _selectedBox != null)
// TODO: Improve filtering to be based on boxes with the most common things instead of first box
if (_contextSensitiveSearchEnabled && _selectedBoxes.Count > 0 && _selectedBoxes[0] != null)
UpdateFilters();
else
SortGroups();
@@ -423,9 +424,10 @@ namespace FlaxEditor.Surface.ContextMenu
OnSearchFilterChanged();
}
}
else if (_contextSensitiveSearchEnabled)
else if (_contextSensitiveSearchEnabled && _selectedBoxes.Count > 0)
{
group.EvaluateVisibilityWithBox(_selectedBox);
// TODO: Filtering could be improved here as well
group.EvaluateVisibilityWithBox(_selectedBoxes[0]);
}
Profiler.EndEvent();
@@ -460,8 +462,8 @@ namespace FlaxEditor.Surface.ContextMenu
Parent = group
};
}
if (_contextSensitiveSearchEnabled)
group.EvaluateVisibilityWithBox(_selectedBox);
if (_contextSensitiveSearchEnabled && _selectedBoxes.Count > 0)
group.EvaluateVisibilityWithBox(_selectedBoxes[0]);
group.SortChildren();
if (ShowExpanded)
group.Open(false);
@@ -474,7 +476,7 @@ namespace FlaxEditor.Surface.ContextMenu
if (!isLayoutLocked)
{
if (_contextSensitiveSearchEnabled && _selectedBox != null)
if (_contextSensitiveSearchEnabled && _selectedBoxes.Count != 0 && _selectedBoxes[0] != null)
UpdateFilters();
else
SortGroups();
@@ -583,7 +585,7 @@ namespace FlaxEditor.Surface.ContextMenu
private void UpdateFilters()
{
if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBox == null)
if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBoxes[0] == null)
{
ResetView();
Profiler.EndEvent();
@@ -592,7 +594,7 @@ namespace FlaxEditor.Surface.ContextMenu
// Update groups
LockChildrenRecursive();
var contextSensitiveSelectedBox = _contextSensitiveSearchEnabled ? _selectedBox : null;
var contextSensitiveSelectedBox = _contextSensitiveSearchEnabled && _selectedBoxes.Count > 0 ? _selectedBoxes[0] : null;
for (int i = 0; i < _groups.Count; i++)
{
_groups[i].UpdateFilter(_searchBox.Text, contextSensitiveSelectedBox);
@@ -640,7 +642,7 @@ namespace FlaxEditor.Surface.ContextMenu
public void OnClickItem(VisjectCMItem item)
{
Hide();
ItemClicked?.Invoke(item, _selectedBox);
ItemClicked?.Invoke(item, _selectedBoxes);
}
/// <summary>
@@ -666,12 +668,12 @@ namespace FlaxEditor.Surface.ContextMenu
for (int i = 0; i < _groups.Count; i++)
{
_groups[i].ResetView();
if (_contextSensitiveSearchEnabled)
_groups[i].EvaluateVisibilityWithBox(_selectedBox);
if (_contextSensitiveSearchEnabled && _selectedBoxes.Count > 0)
_groups[i].EvaluateVisibilityWithBox(_selectedBoxes[0]);
}
UnlockChildrenRecursive();
if (_contextSensitiveSearchEnabled && _selectedBox != null)
if (_contextSensitiveSearchEnabled && _selectedBoxes.Count > 0 && _selectedBoxes[0] != null)
UpdateFilters();
else
SortGroups();
@@ -772,10 +774,10 @@ namespace FlaxEditor.Surface.ContextMenu
/// </summary>
/// <param name="parent">Parent control to attach to it.</param>
/// <param name="location">Popup menu origin location in parent control coordinates.</param>
/// <param name="startBox">The currently selected box that the new node will get connected to. Can be null</param>
public void Show(Control parent, Float2 location, Elements.Box startBox)
/// <param name="startBoxes">The currently selected boxes that the new node will get connected to. Can be empty/ null</param>
public void Show(Control parent, Float2 location, List<Elements.Box> startBoxes)
{
_selectedBox = startBox;
_selectedBoxes = startBoxes;
base.Show(parent, location);
}

View File

@@ -544,35 +544,39 @@ namespace FlaxEditor.Surface.Elements
public override void OnMouseLeave()
{
if (_originalTooltipText != null)
{
TooltipText = _originalTooltipText;
}
if (_isMouseDown)
{
_isMouseDown = false;
if (Surface.CanEdit)
{
if (!IsOutput && HasSingleConnection)
if (IsOutput && Input.GetKey(KeyboardKeys.Control))
{
var connectedBox = Connections[0];
List<Box> connectedBoxes = new List<Box>(Connections);
for (int i = 0; i < connectedBoxes.Count; i++)
{
BreakConnection(connectedBoxes[i]);
Surface.ConnectingStart(connectedBoxes[i], true);
}
}
else if (!IsOutput && HasSingleConnection)
{
var otherBox = Connections[0];
if (Surface.Undo != null && Surface.Undo.Enabled)
{
var action = new ConnectBoxesAction((InputBox)this, (OutputBox)connectedBox, false);
BreakConnection(connectedBox);
var action = new ConnectBoxesAction((InputBox)this, (OutputBox)otherBox, false);
BreakConnection(otherBox);
action.End();
Surface.AddBatchedUndoAction(action);
Surface.MarkAsEdited();
}
else
{
BreakConnection(connectedBox);
}
Surface.ConnectingStart(connectedBox);
BreakConnection(otherBox);
Surface.ConnectingStart(otherBox);
}
else
{
Surface.ConnectingStart(this);
}
}
}
base.OnMouseLeave();

View File

@@ -64,6 +64,7 @@ namespace FlaxEditor.Surface.Elements
{
ParentNode = parentNode;
Archetype = archetype;
AutoFocus = true;
var back = Style.Current.TextBoxBackground;
var grayOutFactor = 0.6f;

View File

@@ -0,0 +1,182 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Surface
{
/// <summary>
/// Visject Surface node control that cna be resized.
/// </summary>
/// <seealso cref="SurfaceNode" />
[HideInEditor]
public class ResizableSurfaceNode : SurfaceNode
{
private Float2 _startResizingSize;
private Float2 _startResizingCornerOffset;
/// <summary>
/// Indicates whether the node is currently being resized.
/// </summary>
protected bool _isResizing;
/// <summary>
/// Index of the Float2 value in the node values list to store node size.
/// </summary>
protected int _sizeValueIndex = -1;
/// <summary>
/// Minimum node size.
/// </summary>
protected Float2 _sizeMin = new Float2(240, 160);
/// <summary>
/// Node resizing rectangle bounds.
/// </summary>
protected Rectangle _resizeButtonRect;
private Float2 SizeValue
{
get => (Float2)Values[_sizeValueIndex];
set => SetValue(_sizeValueIndex, value, false);
}
/// <inheritdoc />
public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
}
/// <inheritdoc />
public override bool CanSelect(ref Float2 location)
{
return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
}
/// <inheritdoc />
public override void OnSurfaceLoaded(SurfaceNodeActions action)
{
// Reapply the curve node size
var size = SizeValue;
if (Surface != null && Surface.GridSnappingEnabled)
size = Surface.SnapToGrid(size, true);
Resize(size.X, size.Y);
base.OnSurfaceLoaded(action);
}
/// <inheritdoc />
public override void OnValuesChanged()
{
base.OnValuesChanged();
var size = SizeValue;
Resize(size.X, size.Y);
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
if (Surface.CanEdit)
{
var style = Style.Current;
if (_isResizing)
{
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
}
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
}
}
/// <inheritdoc />
public override void OnLostFocus()
{
if (_isResizing)
EndResizing();
base.OnLostFocus();
}
/// <inheritdoc />
public override void OnEndMouseCapture()
{
if (_isResizing)
EndResizing();
base.OnEndMouseCapture();
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
{
// Start resizing
_isResizing = true;
_startResizingSize = Size;
_startResizingCornerOffset = Size - location;
StartMouseCapture();
Cursor = CursorType.SizeNWSE;
return true;
}
return false;
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
if (_isResizing)
{
var emptySize = CalculateNodeSize(0, 0);
var size = Float2.Max(location - emptySize + _startResizingCornerOffset, _sizeMin);
Resize(size.X, size.Y);
}
else
{
base.OnMouseMove(location);
}
}
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && _isResizing)
{
EndResizing();
return true;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
protected override void UpdateRectangles()
{
base.UpdateRectangles();
const float buttonMargin = Constants.NodeCloseButtonMargin;
const float buttonSize = Constants.NodeCloseButtonSize;
_resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize);
}
private void EndResizing()
{
Cursor = CursorType.Default;
EndMouseCapture();
_isResizing = false;
if (_startResizingSize != Size)
{
var emptySize = CalculateNodeSize(0, 0);
SizeValue = Size - emptySize;
Surface.MarkAsEdited(false);
}
}
}
}

View File

@@ -14,18 +14,11 @@ namespace FlaxEditor.Surface
/// </summary>
/// <seealso cref="SurfaceNode" />
[HideInEditor]
public class SurfaceComment : SurfaceNode
public class SurfaceComment : ResizableSurfaceNode
{
private Rectangle _colorButtonRect;
private Rectangle _resizeButtonRect;
private Float2 _startResizingSize;
private readonly TextBox _renameTextBox;
/// <summary>
/// True if sizing tool is in use.
/// </summary>
protected bool _isResizing;
/// <summary>
/// True if rename textbox is active in order to rename comment
/// </summary>
@@ -52,12 +45,6 @@ namespace FlaxEditor.Surface
set => SetValue(1, value, false);
}
private Float2 SizeValue
{
get => (Float2)Values[2];
set => SetValue(2, value, false);
}
private int OrderValue
{
get => (int)Values[3];
@@ -68,6 +55,8 @@ namespace FlaxEditor.Surface
public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
_sizeValueIndex = 2; // Index of the Size stored in Values array
_sizeMin = new Float2(140.0f, Constants.NodeHeaderSize);
_renameTextBox = new TextBox(false, 0, 0, Width)
{
Height = Constants.NodeHeaderSize,
@@ -86,10 +75,6 @@ namespace FlaxEditor.Surface
// Read node data
Title = TitleValue;
Color = ColorValue;
var size = SizeValue;
if (Surface != null && Surface.GridSnappingEnabled)
size = Surface.SnapToGrid(size, true);
Size = size;
// Order
// Backwards compatibility - When opening with an older version send the old comments to the back
@@ -126,27 +111,6 @@ namespace FlaxEditor.Surface
// Read node data
Title = TitleValue;
Color = ColorValue;
Size = SizeValue;
}
private void EndResizing()
{
// Clear state
_isResizing = false;
if (_startResizingSize != Size)
{
SizeValue = Size;
Surface.MarkAsEdited(false);
}
EndMouseCapture();
}
/// <inheritdoc />
public override bool CanSelect(ref Float2 location)
{
return _headerRect.MakeOffsetted(Location).Contains(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
}
/// <inheritdoc />
@@ -158,6 +122,8 @@ namespace FlaxEditor.Surface
/// <inheritdoc />
protected override void UpdateRectangles()
{
base.UpdateRectangles();
const float headerSize = Constants.NodeHeaderSize;
const float buttonMargin = Constants.NodeCloseButtonMargin;
const float buttonSize = Constants.NodeCloseButtonSize;
@@ -214,22 +180,22 @@ namespace FlaxEditor.Surface
if (!_isRenaming)
Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center);
// Close button
Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
// Color button
Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
// Check if is resizing
if (_isResizing)
if (Surface.CanEdit)
{
// Draw overlay
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
}
// Close button
Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
// Resize button
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
// Color button
Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
// Resize button
if (_isResizing)
{
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
}
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
}
// Selection outline
if (_isSelected)
@@ -244,88 +210,28 @@ namespace FlaxEditor.Surface
/// <inheritdoc />
protected override Float2 CalculateNodeSize(float width, float height)
{
return Size;
// No margins or headers
return new Float2(width, height);
}
/// <inheritdoc />
public override void OnLostFocus()
{
// Check if was resizing
if (_isResizing)
{
EndResizing();
}
// Check if was renaming
if (_isRenaming)
{
Rename(_renameTextBox.Text);
StopRenaming();
}
// Base
base.OnLostFocus();
}
/// <inheritdoc />
public override void OnEndMouseCapture()
{
// Check if was resizing
if (_isResizing)
{
EndResizing();
}
else
{
base.OnEndMouseCapture();
}
}
/// <inheritdoc />
public override bool ContainsPoint(ref Float2 location, bool precise)
{
return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location);
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
// Check if can start resizing
if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit)
{
// Start sliding
_isResizing = true;
_startResizingSize = Size;
StartMouseCapture();
return true;
}
return false;
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
// Check if is resizing
if (_isResizing)
{
// Update size
var size = Float2.Max(location, new Float2(140.0f, _headerRect.Bottom));
if (Surface.GridSnappingEnabled)
size = Surface.SnapToGrid(size, true);
Size = size;
}
else
{
// Base
base.OnMouseMove(location);
}
}
/// <inheritdoc />
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
{
@@ -391,12 +297,6 @@ namespace FlaxEditor.Surface
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && _isResizing)
{
EndResizing();
return true;
}
if (base.OnMouseUp(location, button))
return true;

View File

@@ -59,6 +59,7 @@ namespace FlaxEditor.Surface
protected SurfaceControl(VisjectSurfaceContext context, float width, float height)
: base(0, 0, width, height)
{
AutoFocus = true;
ClipChildren = false;
Surface = context.Surface;

View File

@@ -151,6 +151,8 @@ namespace FlaxEditor.Surface
/// </summary>
protected virtual Color FooterColor => GroupArchetype.Color;
private Float2 mouseDownMousePosition;
/// <summary>
/// Calculates the size of the node including header, footer, and margins.
/// </summary>
@@ -429,27 +431,6 @@ namespace FlaxEditor.Surface
/// </summary>
public bool HasIndependentBoxes => Archetype.IndependentBoxes != null;
/// <summary>
/// Gets a value indicating whether this node has dependent boxes with assigned valid types. Otherwise any box has no dependent type assigned.
/// </summary>
public bool HasDependentBoxesSetup
{
get
{
if (Archetype.DependentBoxes == null || Archetype.IndependentBoxes == null)
return true;
for (int i = 0; i < Archetype.DependentBoxes.Length; i++)
{
var b = GetBox(Archetype.DependentBoxes[i]);
if (b != null && b.CurrentType == b.DefaultType)
return false;
}
return true;
}
}
private static readonly List<SurfaceNode> UpdateStack = new List<SurfaceNode>();
/// <summary>
@@ -917,7 +898,7 @@ namespace FlaxEditor.Surface
/// <inheritdoc />
public override bool OnTestTooltipOverControl(ref Float2 location)
{
return _headerRect.Contains(ref location) && ShowTooltip && !Surface.IsConnecting && !Surface.IsBoxSelecting;
return _headerRect.Contains(ref location) && ShowTooltip && !Surface.IsConnecting && !Surface.IsSelecting;
}
/// <inheritdoc />
@@ -1075,7 +1056,7 @@ namespace FlaxEditor.Surface
// Header
var headerColor = style.BackgroundHighlighted;
if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsBoxSelecting)
if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting)
headerColor *= 1.07f;
Render2D.FillRectangle(_headerRect, headerColor);
Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center);
@@ -1083,7 +1064,7 @@ namespace FlaxEditor.Surface
// Close button
if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit)
{
bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsBoxSelecting;
bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting;
Render2D.DrawSprite(style.Cross, _closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey);
}
@@ -1121,7 +1102,7 @@ namespace FlaxEditor.Surface
if (button == MouseButton.Left && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location))
return true;
if (button == MouseButton.Right)
return true;
mouseDownMousePosition = Input.Mouse.Position;
return false;
}
@@ -1133,7 +1114,7 @@ namespace FlaxEditor.Surface
return true;
// Close/ delete
bool canDelete = !Surface.IsConnecting && !Surface.WasBoxSelecting && !Surface.WasMovingSelection;
bool canDelete = !Surface.IsConnecting && !Surface.WasSelecting && !Surface.WasMovingSelection;
if (button == MouseButton.Left && canDelete && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location))
{
Surface.Delete(this);
@@ -1143,6 +1124,10 @@ namespace FlaxEditor.Surface
// Secondary Context Menu
if (button == MouseButton.Right)
{
float distance = Float2.Distance(mouseDownMousePosition, Input.Mouse.Position);
if (distance > 2.5f)
return true;
if (!IsSelected)
Surface.Select(this);
var tmp = PointToParent(ref location);

View File

@@ -400,7 +400,7 @@ namespace FlaxEditor.Surface
return scriptType.GetGenericTypeDefinition() == typeof(Dictionary<,>);
}
var managedType = TypeUtils.GetType(scriptType);
return !TypeUtils.IsDelegate(managedType);
return managedType != null && !TypeUtils.IsDelegate(managedType);
}
internal static bool IsValidVisualScriptFunctionType(ScriptType scriptType)
@@ -408,7 +408,7 @@ namespace FlaxEditor.Surface
if (scriptType.IsGenericType || scriptType.IsStatic || !scriptType.IsPublic || scriptType.HasAttribute(typeof(HideInEditorAttribute), true))
return false;
var managedType = TypeUtils.GetType(scriptType);
return !TypeUtils.IsDelegate(managedType);
return managedType != null && !TypeUtils.IsDelegate(managedType);
}
internal static string GetVisualScriptTypeDescription(ScriptType type)

View File

@@ -1,6 +1,8 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.Scripting;
using FlaxEditor.Surface.Elements;
using FlaxEngine;
namespace FlaxEditor.Surface
@@ -233,11 +235,15 @@ namespace FlaxEditor.Surface
/// Begins connecting surface objects action.
/// </summary>
/// <param name="instigator">The connection instigator (eg. start box).</param>
public void ConnectingStart(IConnectionInstigator instigator)
/// <param name="additive">If the instigator should be added to the list of instigators.</param>
public void ConnectingStart(IConnectionInstigator instigator, bool additive = false)
{
if (instigator != null && instigator != _connectionInstigator)
if (instigator != null && instigator != _connectionInstigators)
{
_connectionInstigator = instigator;
if (!additive)
_connectionInstigators.Clear();
_connectionInstigators.Add(instigator);
StartMouseCapture();
}
}
@@ -257,22 +263,30 @@ namespace FlaxEditor.Surface
/// <param name="end">The end object (eg. end box).</param>
public void ConnectingEnd(IConnectionInstigator end)
{
// Ensure that there was a proper start box
if (_connectionInstigator == null)
// Ensure that there is at least one connection instigator
if (_connectionInstigators.Count == 0)
return;
var start = _connectionInstigator;
_connectionInstigator = null;
// Check if boxes are different and end box is specified
if (start == end || end == null)
return;
// Connect them
if (start.CanConnectWith(end))
List<IConnectionInstigator> instigators = new List<IConnectionInstigator>(_connectionInstigators);
for (int i = 0; i < instigators.Count; i++)
{
start.Connect(end);
var start = instigators[i];
// Check if boxes are different and end box is specified
if (start == end || end == null)
return;
// Properly handle connecting to a socket that already has a connection
if (end is Box e && !e.IsOutput && start is Box s && e.AreConnected(s))
e.BreakConnection(s);
// Connect them
if (start.CanConnectWith(end))
start.Connect(end);
}
// Reset instigator list
_connectionInstigators.Clear();
}
}
}

View File

@@ -261,10 +261,10 @@ namespace FlaxEditor.Surface
/// </summary>
/// <param name="activeCM">The active context menu to show.</param>
/// <param name="location">The display location on the surface control.</param>
/// <param name="startBox">The start box.</param>
protected virtual void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, Box startBox)
/// <param name="startBoxes">The start boxes.</param>
protected virtual void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, List<Box> startBoxes)
{
activeCM.Show(this, location, startBox);
activeCM.Show(this, location, startBoxes);
}
/// <summary>
@@ -298,8 +298,10 @@ namespace FlaxEditor.Surface
_cmStartPos = location;
// Offset added in case the user doesn't like the box and wants to quickly get rid of it by clicking
OnShowPrimaryMenu(_activeVisjectCM, _cmStartPos + ContextMenuOffset, _connectionInstigator as Box);
List<Box> startBoxes = new List<Box>(_connectionInstigators.Where(c => c is Box).Cast<Box>());
// Position offset added so the user can quickly close the menu by clicking
OnShowPrimaryMenu(_activeVisjectCM, _cmStartPos + ContextMenuOffset, startBoxes);
if (!string.IsNullOrEmpty(input))
{
@@ -513,17 +515,15 @@ namespace FlaxEditor.Surface
private void OnPrimaryMenuVisibleChanged(Control primaryMenu)
{
if (!primaryMenu.Visible)
{
_connectionInstigator = null;
}
_connectionInstigators.Clear();
}
/// <summary>
/// Handles Visject CM item click event by spawning the selected item.
/// </summary>
/// <param name="visjectCmItem">The item.</param>
/// <param name="selectedBox">The selected box.</param>
protected virtual void OnPrimaryMenuButtonClick(VisjectCMItem visjectCmItem, Box selectedBox)
/// <param name="selectedBoxes">The selected boxes.</param>
protected virtual void OnPrimaryMenuButtonClick(VisjectCMItem visjectCmItem, List<Box> selectedBoxes)
{
if (!CanEdit)
return;
@@ -550,34 +550,36 @@ namespace FlaxEditor.Surface
// Auto select new node
Select(node);
if (selectedBox != null)
for (int i = 0; i < selectedBoxes.Count; i++)
{
Box endBox = null;
foreach (var box in node.GetBoxes().Where(box => box.IsOutput != selectedBox.IsOutput))
Box currentBox = selectedBoxes[i];
if (currentBox != null)
{
if (selectedBox.IsOutput)
Box endBox = null;
foreach (var box in node.GetBoxes().Where(box => box.IsOutput != currentBox.IsOutput))
{
if (box.CanUseType(selectedBox.CurrentType))
if (currentBox.IsOutput)
{
endBox = box;
break;
if (box.CanUseType(currentBox.CurrentType))
{
endBox = box;
break;
}
}
}
else
{
if (selectedBox.CanUseType(box.CurrentType))
else
{
endBox = box;
break;
if (currentBox.CanUseType(box.CurrentType))
{
endBox = box;
break;
}
}
}
if (endBox == null && selectedBox.CanUseType(box.CurrentType))
{
endBox = box;
if (endBox == null && currentBox.CanUseType(box.CurrentType))
endBox = box;
}
TryConnect(currentBox, endBox);
}
TryConnect(selectedBox, endBox);
}
}
@@ -593,13 +595,8 @@ namespace FlaxEditor.Surface
}
// If the user is patiently waiting for his box to get connected to the newly created one fulfill his wish!
_connectionInstigator = startBox;
if (!IsConnecting)
{
ConnectingStart(startBox);
}
ConnectingEnd(endBox);
// Smart-Select next box

View File

@@ -1,5 +1,6 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.Surface.Elements;
using FlaxEngine;
@@ -126,40 +127,45 @@ namespace FlaxEditor.Surface
/// <remarks>Called only when user is connecting nodes.</remarks>
protected virtual void DrawConnectingLine()
{
// Get start position
var startPos = _connectionInstigator.ConnectionOrigin;
// Check if mouse is over any of box
var cmVisible = _activeVisjectCM != null && _activeVisjectCM.Visible;
var endPos = cmVisible ? _rootControl.PointFromParent(ref _cmStartPos) : _rootControl.PointFromParent(ref _mousePos);
Color lineColor = Style.Colors.Connecting;
if (_lastInstigatorUnderMouse != null && !cmVisible)
{
// Check if can connect objects
bool canConnect = _connectionInstigator.CanConnectWith(_lastInstigatorUnderMouse);
lineColor = canConnect ? Style.Colors.ConnectingValid : Style.Colors.ConnectingInvalid;
endPos = _lastInstigatorUnderMouse.ConnectionOrigin;
}
Float2 actualStartPos = startPos;
Float2 actualEndPos = endPos;
if (_connectionInstigator is Archetypes.Tools.RerouteNode)
List<IConnectionInstigator> instigators = new List<IConnectionInstigator>(_connectionInstigators);
for (int i = 0; i < instigators.Count; i++)
{
if (endPos.X < startPos.X && _lastInstigatorUnderMouse is null or Box { IsOutput: true })
IConnectionInstigator currentInstigator = instigators[i];
Float2 currentStartPosition = currentInstigator.ConnectionOrigin;
// Check if mouse is over any box
if (_lastInstigatorUnderMouse != null && !cmVisible)
{
// Check if can connect objects
bool canConnect = currentInstigator.CanConnectWith(_lastInstigatorUnderMouse);
lineColor = canConnect ? Style.Colors.ConnectingValid : Style.Colors.ConnectingInvalid;
endPos = _lastInstigatorUnderMouse.ConnectionOrigin;
}
Float2 actualStartPos = currentStartPosition;
Float2 actualEndPos = endPos;
if (currentInstigator is Archetypes.Tools.RerouteNode)
{
if (endPos.X < currentStartPosition.X && _lastInstigatorUnderMouse is null or Box { IsOutput: true })
{
actualStartPos = endPos;
actualEndPos = currentStartPosition;
}
}
else if (currentInstigator is Box { IsOutput: false })
{
actualStartPos = endPos;
actualEndPos = startPos;
actualEndPos = currentStartPosition;
}
}
else if (_connectionInstigator is Box { IsOutput: false })
{
actualStartPos = endPos;
actualEndPos = startPos;
}
// Draw connection
_connectionInstigator.DrawConnectingLine(ref actualStartPos, ref actualEndPos, ref lineColor);
// Draw connection
currentInstigator.DrawConnectingLine(ref actualStartPos, ref actualEndPos, ref lineColor);
}
}
/// <summary>
@@ -226,10 +232,10 @@ namespace FlaxEditor.Surface
_rootControl.DrawComments();
// Reset input flags here because this is the closest to Update we have
WasBoxSelecting = IsBoxSelecting;
WasSelecting = IsSelecting;
WasMovingSelection = IsMovingSelection;
if (IsBoxSelecting)
if (IsSelecting)
{
DrawSelection();
}

View File

@@ -176,10 +176,10 @@ namespace FlaxEditor.Surface
if (connectedNodes.Count == 0)
return;
for (int i = 0; i < connectedNodes.Count - 1; i++)
for (int i = 0; i < connectedNodes.Count; i++)
{
SurfaceNode nodeA = connectedNodes[i];
List<Box> connectedOutputBoxes = nodeA.GetBoxes().Where(b => b.IsOutput && b.HasAnyConnection).ToList();
List<Box> connectedOutputBoxes = nodeA.GetBoxes().Where(b => b.HasAnyConnection).ToList();
for (int j = 0; j < connectedOutputBoxes.Count; j++)
{

View File

@@ -121,6 +121,8 @@ namespace FlaxEditor.Surface
private void UpdateSelectionRectangle()
{
if (Root == null)
return;
var p1 = _rootControl.PointFromParent(ref _leftMouseDownPos);
var p2 = _rootControl.PointFromParent(ref _mousePos);
var selectionRect = Rectangle.FromPoints(p1, p2);
@@ -291,7 +293,7 @@ namespace FlaxEditor.Surface
if (_leftMouseDown)
{
// Connecting
if (_connectionInstigator != null)
if (_connectionInstigators.Count > 0)
{
}
// Moving
@@ -461,14 +463,15 @@ namespace FlaxEditor.Surface
public override bool OnMouseDown(Float2 location, MouseButton button)
{
// Check if user is connecting boxes
if (_connectionInstigator != null)
if (_connectionInstigators.Count > 0)
return true;
// Base
bool handled = base.OnMouseDown(location, button);
if (!handled)
CustomMouseDown?.Invoke(ref location, button, ref handled);
if (handled)
var root = Root;
if (handled || root == null)
{
// Clear flags
_isMovingSelection = false;
@@ -522,11 +525,11 @@ namespace FlaxEditor.Surface
if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation))
{
// Check if user is pressing control
if (Root.GetKey(KeyboardKeys.Control))
if (root.GetKey(KeyboardKeys.Control))
{
AddToSelection(controlUnderMouse);
}
else if (Root.GetKey(KeyboardKeys.Shift))
else if (root.GetKey(KeyboardKeys.Shift))
{
RemoveFromSelection(controlUnderMouse);
}
@@ -538,7 +541,7 @@ namespace FlaxEditor.Surface
}
// Start moving selected nodes
if (!Root.GetKey(KeyboardKeys.Shift))
if (!root.GetKey(KeyboardKeys.Shift))
{
StartMouseCapture();
_movingSelectionViewPos = _rootControl.Location;
@@ -558,7 +561,7 @@ namespace FlaxEditor.Surface
// Start selecting or commenting
StartMouseCapture();
if (!Root.GetKey(KeyboardKeys.Control) && !Root.GetKey(KeyboardKeys.Shift))
if (!root.GetKey(KeyboardKeys.Control) && !root.GetKey(KeyboardKeys.Shift))
{
ClearSelection();
}
@@ -607,7 +610,7 @@ namespace FlaxEditor.Surface
_movingNodesDelta = Float2.Zero;
}
// Connecting
else if (_connectionInstigator != null)
else if (_connectionInstigators.Count > 0)
{
}
// Selecting
@@ -679,7 +682,7 @@ namespace FlaxEditor.Surface
ShowPrimaryMenu(_cmStartPos);
}
// Letting go of a connection or right clicking while creating a connection
else if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened)
else if (!_isMovingSelection && _connectionInstigators.Count > 0 && !IsPrimaryMenuOpened)
{
_cmStartPos = location;
Cursor = CursorType.Default;
@@ -730,7 +733,12 @@ namespace FlaxEditor.Surface
if (HasNodesSelection)
{
var keyMoveRange = 50;
var keyMoveDelta = 50;
bool altDown = RootWindow.GetKey(KeyboardKeys.Alt);
bool shiftDown = RootWindow.GetKey(KeyboardKeys.Shift);
if (altDown || shiftDown)
keyMoveDelta = shiftDown ? 10 : 25;
switch (key)
{
case KeyboardKeys.Backspace:
@@ -758,17 +766,18 @@ namespace FlaxEditor.Surface
Box selectedBox = GetSelectedBox(SelectedNodes);
if (selectedBox != null)
{
Box toSelect = (key == KeyboardKeys.ArrowUp) ? selectedBox?.ParentNode.GetPreviousBox(selectedBox) : selectedBox?.ParentNode.GetNextBox(selectedBox);
if (toSelect != null && toSelect.IsOutput == selectedBox.IsOutput)
{
Select(toSelect.ParentNode);
toSelect.ParentNode.SelectBox(toSelect);
}
int delta = key == KeyboardKeys.ArrowDown ? 1 : -1;
List<Box> boxes = selectedBox.ParentNode.GetBoxes().FindAll(b => b.IsOutput == selectedBox.IsOutput);
int selectedIndex = boxes.IndexOf(selectedBox);
Box toSelect = boxes[Mathf.Wrap(selectedIndex + delta, 0, boxes.Count - 1)];
Select(toSelect.ParentNode);
toSelect.ParentNode.SelectBox(toSelect);
}
else if (!IsMovingSelection && CanEdit)
{
// Move selected nodes
var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveRange : keyMoveRange);
var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveDelta : keyMoveDelta);
MoveSelectedNodes(delta);
}
return true;
@@ -781,12 +790,8 @@ namespace FlaxEditor.Surface
if (selectedBox != null)
{
Box toSelect = null;
if ((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput))
if (((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) && selectedBox.HasAnyConnection)
{
if (_selectedConnectionIndex < 0 || _selectedConnectionIndex >= selectedBox.Connections.Count)
{
_selectedConnectionIndex = 0;
}
toSelect = selectedBox.Connections[_selectedConnectionIndex];
}
else
@@ -814,7 +819,7 @@ namespace FlaxEditor.Surface
else if (!IsMovingSelection && CanEdit)
{
// Move selected nodes
var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveRange : keyMoveRange, 0);
var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveDelta : keyMoveDelta, 0);
MoveSelectedNodes(delta);
}
return true;
@@ -830,13 +835,9 @@ namespace FlaxEditor.Surface
return true;
if (Root.GetKey(KeyboardKeys.Shift))
{
_selectedConnectionIndex = ((_selectedConnectionIndex - 1) % connectionCount + connectionCount) % connectionCount;
}
else
{
_selectedConnectionIndex = (_selectedConnectionIndex + 1) % connectionCount;
}
return true;
}
}

View File

@@ -33,7 +33,7 @@ namespace FlaxEditor.Surface
Enabled = false;
// Clean data
_connectionInstigator = null;
_connectionInstigators.Clear();
_lastInstigatorUnderMouse = null;
var failed = RootContext.Load();

View File

@@ -121,7 +121,7 @@ namespace FlaxEditor.Surface
/// <summary>
/// The connection start.
/// </summary>
protected IConnectionInstigator _connectionInstigator;
protected List<IConnectionInstigator> _connectionInstigators = new List<IConnectionInstigator>();
/// <summary>
/// The last connection instigator under mouse.
@@ -232,19 +232,19 @@ namespace FlaxEditor.Surface
}
/// <summary>
/// Gets a value indicating whether user is box selecting nodes.
/// Gets a value indicating whether user is selecting nodes.
/// </summary>
public bool IsBoxSelecting => _leftMouseDown && !_isMovingSelection && _connectionInstigator == null;
public bool IsSelecting => _leftMouseDown && !_isMovingSelection && _connectionInstigators.Count == 0;
/// <summary>
/// Gets a value indicating whether user was previously box selecting nodes.
/// Gets a value indicating whether user was previously selecting nodes.
/// </summary>
public bool WasBoxSelecting { get; private set; }
public bool WasSelecting { get; private set; }
/// <summary>
/// Gets a value indicating whether user is moving selected nodes.
/// </summary>
public bool IsMovingSelection => _leftMouseDown && _isMovingSelection && _connectionInstigator == null;
public bool IsMovingSelection => _leftMouseDown && _isMovingSelection && _connectionInstigators.Count == 0;
/// <summary>
/// Gets a value indicating whether user was previously moving selected nodes.
@@ -254,7 +254,7 @@ namespace FlaxEditor.Surface
/// <summary>
/// Gets a value indicating whether user is connecting nodes.
/// </summary>
public bool IsConnecting => _connectionInstigator != null;
public bool IsConnecting => _connectionInstigators.Count > 0;
/// <summary>
/// Gets a value indicating whether the left mouse button is down.

View File

@@ -178,19 +178,31 @@ namespace FlaxEditor.Surface
// Update boxes types for nodes that dependant box types based on incoming connections
{
bool keepUpdating = false;
int updateLimit = 100;
bool keepUpdating = true;
int updatesMin = 2, updatesMax = 100;
do
{
keepUpdating = false;
for (int i = 0; i < RootControl.Children.Count; i++)
{
if (RootControl.Children[i] is SurfaceNode node && !node.HasDependentBoxesSetup)
if (RootControl.Children[i] is SurfaceNode node)
{
node.UpdateBoxesTypes();
keepUpdating = true;
var arch = node.Archetype;
if (arch.DependentBoxes != null && arch.IndependentBoxes != null)
{
foreach (var boxId in arch.DependentBoxes)
{
var b = node.GetBox(boxId);
if (b != null && b.CurrentType == b.DefaultType)
{
keepUpdating = true;
}
}
}
}
}
} while (keepUpdating && updateLimit-- > 0);
} while ((keepUpdating && --updatesMax > 0) || --updatesMin > 0);
}
Loaded?.Invoke(this);

View File

@@ -212,7 +212,7 @@ namespace FlaxEditor.Surface
}
/// <inheritdoc />
protected override void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, Box startBox)
protected override void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, List<Box> startBoxes)
{
// Update nodes for method overrides
Profiler.BeginEvent("Overrides");
@@ -268,7 +268,7 @@ namespace FlaxEditor.Surface
// Update nodes for invoke methods (async)
_nodesCache.Get(activeCM);
base.OnShowPrimaryMenu(activeCM, location, startBox);
base.OnShowPrimaryMenu(activeCM, location, startBoxes);
activeCM.VisibleChanged += OnActiveContextMenuVisibleChanged;
}

View File

@@ -192,7 +192,7 @@ namespace FlaxEditor.Tools.Terrain
{
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
{
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -209,7 +209,7 @@ namespace FlaxEditor.Tools.Terrain
{
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
{
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -74,11 +74,6 @@ struct TextureDataResult
PixelFormat Format;
Int2 Mip0Size;
BytesContainer* Mip0DataPtr;
TextureDataResult()
: Lock(FlaxStorage::LockData::Invalid)
{
}
};
bool GetTextureDataForSampling(Texture* texture, TextureDataResult& data, bool hdr = false)
@@ -149,13 +144,13 @@ bool GetTextureDataForSampling(Texture* texture, TextureDataResult& data, bool h
bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches, Texture* heightmap, float heightmapScale, Texture* splatmap1, Texture* splatmap2)
{
PROFILE_CPU_NAMED("Terrain.GenerateTerrain");
CHECK_RETURN(terrain && terrain->GetChunkSize() != 0, true);
if (numberOfPatches.X < 1 || numberOfPatches.Y < 1)
{
LOG(Warning, "Cannot setup terain with no patches.");
LOG(Warning, "Cannot setup terrain with no patches.");
return false;
}
PROFILE_CPU_NAMED("Terrain.GenerateTerrain");
// Wait for assets to be loaded
if (heightmap && heightmap->WaitForLoaded())
@@ -178,7 +173,9 @@ bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches
terrain->AddPatches(numberOfPatches);
// Prepare data
const auto heightmapSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
const int32 heightmapSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
const float heightmapSizeInv = 1.0f / (float)(heightmapSize - 1);
const Float2 uvPerPatch = Float2::One / Float2(numberOfPatches);
Array<float> heightmapData;
heightmapData.Resize(heightmapSize * heightmapSize);
@@ -192,19 +189,17 @@ bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches
const auto sampler = PixelFormatSampler::Get(dataHeightmap.Format);
// Initialize with sub-range of the input heightmap
const Vector2 uvPerPatch = Vector2::One / Vector2(numberOfPatches);
const float heightmapSizeInv = 1.0f / (heightmapSize - 1);
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
auto patch = terrain->GetPatch(patchIndex);
const Vector2 uvStart = Vector2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
const Float2 uvStart = Float2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
// Sample heightmap pixels with interpolation to get actual heightmap vertices locations
for (int32 z = 0; z < heightmapSize; z++)
{
for (int32 x = 0; x < heightmapSize; x++)
{
const Vector2 uv = uvStart + Vector2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
const Float2 uv = uvStart + Float2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
const Color color = sampler->SampleLinear(dataHeightmap.Mip0DataPtr->Get(), uv, dataHeightmap.Mip0Size, dataHeightmap.RowPitch);
heightmapData[z * heightmapSize + x] = color.R * heightmapScale;
}
@@ -230,37 +225,30 @@ bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches
Texture* splatmaps[2] = { splatmap1, splatmap2 };
Array<Color32> splatmapData;
TextureDataResult data1;
const Vector2 uvPerPatch = Vector2::One / Vector2(numberOfPatches);
const float heightmapSizeInv = 1.0f / (heightmapSize - 1);
for (int32 index = 0; index < ARRAY_COUNT(splatmaps); index++)
{
const auto splatmap = splatmaps[index];
if (!splatmap)
continue;
// Prepare data
if (splatmapData.IsEmpty())
splatmapData.Resize(heightmapSize * heightmapSize);
// Get splatmap data
if (GetTextureDataForSampling(splatmap, data1))
return true;
const auto sampler = PixelFormatSampler::Get(data1.Format);
// Modify heightmap splatmaps with sub-range of the input splatmaps
splatmapData.Resize(heightmapSize * heightmapSize);
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
auto patch = terrain->GetPatch(patchIndex);
const Vector2 uvStart = Vector2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
const Float2 uvStart = Float2((float)patch->GetX(), (float)patch->GetZ()) * uvPerPatch;
// Sample splatmap pixels with interpolation to get actual splatmap values
for (int32 z = 0; z < heightmapSize; z++)
{
for (int32 x = 0; x < heightmapSize; x++)
{
const Vector2 uv = uvStart + Vector2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
const Float2 uv = uvStart + Float2(x * heightmapSizeInv, z * heightmapSizeInv) * uvPerPatch;
const Color color = sampler->SampleLinear(data1.Mip0DataPtr->Get(), uv, data1.Mip0Size, data1.RowPitch);
Color32 layers;
@@ -374,63 +362,38 @@ Color32* TerrainTools::GetSplatMapData(Terrain* terrain, const Int2& patchCoord,
bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
{
PROFILE_CPU_NAMED("Terrain.ExportTerrain");
CHECK_RETURN(terrain && terrain->GetPatchesCount() != 0, true);
const auto firstPatch = terrain->GetPatch(0);
// Calculate texture size
const int32 patchEdgeVertexCount = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
const int32 patchVertexCount = patchEdgeVertexCount * patchEdgeVertexCount;
// Find size of heightmap in patches
const auto firstPatch = terrain->GetPatch(0);
Int2 start(firstPatch->GetX(), firstPatch->GetZ());
Int2 end(start);
for (int32 i = 0; i < terrain->GetPatchesCount(); i++)
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
const int32 x = terrain->GetPatch(i)->GetX();
const int32 y = terrain->GetPatch(i)->GetZ();
if (x < start.X)
start.X = x;
if (y < start.Y)
start.Y = y;
if (x > end.X)
end.X = x;
if (y > end.Y)
end.Y = y;
const auto patch = terrain->GetPatch(patchIndex);
const Int2 pos(patch->GetX(), patch->GetZ());
start = Int2::Min(start, pos);
end = Int2::Max(end, pos);
}
const Int2 size = (end + 1) - start;
// Allocate - with space for non-existent patches
// Allocate heightmap for a whole terrain (NumberOfPatches * 4x4 * ChunkSize + 1)
const Int2 heightmapSize = size * Terrain::ChunksCountEdge * terrain->GetChunkSize() + 1;
Array<float> heightmap;
heightmap.Resize(patchVertexCount * size.X * size.Y);
// Set to any element, where: min < elem < max
heightmap.Resize(heightmapSize.X * heightmapSize.Y);
heightmap.SetAll(firstPatch->GetHeightmapData()[0]);
const int32 heightmapWidth = patchEdgeVertexCount * size.X;
// Fill heightmap with data
// Fill heightmap with data from all patches
const int32 rowSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
// Pick a patch
const auto patch = terrain->GetPatch(patchIndex);
const float* data = patch->GetHeightmapData();
// Beginning of patch
int32 dstIndex = (patch->GetX() - start.X) * patchEdgeVertexCount +
(patch->GetZ() - start.Y) * size.Y * patchVertexCount;
// Iterate over lines in patch
for (int32 z = 0; z < patchEdgeVertexCount; z++)
{
// Iterate over vertices in line
for (int32 x = 0; x < patchEdgeVertexCount; x++)
{
heightmap[dstIndex + x] = data[z * patchEdgeVertexCount + x];
}
dstIndex += heightmapWidth;
}
const Int2 pos(patch->GetX() - start.X, patch->GetZ() - start.Y);
const float* src = patch->GetHeightmapData();
float* dst = heightmap.Get() + pos.X * (rowSize - 1) + pos.Y * heightmapSize.X * (rowSize - 1);
for (int32 row = 0; row < rowSize; row++)
Platform::MemoryCopy(dst + row * heightmapSize.X, src + row * rowSize, rowSize * sizeof(float));
}
// Interpolate to 16-bit int
@@ -438,44 +401,42 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
maxHeight = minHeight = heightmap[0];
for (int32 i = 1; i < heightmap.Count(); i++)
{
float h = heightmap[i];
float h = heightmap.Get()[i];
if (maxHeight < h)
maxHeight = h;
else if (minHeight > h)
minHeight = h;
}
const float maxValue = 65535.0f;
const float alpha = maxValue / (maxHeight - minHeight);
const float alpha = MAX_uint16 / (maxHeight - minHeight);
// Storage for pixel data
Array<uint16> byteHeightmap(heightmap.Capacity());
for (auto& elem : heightmap)
Array<uint16> byteHeightmap;
byteHeightmap.Resize(heightmap.Count());
for (int32 i = 0; i < heightmap.Count(); i++)
{
byteHeightmap.Add(static_cast<uint16>(alpha * (elem - minHeight)));
float height = heightmap.Get()[i];
byteHeightmap.Get()[i] = static_cast<uint16>(alpha * (height - minHeight));
}
// Create texture
TextureData textureData;
textureData.Height = textureData.Width = heightmapWidth;
textureData.Width = heightmapSize.X;
textureData.Height = heightmapSize.Y;
textureData.Depth = 1;
textureData.Format = PixelFormat::R16_UNorm;
textureData.Items.Resize(1);
textureData.Items[0].Mips.Resize(1);
// Fill mip data
TextureMipData* srcMip = textureData.GetData(0, 0);
srcMip->Data.Link(byteHeightmap.Get());
srcMip->Lines = textureData.Height;
srcMip->RowPitch = textureData.Width * 2; // 2 bytes per pixel for format R16
srcMip->RowPitch = textureData.Width * sizeof(uint16);
srcMip->DepthPitch = srcMip->Lines * srcMip->RowPitch;
// Find next non-existing file heightmap file
FileSystem::NormalizePath(outputFolder);
const String baseFileName(TEXT("heightmap"));
String outputPath;
for (int32 i = 0; i < MAX_int32; i++)
for (int32 i = 0; i < 100; i++)
{
outputPath = outputFolder / baseFileName + StringUtils::ToString(i) + TEXT(".png");
if (!FileSystem::FileExists(outputPath))

View File

@@ -172,7 +172,7 @@ namespace FlaxEditor.Tools.Terrain.Undo
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(scene);
@@ -217,11 +217,10 @@ namespace FlaxEditor.Tools.Terrain.Undo
}
// Update navmesh
var scene = Terrain.Scene;
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);

View File

@@ -303,7 +303,7 @@ namespace FlaxEditor.Actions
if (_nodeParents[i] is ActorNode node && node.Actor && node.Actor.Scene && node.AffectsNavigationWithChildren)
{
var bounds = node.Actor.BoxWithChildren;
Navigation.BuildNavMesh(node.Actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -134,7 +134,8 @@ namespace FlaxEditor.Actions
var obj = Object.Find<SceneObject>(ref item.ID);
if (obj != null)
{
scenes.Add(obj.Parent.Scene);
if (obj.Parent != null)
scenes.Add(obj.Parent.Scene);
if (obj is Actor actor)
actor.SetParent(newParent, _worldPositionsStays, true);
else

View File

@@ -121,12 +121,12 @@ namespace FlaxEditor
// Handle simple case where objects were moved just a little and use one navmesh build request to improve performance
if (data.BeforeBounds.Intersects(ref data.AfterBounds))
{
Navigation.BuildNavMesh(data.Scene, BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
}
else
{
Navigation.BuildNavMesh(data.Scene, data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(data.Scene, data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

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