Merge remote-tracking branch 'upstream/master'

This commit is contained in:
rkrahn
2024-03-29 13:38:38 -07:00
329 changed files with 7133 additions and 3928 deletions

View File

@@ -646,7 +646,9 @@ namespace FlaxEditor.Content.GUI
_rubberBandRectangle = new Rectangle(_mousePressLocation, 0, 0);
_isRubberBandSpanning = true;
StartMouseCapture();
return true;
}
return AutoFocus && Focus(this);
}

View File

@@ -213,7 +213,8 @@ namespace FlaxEditor.Content
if (_attributes == null)
{
var data = _type.Asset.GetMethodMetaData(_index, Surface.SurfaceMeta.AttributeMetaTypeID);
_attributes = Surface.SurfaceMeta.GetAttributes(data);
var dataOld = _type.Asset.GetMethodMetaData(_index, Surface.SurfaceMeta.OldAttributeMetaTypeID);
_attributes = Surface.SurfaceMeta.GetAttributes(data, dataOld);
}
return _attributes;
}
@@ -290,13 +291,11 @@ namespace FlaxEditor.Content
_methods = Utils.GetEmptyArray<ScriptMemberInfo>();
// Cache Visual Script attributes
var attributesData = _asset.GetMetaData(Surface.SurfaceMeta.AttributeMetaTypeID);
if (attributesData != null && attributesData.Length != 0)
{
_attributes = Surface.SurfaceMeta.GetAttributes(attributesData);
var data = _asset.GetMetaData(Surface.SurfaceMeta.AttributeMetaTypeID);
var dataOld = _asset.GetMetaData(Surface.SurfaceMeta.OldAttributeMetaTypeID);
_attributes = Surface.SurfaceMeta.GetAttributes(data, dataOld);
}
else
_attributes = Utils.GetEmptyArray<object>();
}
private void OnAssetReloading(Asset asset)

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.IO;
using FlaxEditor.Content.Thumbnails;
using FlaxEditor.Viewport.Previews;
using FlaxEditor.Windows;
@@ -194,4 +195,64 @@ namespace FlaxEditor.Content
base.Dispose();
}
}
/// <summary>
/// Content proxy for quick UI Control prefab creation as widget.
/// </summary>
[ContentContextMenu("New/Widget")]
internal sealed class WidgetProxy : AssetProxy
{
/// <inheritdoc />
public override string Name => "UI Widget";
/// <inheritdoc />
public override bool IsProxyFor(ContentItem item)
{
return false;
}
/// <inheritdoc />
public override string FileExtension => PrefabProxy.Extension;
/// <inheritdoc />
public override EditorWindow Open(Editor editor, ContentItem item)
{
return null;
}
/// <inheritdoc />
public override Color AccentColor => Color.Transparent;
/// <inheritdoc />
public override string TypeName => PrefabProxy.AssetTypename;
/// <inheritdoc />
public override AssetItem ConstructItem(string path, string typeName, ref Guid id)
{
return null;
}
/// <inheritdoc />
public override bool CanCreate(ContentFolder targetLocation)
{
return targetLocation.CanHaveAssets;
}
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
// Create prefab with UI Control
var actor = new UIControl
{
Name = Path.GetFileNameWithoutExtension(outputPath),
StaticFlags = StaticFlags.None,
};
actor.Control = new Button
{
Text = "Button",
};
PrefabManager.CreatePrefab(actor, outputPath, false);
Object.Destroy(actor, 20.0f);
}
}
}

View File

@@ -14,7 +14,7 @@ class PlatformTools;
#if OFFICIAL_BUILD
// Use the fixed .NET SDK version in packaged builds for compatibility (FlaxGame is precompiled with it)
#define GAME_BUILD_DOTNET_VER TEXT("-dotnet=7")
#define GAME_BUILD_DOTNET_VER TEXT("-dotnet=8")
#else
#define GAME_BUILD_DOTNET_VER TEXT("")
#endif

View File

@@ -22,6 +22,11 @@ IMPLEMENT_ENGINE_SETTINGS_GETTER(AndroidPlatformSettings, AndroidPlatform);
namespace
{
struct AndroidPlatformCache
{
AndroidPlatformSettings::TextureQuality TexturesQuality;
};
void DeployIcon(const CookingData& data, const TextureData& iconData, const Char* subDir, int32 iconSize, int32 adaptiveIconSize)
{
const String mipmapPath = data.OriginalOutputPath / TEXT("app/src/main/res") / subDir;
@@ -30,6 +35,24 @@ namespace
FileSystem::CreateDirectory(mipmapPath);
EditorUtilities::ExportApplicationImage(iconData, iconSize, iconSize, PixelFormat::B8G8R8A8_UNorm, iconPath);
}
PixelFormat GetQualityTextureFormat(bool sRGB, PixelFormat format)
{
const auto platformSettings = AndroidPlatformSettings::Get();
switch (platformSettings->TexturesQuality)
{
case AndroidPlatformSettings::TextureQuality::Uncompressed:
return PixelFormatExtensions::FindUncompressedFormat(format);
case AndroidPlatformSettings::TextureQuality::ASTC_High:
return sRGB ? PixelFormat::ASTC_4x4_UNorm_sRGB : PixelFormat::ASTC_4x4_UNorm;
case AndroidPlatformSettings::TextureQuality::ASTC_Medium:
return sRGB ? PixelFormat::ASTC_6x6_UNorm_sRGB : PixelFormat::ASTC_6x6_UNorm;
case AndroidPlatformSettings::TextureQuality::ASTC_Low:
return sRGB ? PixelFormat::ASTC_8x8_UNorm_sRGB : PixelFormat::ASTC_8x8_UNorm;
default:
return format;
}
}
}
const Char* AndroidPlatformTools::GetDisplayName() const
@@ -54,62 +77,67 @@ ArchitectureType AndroidPlatformTools::GetArchitecture() const
PixelFormat AndroidPlatformTools::GetTextureFormat(CookingData& data, TextureBase* texture, PixelFormat format)
{
// TODO: add ETC compression support for Android
// TODO: add ASTC compression support for Android
// BC formats are not widely supported on Android
if (PixelFormatExtensions::IsCompressedBC(format))
{
switch (format)
{
case PixelFormat::BC1_Typeless:
case PixelFormat::BC2_Typeless:
case PixelFormat::BC3_Typeless:
return PixelFormat::R8G8B8A8_Typeless;
case PixelFormat::BC1_UNorm:
case PixelFormat::BC2_UNorm:
case PixelFormat::BC3_UNorm:
return PixelFormat::R8G8B8A8_UNorm;
case PixelFormat::BC1_UNorm_sRGB:
case PixelFormat::BC2_UNorm_sRGB:
case PixelFormat::BC3_UNorm_sRGB:
return PixelFormat::R8G8B8A8_UNorm_sRGB;
case PixelFormat::BC4_Typeless:
return PixelFormat::R8_Typeless;
case PixelFormat::BC4_UNorm:
return PixelFormat::R8_UNorm;
case PixelFormat::BC4_SNorm:
return PixelFormat::R8_SNorm;
case PixelFormat::BC5_Typeless:
return PixelFormat::R16G16_Typeless;
case PixelFormat::BC5_UNorm:
return PixelFormat::R16G16_UNorm;
case PixelFormat::BC5_SNorm:
return PixelFormat::R16G16_SNorm;
case PixelFormat::BC7_Typeless:
case PixelFormat::BC6H_Typeless:
return PixelFormat::R16G16B16A16_Typeless;
case PixelFormat::BC7_UNorm:
case PixelFormat::BC6H_Uf16:
case PixelFormat::BC6H_Sf16:
return PixelFormat::R16G16B16A16_Float;
case PixelFormat::BC7_UNorm_sRGB:
return PixelFormat::R16G16B16A16_UNorm;
default:
return format;
}
}
switch (format)
{
// Not all Android devices support R11G11B10 textures (eg. M6 Note)
case PixelFormat::R11G11B10_Float:
// Not all Android devices support R11G11B10 textures (eg. M6 Note)
return PixelFormat::R16G16B16A16_UNorm;
case PixelFormat::BC1_Typeless:
case PixelFormat::BC2_Typeless:
case PixelFormat::BC3_Typeless:
case PixelFormat::BC4_Typeless:
case PixelFormat::BC5_Typeless:
case PixelFormat::BC1_UNorm:
case PixelFormat::BC2_UNorm:
case PixelFormat::BC3_UNorm:
case PixelFormat::BC4_UNorm:
case PixelFormat::BC5_UNorm:
return GetQualityTextureFormat(false, format);
case PixelFormat::BC1_UNorm_sRGB:
case PixelFormat::BC2_UNorm_sRGB:
case PixelFormat::BC3_UNorm_sRGB:
case PixelFormat::BC7_UNorm_sRGB:
return GetQualityTextureFormat(true, format);
case PixelFormat::BC4_SNorm:
return PixelFormat::R8_SNorm;
case PixelFormat::BC5_SNorm:
return PixelFormat::R16G16_SNorm;
case PixelFormat::BC6H_Typeless:
case PixelFormat::BC6H_Uf16:
case PixelFormat::BC6H_Sf16:
case PixelFormat::BC7_Typeless:
case PixelFormat::BC7_UNorm:
return PixelFormat::R16G16B16A16_Float; // TODO: ASTC HDR
default:
return format;
}
}
void AndroidPlatformTools::LoadCache(CookingData& data, IBuildCache* cache, const Span<byte>& bytes)
{
const auto platformSettings = AndroidPlatformSettings::Get();
bool invalidTextures = true;
if (bytes.Length() == sizeof(AndroidPlatformCache))
{
auto* platformCache = (AndroidPlatformCache*)bytes.Get();
invalidTextures = platformCache->TexturesQuality != platformSettings->TexturesQuality;
}
if (invalidTextures)
{
LOG(Info, "{0} option has been modified.", TEXT("TexturesQuality"));
cache->InvalidateCacheTextures();
}
}
Array<byte> AndroidPlatformTools::SaveCache(CookingData& data, IBuildCache* cache)
{
const auto platformSettings = AndroidPlatformSettings::Get();
AndroidPlatformCache platformCache;
platformCache.TexturesQuality = platformSettings->TexturesQuality;
Array<byte> result;
result.Add((const byte*)&platformCache, sizeof(platformCache));
return result;
}
void AndroidPlatformTools::OnBuildStarted(CookingData& data)
{
// Adjust the cooking output folder to be located inside the Gradle assets directory
@@ -328,7 +356,7 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
// Copy result package
const String apk = data.OriginalOutputPath / (distributionPackage ? TEXT("app/build/outputs/apk/release/app-release-unsigned.apk") : TEXT("app/build/outputs/apk/debug/app-debug.apk"));
const String outputApk = data.OriginalOutputPath / gameSettings->ProductName + TEXT(".apk");
const String outputApk = data.OriginalOutputPath / EditorUtilities::GetOutputName() + TEXT(".apk");
if (FileSystem::CopyFile(outputApk, apk))
{
LOG(Error, "Failed to copy package from {0} to {1}", apk, outputApk);

View File

@@ -30,6 +30,8 @@ public:
PlatformType GetPlatform() const override;
ArchitectureType GetArchitecture() const override;
PixelFormat GetTextureFormat(CookingData& data, TextureBase* texture, PixelFormat format) override;
void LoadCache(CookingData& data, IBuildCache* cache, const Span<byte>& bytes) override;
Array<byte> SaveCache(CookingData& data, IBuildCache* cache) override;
void OnBuildStarted(CookingData& data) override;
bool OnPostProcess(CookingData& data) override;
};

View File

@@ -494,11 +494,11 @@ bool WindowsPlatformTools::OnDeployBinaries(CookingData& data)
{
const auto platformSettings = WindowsPlatformSettings::Get();
// Apply executable icon
Array<String> files;
FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*.exe"), DirectorySearchOption::TopDirectoryOnly);
if (files.HasItems())
{
// Apply executable icon
TextureData iconData;
if (!EditorUtilities::GetApplicationImage(platformSettings->OverrideIcon, iconData))
{
@@ -508,11 +508,31 @@ bool WindowsPlatformTools::OnDeployBinaries(CookingData& data)
return true;
}
}
// Rename app
const String newName = EditorUtilities::GetOutputName();
if (newName != StringUtils::GetFileNameWithoutExtension(files[0]))
{
if (FileSystem::MoveFile(data.NativeCodeOutputPath / newName + TEXT(".exe"), files[0], true))
{
data.Error(TEXT("Failed to change output executable name."));
return true;
}
}
}
return false;
}
void WindowsPlatformTools::OnBuildStarted(CookingData& data)
{
// Remove old executable
Array<String> files;
FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*.exe"), DirectorySearchOption::TopDirectoryOnly);
for (auto& file : files)
FileSystem::DeleteFile(file);
}
void WindowsPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir)
{
// Pick the first executable file

View File

@@ -31,6 +31,7 @@ public:
ArchitectureType GetArchitecture() const override;
bool UseSystemDotnet() const override;
bool OnDeployBinaries(CookingData& data) override;
void OnBuildStarted(CookingData& data) override;
void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override;
};

View File

@@ -24,6 +24,11 @@ IMPLEMENT_SETTINGS_GETTER(iOSPlatformSettings, iOSPlatform);
namespace
{
struct iOSPlatformCache
{
iOSPlatformSettings::TextureQuality TexturesQuality;
};
String GetAppName()
{
const auto gameSettings = GameSettings::Get();
@@ -60,6 +65,24 @@ namespace
result = result.TrimTrailing();
return result;
}
PixelFormat GetQualityTextureFormat(bool sRGB, PixelFormat format)
{
const auto platformSettings = iOSPlatformSettings::Get();
switch (platformSettings->TexturesQuality)
{
case iOSPlatformSettings::TextureQuality::Uncompressed:
return PixelFormatExtensions::FindUncompressedFormat(format);
case iOSPlatformSettings::TextureQuality::ASTC_High:
return sRGB ? PixelFormat::ASTC_4x4_UNorm_sRGB : PixelFormat::ASTC_4x4_UNorm;
case iOSPlatformSettings::TextureQuality::ASTC_Medium:
return sRGB ? PixelFormat::ASTC_6x6_UNorm_sRGB : PixelFormat::ASTC_6x6_UNorm;
case iOSPlatformSettings::TextureQuality::ASTC_Low:
return sRGB ? PixelFormat::ASTC_8x8_UNorm_sRGB : PixelFormat::ASTC_8x8_UNorm;
default:
return format;
}
}
}
const Char* iOSPlatformTools::GetDisplayName() const
@@ -89,51 +112,37 @@ DotNetAOTModes iOSPlatformTools::UseAOT() const
PixelFormat iOSPlatformTools::GetTextureFormat(CookingData& data, TextureBase* texture, PixelFormat format)
{
// TODO: add ETC compression support for iOS
// TODO: add ASTC compression support for iOS
if (PixelFormatExtensions::IsCompressedBC(format))
switch (format)
{
switch (format)
{
case PixelFormat::BC1_Typeless:
case PixelFormat::BC2_Typeless:
case PixelFormat::BC3_Typeless:
return PixelFormat::R8G8B8A8_Typeless;
case PixelFormat::BC1_UNorm:
case PixelFormat::BC2_UNorm:
case PixelFormat::BC3_UNorm:
return PixelFormat::R8G8B8A8_UNorm;
case PixelFormat::BC1_UNorm_sRGB:
case PixelFormat::BC2_UNorm_sRGB:
case PixelFormat::BC3_UNorm_sRGB:
return PixelFormat::R8G8B8A8_UNorm_sRGB;
case PixelFormat::BC4_Typeless:
return PixelFormat::R8_Typeless;
case PixelFormat::BC4_UNorm:
return PixelFormat::R8_UNorm;
case PixelFormat::BC4_SNorm:
return PixelFormat::R8_SNorm;
case PixelFormat::BC5_Typeless:
return PixelFormat::R16G16_Typeless;
case PixelFormat::BC5_UNorm:
return PixelFormat::R16G16_UNorm;
case PixelFormat::BC5_SNorm:
return PixelFormat::R16G16_SNorm;
case PixelFormat::BC7_Typeless:
case PixelFormat::BC6H_Typeless:
return PixelFormat::R16G16B16A16_Typeless;
case PixelFormat::BC7_UNorm:
case PixelFormat::BC6H_Uf16:
case PixelFormat::BC6H_Sf16:
return PixelFormat::R16G16B16A16_Float;
case PixelFormat::BC7_UNorm_sRGB:
return PixelFormat::R16G16B16A16_UNorm;
default:
return format;
}
case PixelFormat::BC1_Typeless:
case PixelFormat::BC2_Typeless:
case PixelFormat::BC3_Typeless:
case PixelFormat::BC4_Typeless:
case PixelFormat::BC5_Typeless:
case PixelFormat::BC1_UNorm:
case PixelFormat::BC2_UNorm:
case PixelFormat::BC3_UNorm:
case PixelFormat::BC4_UNorm:
case PixelFormat::BC5_UNorm:
return GetQualityTextureFormat(false, format);
case PixelFormat::BC1_UNorm_sRGB:
case PixelFormat::BC2_UNorm_sRGB:
case PixelFormat::BC3_UNorm_sRGB:
case PixelFormat::BC7_UNorm_sRGB:
return GetQualityTextureFormat(true, format);
case PixelFormat::BC4_SNorm:
return PixelFormat::R8_SNorm;
case PixelFormat::BC5_SNorm:
return PixelFormat::R16G16_SNorm;
case PixelFormat::BC6H_Typeless:
case PixelFormat::BC6H_Uf16:
case PixelFormat::BC6H_Sf16:
case PixelFormat::BC7_Typeless:
case PixelFormat::BC7_UNorm:
return PixelFormat::R16G16B16A16_Float; // TODO: ASTC HDR
default:
return format;
}
return format;
}
@@ -143,6 +152,32 @@ bool iOSPlatformTools::IsNativeCodeFile(CookingData& data, const String& file)
return extension.IsEmpty() || extension == TEXT("dylib");
}
void iOSPlatformTools::LoadCache(CookingData& data, IBuildCache* cache, const Span<byte>& bytes)
{
const auto platformSettings = iOSPlatformSettings::Get();
bool invalidTextures = true;
if (bytes.Length() == sizeof(iOSPlatformCache))
{
auto* platformCache = (iOSPlatformCache*)bytes.Get();
invalidTextures = platformCache->TexturesQuality != platformSettings->TexturesQuality;
}
if (invalidTextures)
{
LOG(Info, "{0} option has been modified.", TEXT("TexturesQuality"));
cache->InvalidateCacheTextures();
}
}
Array<byte> iOSPlatformTools::SaveCache(CookingData& data, IBuildCache* cache)
{
const auto platformSettings = iOSPlatformSettings::Get();
iOSPlatformCache platformCache;
platformCache.TexturesQuality = platformSettings->TexturesQuality;
Array<byte> result;
result.Add((const byte*)&platformCache, sizeof(platformCache));
return result;
}
void iOSPlatformTools::OnBuildStarted(CookingData& data)
{
// Adjust the cooking output folders for packaging app
@@ -167,13 +202,25 @@ bool iOSPlatformTools::OnPostProcess(CookingData& data)
if (EditorUtilities::FormatAppPackageName(appIdentifier))
return true;
// Copy fresh Gradle project template
// Copy fresh XCode project template
if (FileSystem::CopyDirectory(data.OriginalOutputPath, platformDataPath / TEXT("Project"), true))
{
LOG(Error, "Failed to deploy XCode project to {0} from {1}", data.OriginalOutputPath, platformDataPath);
return true;
}
// Fix MoltenVK lib (copied from VulkanSDK xcframework)
FileSystem::MoveFile(data.DataOutputPath / TEXT("libMoltenVK.dylib"), data.DataOutputPath / TEXT("MoltenVK"), true);
{
// Fix rpath to point into dynamic library (rather than framework location)
CreateProcessSettings procSettings;
procSettings.HiddenWindow = true;
procSettings.WorkingDirectory = data.DataOutputPath;
procSettings.FileName = TEXT("/usr/bin/install_name_tool");
procSettings.Arguments = TEXT("-id \"@rpath/libMoltenVK.dylib\" libMoltenVK.dylib");
Platform::CreateProcess(procSettings);
}
// Format project template files
Dictionary<String, String> configReplaceMap;
configReplaceMap[TEXT("${AppName}")] = appName;

View File

@@ -19,6 +19,8 @@ public:
ArchitectureType GetArchitecture() const override;
DotNetAOTModes UseAOT() const override;
PixelFormat GetTextureFormat(CookingData& data, TextureBase* texture, PixelFormat format) override;
void LoadCache(CookingData& data, IBuildCache* cache, const Span<byte>& bytes) override;
Array<byte> SaveCache(CookingData& data, IBuildCache* cache) override;
bool IsNativeCodeFile(CookingData& data, const String& file) override;
void OnBuildStarted(CookingData& data) override;
bool OnPostProcess(CookingData& data) override;

View File

@@ -8,6 +8,37 @@
class TextureBase;
/// <summary>
/// The game cooker cache interface.
/// </summary>
class FLAXENGINE_API IBuildCache
{
public:
/// <summary>
/// Removes all cached entries for assets that contain a given asset type. This forces rebuild for them.
/// </summary>
virtual void InvalidateCachePerType(const StringView& typeName) = 0;
/// <summary>
/// Removes all cached entries for assets that contain a given asset type. This forces rebuild for them.
/// </summary>
template<typename T>
FORCE_INLINE void InvalidateCachePerType()
{
InvalidateCachePerType(T::TypeName);
}
/// <summary>
/// Removes all cached entries for assets that contain a shader. This forces rebuild for them.
/// </summary>
void InvalidateCacheShaders();
/// <summary>
/// Removes all cached entries for assets that contain a texture. This forces rebuild for them.
/// </summary>
void InvalidateCacheTextures();
};
/// <summary>
/// The platform support tools base interface.
/// </summary>
@@ -76,6 +107,27 @@ public:
virtual bool IsNativeCodeFile(CookingData& data, const String& file);
public:
/// <summary>
/// Loads the build cache. Allows to invalidate any cached asset types based on the build settings for incremental builds (eg. invalidate textures/shaders).
/// </summary>
/// <param name="data">The cooking data.</param>
/// <param name="data">The build cache interface.</param>
/// <param name="data">The loaded cache data. Can be empty when starting a fresh build.</param>
virtual void LoadCache(CookingData& data, IBuildCache* cache, const Span<byte>& bytes)
{
}
/// <summary>
/// Saves the build cache. Allows to store any build settings to be used for cache invalidation on incremental builds.
/// </summary>
/// <param name="data">The cooking data.</param>
/// <param name="data">The build cache interface.</param>
/// <returns>Data to cache, will be restored during next incremental build.<returns>
virtual Array<byte> SaveCache(CookingData& data, IBuildCache* cache)
{
return Array<byte>();
}
/// <summary>
/// Called when game building starts.
/// </summary>

View File

@@ -31,10 +31,12 @@
#include "Engine/Graphics/Shaders/GPUShader.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Graphics/Materials/MaterialShader.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h"
#include "Engine/Engine/Base/GameBase.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Scripting/Enums.h"
#if PLATFORM_TOOLS_WINDOWS
#include "Engine/Platform/Windows/WindowsPlatformSettings.h"
@@ -49,6 +51,20 @@
Dictionary<String, CookAssetsStep::ProcessAssetFunc> CookAssetsStep::AssetProcessors;
void IBuildCache::InvalidateCacheShaders()
{
InvalidateCachePerType<Shader>();
InvalidateCachePerType<Material>();
InvalidateCachePerType<ParticleEmitter>();
}
void IBuildCache::InvalidateCacheTextures()
{
InvalidateCachePerType<Texture>();
InvalidateCachePerType<CubeTexture>();
InvalidateCachePerType<SpriteAtlas>();
}
bool CookAssetsStep::CacheEntry::IsValid(bool withDependencies)
{
AssetInfo assetInfo;
@@ -113,15 +129,13 @@ void CookAssetsStep::CacheData::InvalidateCachePerType(const StringView& typeNam
void CookAssetsStep::CacheData::Load(CookingData& data)
{
PROFILE_CPU();
HeaderFilePath = data.CacheDirectory / String::Format(TEXT("CookedHeader_{0}.bin"), FLAXENGINE_VERSION_BUILD);
CacheFolder = data.CacheDirectory / TEXT("Cooked");
Entries.Clear();
if (!FileSystem::DirectoryExists(CacheFolder))
{
FileSystem::CreateDirectory(CacheFolder);
}
if (!FileSystem::FileExists(HeaderFilePath))
{
LOG(Warning, "Missing incremental build cooking assets cache.");
@@ -143,9 +157,7 @@ void CookAssetsStep::CacheData::Load(CookingData& data)
return;
LOG(Info, "Loading incremental build cooking cache (entries count: {0})", entriesCount);
file->ReadBytes(&Settings, sizeof(Settings));
Entries.EnsureCapacity(Math::RoundUpToPowerOf2(static_cast<int32>(entriesCount * 3.0f)));
Array<Pair<String, DateTime>> fileDependencies;
@@ -179,6 +191,9 @@ void CookAssetsStep::CacheData::Load(CookingData& data)
e.FileDependencies = fileDependencies;
}
Array<byte> platformCache;
file->Read(platformCache);
int32 checkChar;
file->ReadInt32(&checkChar);
if (checkChar != 13)
@@ -187,6 +202,9 @@ void CookAssetsStep::CacheData::Load(CookingData& data)
Entries.Clear();
}
// Per-platform custom data loading (eg. to invalidate textures/shaders options)
data.Tools->LoadCache(data, this, ToSpan(platformCache));
const auto buildSettings = BuildSettings::Get();
const auto gameSettings = GameSettings::Get();
@@ -200,12 +218,12 @@ void CookAssetsStep::CacheData::Load(CookingData& data)
if (MATERIAL_GRAPH_VERSION != Settings.Global.MaterialGraphVersion)
{
LOG(Info, "{0} option has been modified.", TEXT("MaterialGraphVersion"));
InvalidateCachePerType(Material::TypeName);
InvalidateCachePerType<Material>();
}
if (PARTICLE_GPU_GRAPH_VERSION != Settings.Global.ParticleGraphVersion)
{
LOG(Info, "{0} option has been modified.", TEXT("ParticleGraphVersion"));
InvalidateCachePerType(ParticleEmitter::TypeName);
InvalidateCachePerType<ParticleEmitter>();
}
if (buildSettings->ShadersNoOptimize != Settings.Global.ShadersNoOptimize)
{
@@ -262,24 +280,24 @@ void CookAssetsStep::CacheData::Load(CookingData& data)
#endif
if (invalidateShaders)
{
InvalidateCachePerType(Shader::TypeName);
InvalidateCachePerType(Material::TypeName);
InvalidateCachePerType(ParticleEmitter::TypeName);
InvalidateCachePerType<Shader>();
InvalidateCachePerType<Material>();
InvalidateCachePerType<ParticleEmitter>();
}
// Invalidate textures if streaming settings gets modified
if (Settings.Global.StreamingSettingsAssetId != gameSettings->Streaming || (Entries.ContainsKey(gameSettings->Streaming) && !Entries[gameSettings->Streaming].IsValid()))
{
InvalidateCachePerType(Texture::TypeName);
InvalidateCachePerType(CubeTexture::TypeName);
InvalidateCachePerType(SpriteAtlas::TypeName);
InvalidateCachePerType<Texture>();
InvalidateCachePerType<CubeTexture>();
InvalidateCachePerType<SpriteAtlas>();
}
}
void CookAssetsStep::CacheData::Save()
void CookAssetsStep::CacheData::Save(CookingData& data)
{
PROFILE_CPU();
LOG(Info, "Saving incremental build cooking cache (entries count: {0})", Entries.Count());
auto file = FileWriteStream::Open(HeaderFilePath);
if (file == nullptr)
return;
@@ -302,6 +320,7 @@ void CookAssetsStep::CacheData::Save()
file->Write(f.Second);
}
}
file->Write(data.Tools->SaveCache(data, this));
file->WriteInt32(13);
}
@@ -624,7 +643,8 @@ bool ProcessTextureBase(CookAssetsStep::AssetCookData& data)
const auto asset = static_cast<TextureBase*>(data.Asset);
const auto& assetHeader = asset->StreamingTexture()->GetHeader();
const auto format = asset->Format();
const auto targetFormat = data.Data.Tools->GetTextureFormat(data.Data, asset, format);
auto targetFormat = data.Data.Tools->GetTextureFormat(data.Data, asset, format);
CHECK_RETURN(!PixelFormatExtensions::IsTypeless(targetFormat), true);
const auto streamingSettings = StreamingSettings::Get();
int32 mipLevelsMax = GPU_MAX_TEXTURE_MIP_LEVELS;
if (assetHeader->TextureGroup >= 0 && assetHeader->TextureGroup < streamingSettings->TextureGroups.Count())
@@ -634,6 +654,11 @@ bool ProcessTextureBase(CookAssetsStep::AssetCookData& data)
group.MipLevelsMaxPerPlatform.TryGet(data.Data.Tools->GetPlatform(), mipLevelsMax);
}
// If texture is smaller than the block size of the target format (eg. 4x4 texture using ASTC_6x6) then fallback to uncompressed
int32 blockSize = PixelFormatExtensions::ComputeBlockSize(targetFormat);
if (assetHeader->Width < blockSize || assetHeader->Height < blockSize || (blockSize != 1 && mipLevelsMax < 4))
targetFormat = PixelFormatExtensions::FindUncompressedFormat(format);
// Faster path if don't need to modify texture for the target platform
if (format == targetFormat && assetHeader->MipLevels <= mipLevelsMax)
{
@@ -961,6 +986,7 @@ public:
const int32 count = addedEntries.Count();
if (count == 0)
return false;
PROFILE_CPU();
// Get assets init data and load all chunks
Array<AssetInitData> assetsData;
@@ -1143,7 +1169,7 @@ bool CookAssetsStep::Perform(CookingData& data)
// Cook asset
if (Process(data, cache, assetRef.Get()))
{
cache.Save();
cache.Save(data);
return true;
}
data.Stats.CookedAssets++;
@@ -1151,12 +1177,12 @@ bool CookAssetsStep::Perform(CookingData& data)
// Auto save build cache after every few cooked assets (reduces next build time if cooking fails later)
if (data.Stats.CookedAssets % 50 == 0)
{
cache.Save();
cache.Save(data);
}
}
// Save build cache header
cache.Save();
cache.Save(data);
// Create build game header
{
@@ -1173,7 +1199,6 @@ bool CookAssetsStep::Perform(CookingData& data)
}
stream->WriteInt32(('x' + 'D') * 131); // think about it as '131 times xD'
stream->WriteInt32(FLAXENGINE_VERSION_BUILD);
Array<byte> bytes;

View File

@@ -3,6 +3,7 @@
#pragma once
#include "Editor/Cooker/GameCooker.h"
#include "Editor/Cooker/PlatformTools.h"
#include "Engine/Core/Types/Pair.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Collections/Dictionary.h"
@@ -56,7 +57,7 @@ public:
/// <summary>
/// Assets cooking cache data (incremental building feature).
/// </summary>
struct FLAXENGINE_API CacheData
struct FLAXENGINE_API CacheData : public IBuildCache
{
/// <summary>
/// The cache header file path.
@@ -136,16 +137,6 @@ public:
/// <returns>The added entry reference.</returns>
CacheEntry& CreateEntry(const Asset* asset, String& cachedFilePath);
/// <summary>
/// Removes all cached entries for assets that contain a shader. This forces rebuild for them.
/// </summary>
void InvalidateShaders();
/// <summary>
/// Removes all cached entries for assets that contain a texture. This forces rebuild for them.
/// </summary>
void InvalidateCachePerType(const StringView& typeName);
/// <summary>
/// Loads the cache for the given cooking data.
/// </summary>
@@ -155,7 +146,11 @@ public:
/// <summary>
/// Saves this cache (header file).
/// </summary>
void Save();
/// <param name="data">The data.</param>
void Save(CookingData& data);
using IBuildCache::InvalidateCachePerType;
void InvalidateCachePerType(const StringView& typeName) override;
};
struct FLAXENGINE_API AssetCookData

View File

@@ -116,7 +116,7 @@ bool DeployDataStep::Perform(CookingData& data)
for (String& version : versions)
{
version = String(StringUtils::GetFileName(version));
if (!version.StartsWith(TEXT("7.")) && !version.StartsWith(TEXT("8."))) // .NET 7 or .NET 8
if (!version.StartsWith(TEXT("8."))) // Check for major part of 8.0
version.Clear();
}
Sorting::QuickSort(versions);
@@ -204,14 +204,14 @@ bool DeployDataStep::Perform(CookingData& data)
{
// AOT runtime files inside Engine Platform folder
packFolder /= TEXT("Dotnet");
dstDotnetLibs /= TEXT("lib/net7.0");
srcDotnetLibs = packFolder / TEXT("lib/net7.0");
dstDotnetLibs /= TEXT("lib/net8.0");
srcDotnetLibs = packFolder / TEXT("lib/net8.0");
}
else
{
// Runtime files inside Dotnet SDK folder but placed for AOT
dstDotnetLibs /= TEXT("lib/net7.0");
srcDotnetLibs /= TEXT("../lib/net7.0");
dstDotnetLibs /= TEXT("lib/net8.0");
srcDotnetLibs /= TEXT("../lib/net8.0");
}
}
else
@@ -219,16 +219,18 @@ bool DeployDataStep::Perform(CookingData& data)
if (srcDotnetFromEngine)
{
// Runtime files inside Engine Platform folder
dstDotnetLibs /= TEXT("lib/net7.0");
srcDotnetLibs /= TEXT("lib/net7.0");
dstDotnetLibs /= TEXT("lib/net8.0");
srcDotnetLibs /= TEXT("lib/net8.0");
}
else
{
// Runtime files inside Dotnet SDK folder
dstDotnetLibs /= TEXT("shared/Microsoft.NETCore.App");
srcDotnetLibs /= TEXT("../lib/net7.0");
srcDotnetLibs /= TEXT("../lib/net8.0");
}
}
LOG(Info, "Copying .NET files from {} to {}", packFolder, dstDotnet);
LOG(Info, "Copying .NET files from {} to {}", srcDotnetLibs, dstDotnetLibs);
FileSystem::CopyFile(dstDotnet / TEXT("LICENSE.TXT"), packFolder / TEXT("LICENSE.txt"));
FileSystem::CopyFile(dstDotnet / TEXT("LICENSE.TXT"), packFolder / TEXT("LICENSE.TXT"));
FileSystem::CopyFile(dstDotnet / TEXT("THIRD-PARTY-NOTICES.TXT"), packFolder / TEXT("ThirdPartyNotices.txt"));

View File

@@ -35,7 +35,8 @@ void PrecompileAssembliesStep::OnBuildStarted(CookingData& data)
if (cachedData != aotModeCacheValue)
{
LOG(Info, "AOT cache invalidation");
FileSystem::DeleteDirectory(data.ManagedCodeOutputPath);
FileSystem::DeleteDirectory(data.ManagedCodeOutputPath); // Remove AOT cache
FileSystem::DeleteDirectory(data.DataOutputPath / TEXT("Dotnet")); // Remove deployed Dotnet libs (be sure to remove any leftovers from previous build)
}
}
if (!FileSystem::DirectoryExists(data.ManagedCodeOutputPath))

View File

@@ -385,6 +385,15 @@ namespace FlaxEditor.CustomEditors
LinkedLabel = label;
}
internal bool CanEditValue
{
get
{
var readOnly = Values.Info.GetAttribute<ReadOnlyAttribute>();
return readOnly == null;
}
}
/// <summary>
/// If true, the value reverting to default/reference will be handled via iteration over children editors, instead of for a whole object at once.
/// </summary>
@@ -413,7 +422,7 @@ namespace FlaxEditor.CustomEditors
{
if (!Values.IsDefaultValueModified)
return false;
return true;
return CanEditValue;
}
}
@@ -422,7 +431,7 @@ namespace FlaxEditor.CustomEditors
/// </summary>
public void RevertToDefaultValue()
{
if (!Values.HasDefaultValue)
if (!Values.HasDefaultValue || !CanEditValue)
return;
RevertDiffToDefault();
}
@@ -471,7 +480,7 @@ namespace FlaxEditor.CustomEditors
}
else
{
if (Values.IsReferenceValueModified)
if (CanRevertReferenceValue)
SetValueToReference();
}
}
@@ -485,7 +494,7 @@ namespace FlaxEditor.CustomEditors
{
if (!Values.IsReferenceValueModified)
return false;
return true;
return CanEditValue;
}
}
@@ -494,7 +503,7 @@ namespace FlaxEditor.CustomEditors
/// </summary>
public void RevertToReferenceValue()
{
if (!Values.HasReferenceValue)
if (!Values.HasReferenceValue || !CanEditValue)
return;
RevertDiffToReference();
}

View File

@@ -4,6 +4,7 @@
#include "Engine/Core/Log.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Types/Stopwatch.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Scripting/Scripting.h"
@@ -81,7 +82,7 @@ bool CustomEditorsUtilService::Init()
void OnAssemblyLoaded(MAssembly* assembly)
{
const auto startTime = DateTime::NowUTC();
Stopwatch stopwatch;
// Prepare FlaxEngine
auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly;
@@ -162,8 +163,8 @@ void OnAssemblyLoaded(MAssembly* assembly)
}
}
const auto endTime = DateTime::NowUTC();
LOG(Info, "Assembly \'{0}\' scanned for custom editors in {1} ms", assembly->ToString(), (int32)(endTime - startTime).GetTotalMilliseconds());
stopwatch.Stop();
LOG(Info, "Assembly \'{0}\' scanned for custom editors in {1} ms", assembly->ToString(), stopwatch.GetMilliseconds());
}
void OnAssemblyUnloading(MAssembly* assembly)

View File

@@ -589,25 +589,27 @@ namespace FlaxEditor.CustomEditors.Dedicated
LayoutElementsContainer yEl;
LayoutElementsContainer hEl;
LayoutElementsContainer vEl;
Color axisColorX = ActorTransformEditor.AxisColorX;
Color axisColorY = ActorTransformEditor.AxisColorY;
if (xEq)
{
xEl = UniformPanelCapsuleForObjectWithText(horUp, "X: ", xItem.GetValues(Values));
vEl = UniformPanelCapsuleForObjectWithText(horDown, "Width: ", widthItem.GetValues(Values));
xEl = UniformPanelCapsuleForObjectWithText(horUp, "X: ", xItem.GetValues(Values), axisColorX);
vEl = UniformPanelCapsuleForObjectWithText(horDown, "Width: ", widthItem.GetValues(Values), axisColorX);
}
else
{
xEl = UniformPanelCapsuleForObjectWithText(horUp, "Left: ", leftItem.GetValues(Values));
vEl = UniformPanelCapsuleForObjectWithText(horDown, "Right: ", rightItem.GetValues(Values));
xEl = UniformPanelCapsuleForObjectWithText(horUp, "Left: ", leftItem.GetValues(Values), axisColorX);
vEl = UniformPanelCapsuleForObjectWithText(horDown, "Right: ", rightItem.GetValues(Values), axisColorX);
}
if (yEq)
{
yEl = UniformPanelCapsuleForObjectWithText(horUp, "Y: ", yItem.GetValues(Values));
hEl = UniformPanelCapsuleForObjectWithText(horDown, "Height: ", heightItem.GetValues(Values));
yEl = UniformPanelCapsuleForObjectWithText(horUp, "Y: ", yItem.GetValues(Values), axisColorY);
hEl = UniformPanelCapsuleForObjectWithText(horDown, "Height: ", heightItem.GetValues(Values), axisColorY);
}
else
{
yEl = UniformPanelCapsuleForObjectWithText(horUp, "Top: ", topItem.GetValues(Values));
hEl = UniformPanelCapsuleForObjectWithText(horDown, "Bottom: ", bottomItem.GetValues(Values));
yEl = UniformPanelCapsuleForObjectWithText(horUp, "Top: ", topItem.GetValues(Values), axisColorY);
hEl = UniformPanelCapsuleForObjectWithText(horDown, "Bottom: ", bottomItem.GetValues(Values), axisColorY);
}
xEl.Control.AnchorMin = new Float2(0, xEl.Control.AnchorMin.Y);
xEl.Control.AnchorMax = new Float2(0.5f, xEl.Control.AnchorMax.Y);
@@ -624,28 +626,34 @@ namespace FlaxEditor.CustomEditors.Dedicated
private VerticalPanelElement VerticalPanelWithoutMargin(LayoutElementsContainer cont)
{
var horUp = cont.VerticalPanel();
horUp.Panel.Margin = Margin.Zero;
return horUp;
var panel = cont.VerticalPanel();
panel.Panel.Margin = Margin.Zero;
return panel;
}
private CustomElementsContainer<UniformGridPanel> UniformGridTwoByOne(LayoutElementsContainer cont)
{
var horUp = cont.CustomContainer<UniformGridPanel>();
horUp.CustomControl.SlotsHorizontally = 2;
horUp.CustomControl.SlotsVertically = 1;
horUp.CustomControl.SlotPadding = Margin.Zero;
horUp.CustomControl.ClipChildren = false;
return horUp;
var grid = cont.CustomContainer<UniformGridPanel>();
grid.CustomControl.SlotsHorizontally = 2;
grid.CustomControl.SlotsVertically = 1;
grid.CustomControl.SlotPadding = Margin.Zero;
grid.CustomControl.ClipChildren = false;
return grid;
}
private CustomElementsContainer<UniformGridPanel> UniformPanelCapsuleForObjectWithText(LayoutElementsContainer el, string text, ValueContainer values)
private CustomElementsContainer<UniformGridPanel> UniformPanelCapsuleForObjectWithText(LayoutElementsContainer el, string text, ValueContainer values, Color borderColor)
{
CustomElementsContainer<UniformGridPanel> hor = UniformGridTwoByOne(el);
hor.CustomControl.SlotPadding = new Margin(5, 5, 0, 0);
LabelElement lab = hor.Label(text);
hor.Object(values);
return hor;
var grid = UniformGridTwoByOne(el);
grid.CustomControl.SlotPadding = new Margin(5, 5, 1, 1);
var label = grid.Label(text);
var editor = grid.Object(values);
if (editor is FloatEditor floatEditor && floatEditor.Element is FloatValueElement floatEditorElement)
{
var back = FlaxEngine.GUI.Style.Current.TextBoxBackground;
floatEditorElement.ValueBox.BorderColor = Color.Lerp(borderColor, back, ActorTransformEditor.AxisGreyOutFactor);
floatEditorElement.ValueBox.BorderSelectedColor = borderColor;
}
return grid;
}
private bool _cachedXEq;

View File

@@ -26,6 +26,11 @@ namespace FlaxEditor.CustomEditors.Editors
/// </summary>
public static Color AxisColorZ = new Color(0.0f, 0.0235294f, 1.0f, 1.0f);
/// <summary>
/// The axes colors grey out scale when input field is not focused.
/// </summary>
public static float AxisGreyOutFactor = 0.6f;
/// <summary>
/// Custom editor for actor position property.
/// </summary>
@@ -39,13 +44,15 @@ namespace FlaxEditor.CustomEditors.Editors
// Override colors
var back = FlaxEngine.GUI.Style.Current.TextBoxBackground;
var grayOutFactor = 0.6f;
XElement.ValueBox.BorderColor = Color.Lerp(AxisColorX, back, grayOutFactor);
XElement.ValueBox.BorderColor = Color.Lerp(AxisColorX, back, AxisGreyOutFactor);
XElement.ValueBox.BorderSelectedColor = AxisColorX;
YElement.ValueBox.BorderColor = Color.Lerp(AxisColorY, back, grayOutFactor);
XElement.ValueBox.Category = Utils.ValueCategory.Distance;
YElement.ValueBox.BorderColor = Color.Lerp(AxisColorY, back, AxisGreyOutFactor);
YElement.ValueBox.BorderSelectedColor = AxisColorY;
ZElement.ValueBox.BorderColor = Color.Lerp(AxisColorZ, back, grayOutFactor);
YElement.ValueBox.Category = Utils.ValueCategory.Distance;
ZElement.ValueBox.BorderColor = Color.Lerp(AxisColorZ, back, AxisGreyOutFactor);
ZElement.ValueBox.BorderSelectedColor = AxisColorZ;
ZElement.ValueBox.Category = Utils.ValueCategory.Distance;
}
}
@@ -62,13 +69,15 @@ namespace FlaxEditor.CustomEditors.Editors
// Override colors
var back = FlaxEngine.GUI.Style.Current.TextBoxBackground;
var grayOutFactor = 0.6f;
XElement.ValueBox.BorderColor = Color.Lerp(AxisColorX, back, grayOutFactor);
XElement.ValueBox.BorderColor = Color.Lerp(AxisColorX, back, AxisGreyOutFactor);
XElement.ValueBox.BorderSelectedColor = AxisColorX;
YElement.ValueBox.BorderColor = Color.Lerp(AxisColorY, back, grayOutFactor);
XElement.ValueBox.Category = Utils.ValueCategory.Angle;
YElement.ValueBox.BorderColor = Color.Lerp(AxisColorY, back, AxisGreyOutFactor);
YElement.ValueBox.BorderSelectedColor = AxisColorY;
ZElement.ValueBox.BorderColor = Color.Lerp(AxisColorZ, back, grayOutFactor);
YElement.ValueBox.Category = Utils.ValueCategory.Angle;
ZElement.ValueBox.BorderColor = Color.Lerp(AxisColorZ, back, AxisGreyOutFactor);
ZElement.ValueBox.BorderSelectedColor = AxisColorZ;
ZElement.ValueBox.Category = Utils.ValueCategory.Angle;
}
}
@@ -102,14 +111,17 @@ namespace FlaxEditor.CustomEditors.Editors
SetLinkStyle();
var textSize = FlaxEngine.GUI.Style.Current.FontMedium.MeasureText(LinkedLabel.Text.Value);
_linkButton.LocalX += textSize.X + 10;
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
if (LinkedLabel != null)
{
menu.AddSeparator();
if (LinkValues)
menu.AddButton("Unlink", ToggleLink).LinkTooltip("Unlinks scale components from uniform scaling");
else
menu.AddButton("Link", ToggleLink).LinkTooltip("Links scale components for uniform scaling");
};
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
{
menu.AddSeparator();
if (LinkValues)
menu.AddButton("Unlink", ToggleLink).LinkTooltip("Unlinks scale components from uniform scaling");
else
menu.AddButton("Link", ToggleLink).LinkTooltip("Links scale components for uniform scaling");
};
}
// Override colors
var back = FlaxEngine.GUI.Style.Current.TextBoxBackground;

View File

@@ -21,32 +21,31 @@ namespace FlaxEditor.CustomEditors.Editors
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_element = null;
// Try get limit attribute for value min/max range setting and slider speed
var doubleValue = layout.DoubleValue();
doubleValue.ValueBox.ValueChanged += OnValueChanged;
doubleValue.ValueBox.SlidingEnd += ClearToken;
_element = doubleValue;
var attributes = Values.GetAttributes();
if (attributes != null)
{
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
var limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
doubleValue.SetLimits(limit);
var valueCategory = ((ValueCategoryAttribute)attributes.FirstOrDefault(x => x is ValueCategoryAttribute))?.Category ?? Utils.ValueCategory.None;
if (valueCategory != Utils.ValueCategory.None)
{
// Use double value editor with limit
var doubleValue = layout.DoubleValue();
doubleValue.SetLimits((LimitAttribute)limit);
doubleValue.ValueBox.ValueChanged += OnValueChanged;
doubleValue.ValueBox.SlidingEnd += ClearToken;
_element = doubleValue;
return;
doubleValue.SetCategory(valueCategory);
if (LinkedLabel != null)
{
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
{
menu.AddSeparator();
var mb = menu.AddButton("Show formatted", bt => { doubleValue.SetCategory(bt.Checked ? valueCategory : Utils.ValueCategory.None); });
mb.AutoCheck = true;
mb.Checked = doubleValue.ValueBox.Category != Utils.ValueCategory.None;
};
}
}
}
if (_element == null)
{
// Use double value editor
var doubleValue = layout.DoubleValue();
doubleValue.ValueBox.ValueChanged += OnValueChanged;
doubleValue.ValueBox.SlidingEnd += ClearToken;
_element = doubleValue;
}
}
private void OnValueChanged()

View File

@@ -4,6 +4,7 @@ using System;
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using Utils = FlaxEngine.Utils;
namespace FlaxEditor.CustomEditors.Editors
{
@@ -27,41 +28,42 @@ namespace FlaxEditor.CustomEditors.Editors
public override void Initialize(LayoutElementsContainer layout)
{
_element = null;
// Try get limit attribute for value min/max range setting and slider speed
var attributes = Values.GetAttributes();
var range = (RangeAttribute)attributes?.FirstOrDefault(x => x is RangeAttribute);
if (range != null)
{
// Use slider
var slider = layout.Slider();
slider.Slider.SetLimits(range);
slider.Slider.ValueChanged += OnValueChanged;
slider.Slider.SlidingEnd += ClearToken;
_element = slider;
return;
}
var floatValue = layout.FloatValue();
floatValue.ValueBox.ValueChanged += OnValueChanged;
floatValue.ValueBox.SlidingEnd += ClearToken;
_element = floatValue;
if (attributes != null)
{
var range = attributes.FirstOrDefault(x => x is RangeAttribute);
if (range != null)
var limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
floatValue.SetLimits(limit);
var valueCategory = ((ValueCategoryAttribute)attributes.FirstOrDefault(x => x is ValueCategoryAttribute))?.Category ?? Utils.ValueCategory.None;
if (valueCategory != Utils.ValueCategory.None)
{
// Use slider
var slider = layout.Slider();
slider.SetLimits((RangeAttribute)range);
slider.Slider.ValueChanged += OnValueChanged;
slider.Slider.SlidingEnd += ClearToken;
_element = slider;
return;
floatValue.SetCategory(valueCategory);
if (LinkedLabel != null)
{
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
{
menu.AddSeparator();
var mb = menu.AddButton("Show formatted", bt => { floatValue.SetCategory(bt.Checked ? valueCategory : Utils.ValueCategory.None); });
mb.AutoCheck = true;
mb.Checked = floatValue.ValueBox.Category != Utils.ValueCategory.None;
};
}
}
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
// Use float value editor with limit
var floatValue = layout.FloatValue();
floatValue.SetLimits((LimitAttribute)limit);
floatValue.ValueBox.ValueChanged += OnValueChanged;
floatValue.ValueBox.SlidingEnd += ClearToken;
_element = floatValue;
return;
}
}
if (_element == null)
{
// Use float value editor
var floatValue = layout.FloatValue();
floatValue.ValueBox.ValueChanged += OnValueChanged;
floatValue.ValueBox.SlidingEnd += ClearToken;
_element = floatValue;
}
}

View File

@@ -22,7 +22,8 @@ namespace FlaxEditor.CustomEditors.Editors
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
LinkedLabel.SetupContextMenu += OnSetupContextMenu;
if (LinkedLabel != null)
LinkedLabel.SetupContextMenu += OnSetupContextMenu;
var comboBoxElement = layout.ComboBox();
_comboBox = comboBoxElement.ComboBox;
var names = new List<string>();

View File

@@ -45,23 +45,29 @@ namespace FlaxEditor.CustomEditors.Editors
gridControl.SlotsVertically = 1;
XElement = grid.FloatValue();
XElement.ValueBox.Category = Utils.ValueCategory.Angle;
XElement.ValueBox.ValueChanged += OnValueChanged;
XElement.ValueBox.SlidingEnd += ClearToken;
YElement = grid.FloatValue();
YElement.ValueBox.Category = Utils.ValueCategory.Angle;
YElement.ValueBox.ValueChanged += OnValueChanged;
YElement.ValueBox.SlidingEnd += ClearToken;
ZElement = grid.FloatValue();
ZElement.ValueBox.Category = Utils.ValueCategory.Angle;
ZElement.ValueBox.ValueChanged += OnValueChanged;
ZElement.ValueBox.SlidingEnd += ClearToken;
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
if (LinkedLabel != null)
{
menu.AddSeparator();
var value = ((Quaternion)Values[0]).EulerAngles;
menu.AddButton("Copy Euler", () => { Clipboard.Text = JsonSerializer.Serialize(value); }).TooltipText = "Copy the Euler Angles in Degrees";
};
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
{
menu.AddSeparator();
var value = ((Quaternion)Values[0]).EulerAngles;
menu.AddButton("Copy Euler", () => { Clipboard.Text = JsonSerializer.Serialize(value); }).TooltipText = "Copy the Euler Angles in Degrees";
};
}
}
private void OnValueChanged()

View File

@@ -70,25 +70,48 @@ namespace FlaxEditor.CustomEditors.Editors
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
var category = Utils.ValueCategory.None;
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
var categoryAttribute = (ValueCategoryAttribute)attributes.FirstOrDefault(x => x is ValueCategoryAttribute);
if (categoryAttribute != null)
category = categoryAttribute.Category;
}
XElement = grid.FloatValue();
XElement.SetLimits(limit);
XElement.SetCategory(category);
XElement.ValueBox.ValueChanged += OnXValueChanged;
XElement.ValueBox.SlidingEnd += ClearToken;
YElement = grid.FloatValue();
YElement.SetLimits(limit);
YElement.SetCategory(category);
YElement.ValueBox.ValueChanged += OnYValueChanged;
YElement.ValueBox.SlidingEnd += ClearToken;
ZElement = grid.FloatValue();
ZElement.SetLimits(limit);
ZElement.SetCategory(category);
ZElement.ValueBox.ValueChanged += OnZValueChanged;
ZElement.ValueBox.SlidingEnd += ClearToken;
if (LinkedLabel != null)
{
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
{
menu.AddSeparator();
var mb = menu.AddButton("Show formatted", bt =>
{
XElement.SetCategory(bt.Checked ? category : Utils.ValueCategory.None);
YElement.SetCategory(bt.Checked ? category : Utils.ValueCategory.None);
ZElement.SetCategory(bt.Checked ? category : Utils.ValueCategory.None);
});
mb.AutoCheck = true;
mb.Checked = XElement.ValueBox.Category != Utils.ValueCategory.None;
};
}
}
private void OnXValueChanged()
@@ -248,26 +271,49 @@ namespace FlaxEditor.CustomEditors.Editors
gridControl.SlotsVertically = 1;
LimitAttribute limit = null;
Utils.ValueCategory category = Utils.ValueCategory.None;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
var categoryAttribute = (ValueCategoryAttribute)attributes.FirstOrDefault(x => x is ValueCategoryAttribute);
if (categoryAttribute != null)
category = categoryAttribute.Category;
}
XElement = grid.DoubleValue();
XElement.SetLimits(limit);
XElement.SetCategory(category);
XElement.ValueBox.ValueChanged += OnValueChanged;
XElement.ValueBox.SlidingEnd += ClearToken;
YElement = grid.DoubleValue();
YElement.SetLimits(limit);
YElement.SetCategory(category);
YElement.ValueBox.ValueChanged += OnValueChanged;
YElement.ValueBox.SlidingEnd += ClearToken;
ZElement = grid.DoubleValue();
ZElement.SetLimits(limit);
ZElement.SetCategory(category);
ZElement.ValueBox.ValueChanged += OnValueChanged;
ZElement.ValueBox.SlidingEnd += ClearToken;
if (LinkedLabel != null)
{
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
{
menu.AddSeparator();
var mb = menu.AddButton("Show formatted", bt =>
{
XElement.SetCategory(bt.Checked ? category : Utils.ValueCategory.None);
YElement.SetCategory(bt.Checked ? category : Utils.ValueCategory.None);
ZElement.SetCategory(bt.Checked ? category : Utils.ValueCategory.None);
});
mb.AutoCheck = true;
mb.Checked = XElement.ValueBox.Category != Utils.ValueCategory.None;
};
}
}
private void OnValueChanged()

View File

@@ -51,6 +51,15 @@ namespace FlaxEditor.CustomEditors.Elements
}
}
/// <summary>
/// Sets the editor value category.
/// </summary>
/// <param name="category">The category.</param>
public void SetCategory(Utils.ValueCategory category)
{
ValueBox.Category = category;
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>

View File

@@ -51,6 +51,15 @@ namespace FlaxEditor.CustomEditors.Elements
}
}
/// <summary>
/// Sets the editor value category.
/// </summary>
/// <param name="category">The category.</param>
public void SetCategory(Utils.ValueCategory category)
{
ValueBox.Category = category;
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>

View File

@@ -315,9 +315,9 @@ namespace FlaxEditor.CustomEditors
internal LabelElement Header(HeaderAttribute header)
{
var element = Header(header.Text);
if (header.FontSize != -1)
if (header.FontSize > 0)
element.Label.Font = new FontReference(element.Label.Font.Font, header.FontSize);
if (header.Color != 0)
if (header.Color > 0)
element.Label.TextColor = Color.FromRGBA(header.Color);
return element;
}

View File

@@ -407,12 +407,26 @@ int32 Editor::LoadProduct()
{
Array<String> projectFiles;
FileSystem::DirectoryGetFiles(projectFiles, projectPath, TEXT("*.flaxproj"), DirectorySearchOption::TopDirectoryOnly);
if (projectFiles.Count() == 1)
if (projectFiles.Count() > 1)
{
// Skip creating new project if it already exists
LOG(Info, "Skip creatinng new project because it already exists");
Platform::Fatal(TEXT("Too many project files."));
return -2;
}
else if (projectFiles.Count() == 1)
{
LOG(Info, "Skip creating new project because it already exists");
CommandLine::Options.NewProject.Reset();
}
else
{
Array<String> files;
FileSystem::DirectoryGetFiles(files, projectPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly);
if (files.Count() > 0)
{
Platform::Fatal(String::Format(TEXT("Target project folder '{0}' is not empty."), projectPath));
return -1;
}
}
}
if (CommandLine::Options.NewProject)
{

View File

@@ -290,7 +290,6 @@ namespace FlaxEditor
StateMachine = new EditorStateMachine(this);
Undo = new EditorUndo(this);
UIControl.FallbackParentGetDelegate += OnUIControlFallbackParentGet;
if (newProject)
InitProject();
@@ -355,27 +354,6 @@ namespace FlaxEditor
StateMachine.LoadingState.StartInitEnding(skipCompile);
}
private ContainerControl OnUIControlFallbackParentGet(UIControl control)
{
// Check if prefab root control is this UIControl
var loadingPreview = Viewport.Previews.PrefabPreview.LoadingPreview;
var activePreviews = Viewport.Previews.PrefabPreview.ActivePreviews;
if (activePreviews != null)
{
foreach (var preview in activePreviews)
{
if (preview == loadingPreview ||
(preview.Instance != null && (preview.Instance == control || preview.Instance.HasActorInHierarchy(control))))
{
// Link it to the prefab preview to see it in the editor
preview.customControlLinked = control;
return preview;
}
}
}
return null;
}
internal void RegisterModule(EditorModule module)
{
Log("Register Editor module " + module);

View File

@@ -431,7 +431,6 @@ namespace FlaxEditor.GUI
/// </summary>
protected CurveEditor()
{
_tickStrengths = new float[TickSteps.Length];
Accessor.GetDefaultValue(out DefaultValue);
var style = Style.Current;
@@ -780,75 +779,31 @@ namespace FlaxEditor.GUI
return _mainPanel.PointToParent(point);
}
private void DrawAxis(Float2 axis, ref Rectangle viewRect, float min, float max, float pixelRange)
private void DrawAxis(Float2 axis, Rectangle viewRect, float min, float max, float pixelRange)
{
int minDistanceBetweenTicks = 20;
int maxDistanceBetweenTicks = 60;
var range = max - min;
// Find the strength for each modulo number tick marker
int smallestTick = 0;
int biggestTick = TickSteps.Length - 1;
for (int i = TickSteps.Length - 1; i >= 0; i--)
Utilities.Utils.DrawCurveTicks((float tick, float strength) =>
{
// Calculate how far apart these modulo tick steps are spaced
float tickSpacing = TickSteps[i] * pixelRange / range;
var p = PointFromKeyframes(axis * tick, ref viewRect);
// Calculate the strength of the tick markers based on the spacing
_tickStrengths[i] = Mathf.Saturate((tickSpacing - minDistanceBetweenTicks) / (maxDistanceBetweenTicks - minDistanceBetweenTicks));
// Draw line
var lineRect = new Rectangle
(
viewRect.Location + (p - 0.5f) * axis,
Float2.Lerp(viewRect.Size, Float2.One, axis)
);
Render2D.FillRectangle(lineRect, _linesColor.AlphaMultiplied(strength));
// Beyond threshold the ticks don't get any bigger or fatter
if (_tickStrengths[i] >= 1)
biggestTick = i;
// Do not show small tick markers
if (tickSpacing <= minDistanceBetweenTicks)
{
smallestTick = i;
break;
}
}
// Draw all tick levels
int tickLevels = biggestTick - smallestTick + 1;
for (int level = 0; level < tickLevels; level++)
{
float strength = _tickStrengths[smallestTick + level];
if (strength <= Mathf.Epsilon)
continue;
// Draw all ticks
int l = Mathf.Clamp(smallestTick + level, 0, TickSteps.Length - 1);
int startTick = Mathf.FloorToInt(min / TickSteps[l]);
int endTick = Mathf.CeilToInt(max / TickSteps[l]);
for (int i = startTick; i <= endTick; i++)
{
if (l < biggestTick && (i % Mathf.RoundToInt(TickSteps[l + 1] / TickSteps[l]) == 0))
continue;
var tick = i * TickSteps[l];
var p = PointFromKeyframes(axis * tick, ref viewRect);
// Draw line
var lineRect = new Rectangle
(
viewRect.Location + (p - 0.5f) * axis,
Float2.Lerp(viewRect.Size, Float2.One, axis)
);
Render2D.FillRectangle(lineRect, _linesColor.AlphaMultiplied(strength));
// Draw label
string label = tick.ToString(CultureInfo.InvariantCulture);
var labelRect = new Rectangle
(
viewRect.X + 4.0f + (p.X * axis.X),
viewRect.Y - LabelsSize + (p.Y * axis.Y) + (viewRect.Size.Y * axis.X),
50,
LabelsSize
);
Render2D.DrawText(_labelsFont, label, labelRect, _labelsColor.AlphaMultiplied(strength), TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 0.7f);
}
}
// Draw label
string label = tick.ToString(CultureInfo.InvariantCulture);
var labelRect = new Rectangle
(
viewRect.X + 4.0f + (p.X * axis.X),
viewRect.Y - LabelsSize + (p.Y * axis.Y) + (viewRect.Size.Y * axis.X),
50,
LabelsSize
);
Render2D.DrawText(_labelsFont, label, labelRect, _labelsColor.AlphaMultiplied(strength), TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 0.7f);
}, TickSteps, ref _tickStrengths, min, max, pixelRange);
}
/// <summary>
@@ -890,9 +845,9 @@ namespace FlaxEditor.GUI
Render2D.PushClip(ref viewRect);
if ((ShowAxes & UseMode.Vertical) == UseMode.Vertical)
DrawAxis(Float2.UnitX, ref viewRect, min.X, max.X, pixelRange.X);
DrawAxis(Float2.UnitX, viewRect, min.X, max.X, pixelRange.X);
if ((ShowAxes & UseMode.Horizontal) == UseMode.Horizontal)
DrawAxis(Float2.UnitY, ref viewRect, min.Y, max.Y, pixelRange.Y);
DrawAxis(Float2.UnitY, viewRect, min.Y, max.Y, pixelRange.Y);
Render2D.PopClip();
}

View File

@@ -3,6 +3,7 @@
using System;
using FlaxEditor.Utilities;
using FlaxEngine;
using Utils = FlaxEngine.Utils;
namespace FlaxEditor.GUI.Input
{
@@ -13,6 +14,8 @@ namespace FlaxEditor.GUI.Input
[HideInEditor]
public class DoubleValueBox : ValueBox<double>
{
private Utils.ValueCategory _category = Utils.ValueCategory.None;
/// <inheritdoc />
public override double Value
{
@@ -129,10 +132,25 @@ namespace FlaxEditor.GUI.Input
Value = Value;
}
/// <summary>
/// Gets or sets the category of the value. This can be none for just a number or a more specific one like a distance.
/// </summary>
public Utils.ValueCategory Category
{
get => _category;
set
{
if (_category == value)
return;
_category = value;
UpdateText();
}
}
/// <inheritdoc />
protected sealed override void UpdateText()
{
SetText(Utilities.Utils.FormatFloat(_value));
SetText(Utilities.Utils.FormatFloat(_value, Category));
}
/// <inheritdoc />

View File

@@ -1,9 +1,9 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Globalization;
using FlaxEditor.Utilities;
using FlaxEngine;
using Utils = FlaxEngine.Utils;
namespace FlaxEditor.GUI.Input
{
@@ -14,6 +14,8 @@ namespace FlaxEditor.GUI.Input
[HideInEditor]
public class FloatValueBox : ValueBox<float>
{
private Utils.ValueCategory _category = Utils.ValueCategory.None;
/// <inheritdoc />
public override float Value
{
@@ -137,10 +139,25 @@ namespace FlaxEditor.GUI.Input
Value = Value;
}
/// <summary>
/// Gets or sets the category of the value. This can be none for just a number or a more specific one like a distance.
/// </summary>
public Utils.ValueCategory Category
{
get => _category;
set
{
if (_category == value)
return;
_category = value;
UpdateText();
}
}
/// <inheritdoc />
protected sealed override void UpdateText()
{
SetText(Utilities.Utils.FormatFloat(_value));
SetText(Utilities.Utils.FormatFloat(_value, Category));
}
/// <inheritdoc />

View File

@@ -28,7 +28,6 @@ namespace FlaxEditor.GUI.Timeline.GUI
{
_timeline = timeline;
_tickSteps = Utilities.Utils.CurveTickSteps;
_tickStrengths = new float[_tickSteps.Length];
}
private void UpdateSelectionRectangle()
@@ -173,55 +172,20 @@ namespace FlaxEditor.GUI.Timeline.GUI
var rightFrame = Mathf.Ceil((right - Timeline.StartOffset) / zoom) * _timeline.FramesPerSecond;
var min = leftFrame;
var max = rightFrame;
int smallestTick = 0;
int biggestTick = _tickSteps.Length - 1;
for (int i = _tickSteps.Length - 1; i >= 0; i--)
{
// Calculate how far apart these modulo tick steps are spaced
float tickSpacing = _tickSteps[i] * _timeline.Zoom;
// Calculate the strength of the tick markers based on the spacing
_tickStrengths[i] = Mathf.Saturate((tickSpacing - minDistanceBetweenTicks) / (maxDistanceBetweenTicks - minDistanceBetweenTicks));
// Beyond threshold the ticks don't get any bigger or fatter
if (_tickStrengths[i] >= 1)
biggestTick = i;
// Do not show small tick markers
if (tickSpacing <= minDistanceBetweenTicks)
{
smallestTick = i;
break;
}
}
int tickLevels = biggestTick - smallestTick + 1;
// Draw vertical lines for time axis
for (int level = 0; level < tickLevels; level++)
var pixelsInRange = _timeline.Zoom;
var pixelRange = pixelsInRange * (max - min);
var tickRange = Utilities.Utils.DrawCurveTicks((float tick, float strength) =>
{
float strength = _tickStrengths[smallestTick + level];
if (strength <= Mathf.Epsilon)
continue;
// Draw all ticks
int l = Mathf.Clamp(smallestTick + level, 0, _tickSteps.Length - 1);
var lStep = _tickSteps[l];
var lNextStep = _tickSteps[l + 1];
int startTick = Mathf.FloorToInt(min / lStep);
int endTick = Mathf.CeilToInt(max / lStep);
Color lineColor = style.ForegroundDisabled.RGBMultiplied(0.7f).AlphaMultiplied(strength);
for (int i = startTick; i <= endTick; i++)
{
if (l < biggestTick && (i % Mathf.RoundToInt(lNextStep / lStep) == 0))
continue;
var tick = i * lStep;
var time = tick / _timeline.FramesPerSecond;
var x = time * zoom + Timeline.StartOffset;
// Draw line
Render2D.FillRectangle(new Rectangle(x - 0.5f, 0, 1.0f, height), lineColor);
}
}
var time = tick / _timeline.FramesPerSecond;
var x = time * zoom + Timeline.StartOffset;
var lineColor = style.ForegroundDisabled.RGBMultiplied(0.7f).AlphaMultiplied(strength);
Render2D.FillRectangle(new Rectangle(x - 0.5f, 0, 1.0f, height), lineColor);
}, _tickSteps, ref _tickStrengths, min, max, pixelRange, minDistanceBetweenTicks, maxDistanceBetweenTicks);
var smallestTick = tickRange.X;
var biggestTick = tickRange.Y;
var tickLevels = biggestTick - smallestTick + 1;
// Draw selection rectangle
if (_isSelecting)

View File

@@ -904,7 +904,7 @@ namespace FlaxEditor.GUI
var k = new Keyframe
{
Time = keyframesPos.X,
Value = DefaultValue,
Value = Utilities.Utils.CloneValue(DefaultValue),
};
OnEditingStart();
AddKeyframe(k);

View File

@@ -1,5 +1,7 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Globalization;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -44,6 +46,25 @@ namespace FlaxEditor.GUI.Timeline.GUI
var thickness = 2.0f;
var borderColor = _isMoving ? moveColor : (IsMouseOver && _canEdit ? Color.Yellow : style.BorderNormal);
Render2D.FillRectangle(new Rectangle((Width - thickness) * 0.5f, timeAxisHeaderOffset, thickness, Height - timeAxisHeaderOffset), borderColor);
if (_canEdit && _isMoving)
{
// TODO: handle start
string labelText;
switch (_timeline.TimeShowMode)
{
case Timeline.TimeShowModes.Frames:
labelText = _timeline.DurationFrames.ToString("###0", CultureInfo.InvariantCulture);
break;
case Timeline.TimeShowModes.Seconds:
labelText = _timeline.Duration.ToString("###0.##'s'", CultureInfo.InvariantCulture);
break;
case Timeline.TimeShowModes.Time:
labelText = TimeSpan.FromSeconds(_timeline.DurationFrames / _timeline.FramesPerSecond).ToString("g");
break;
default: throw new ArgumentOutOfRangeException();
}
Render2D.DrawText(style.FontSmall, labelText, style.Foreground, new Float2((Width - thickness) * 0.5f + 4, timeAxisHeaderOffset));
}
}
/// <inheritdoc />
@@ -90,13 +111,26 @@ namespace FlaxEditor.GUI.Timeline.GUI
{
_timeline.MarkAsEdited();
}
Cursor = CursorType.SizeWE;
}
else if (IsMouseOver && _canEdit)
{
Cursor = CursorType.SizeWE;
}
else
{
Cursor = CursorType.Default;
base.OnMouseMove(location);
}
}
/// <inheritdoc />
public override void OnMouseLeave()
{
Cursor = CursorType.Default;
base.OnMouseLeave();
}
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
@@ -127,6 +161,7 @@ namespace FlaxEditor.GUI.Timeline.GUI
{
EndMoving();
}
Cursor = CursorType.Default;
base.OnLostFocus();
}

View File

@@ -1141,17 +1141,19 @@ namespace FlaxEditor.GUI.Timeline
{
foreach (var e in _playbackNavigation)
{
e.Enabled = false;
e.Visible = false;
e.Enabled = true;
e.Visible = true;
}
}
if (_playbackStop != null)
{
_playbackStop.Visible = false;
_playbackStop.Visible = true;
_playbackStop.Enabled = false;
}
if (_playbackPlay != null)
{
_playbackPlay.Visible = false;
_playbackPlay.Visible = true;
_playbackPlay.Enabled = false;
}
if (_positionHandle != null)
{

View File

@@ -204,7 +204,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
b.TooltipText = Utilities.Utils.GetTooltip(actorNode.Actor);
}
}
menu.AddButton("Select...", OnClickedSelect).TooltipText = "Opens actor picker dialog to select the target actor for this track";
menu.AddButton("Retarget...", OnClickedSelect).TooltipText = "Opens actor picker dialog to select the target actor for this track";
}
/// <summary>

View File

@@ -7,6 +7,8 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEditor.Scripting;
using FlaxEditor.Utilities;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
@@ -56,7 +58,6 @@ namespace FlaxEditor.GUI.Timeline.Tracks
throw new Exception("Invalid track data.");
var keyframes = new KeyframesEditor.Keyframe[keyframesCount];
var dataBuffer = new byte[e.ValueSize];
var propertyType = TypeUtils.GetManagedType(e.MemberTypeName);
if (propertyType == null)
{
@@ -67,20 +68,40 @@ namespace FlaxEditor.GUI.Timeline.Tracks
return;
}
GCHandle handle = GCHandle.Alloc(dataBuffer, GCHandleType.Pinned);
for (int i = 0; i < keyframesCount; i++)
if (e.ValueSize != 0)
{
var time = stream.ReadSingle();
stream.Read(dataBuffer, 0, e.ValueSize);
var value = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), propertyType);
keyframes[i] = new KeyframesEditor.Keyframe
// POD value type - use raw memory
var dataBuffer = new byte[e.ValueSize];
GCHandle handle = GCHandle.Alloc(dataBuffer, GCHandleType.Pinned);
for (int i = 0; i < keyframesCount; i++)
{
Time = time,
Value = value,
};
var time = stream.ReadSingle();
stream.Read(dataBuffer, 0, e.ValueSize);
var value = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), propertyType);
keyframes[i] = new KeyframesEditor.Keyframe
{
Time = time,
Value = value,
};
}
handle.Free();
}
else
{
// Generic value - use Json storage (as UTF-8)
for (int i = 0; i < keyframesCount; i++)
{
var time = stream.ReadSingle();
var len = stream.ReadInt32();
var value = len != 0 ? FlaxEngine.Json.JsonSerializer.Deserialize(Encoding.UTF8.GetString(stream.ReadBytes(len)), propertyType) : null;
keyframes[i] = new KeyframesEditor.Keyframe
{
Time = time,
Value = value,
};
}
}
handle.Free();
e.Keyframes.DefaultValue = e.GetDefaultValue(propertyType);
e.Keyframes.SetKeyframes(keyframes);
@@ -113,17 +134,35 @@ namespace FlaxEditor.GUI.Timeline.Tracks
stream.Write(propertyTypeNameData);
stream.Write('\0');
var dataBuffer = new byte[e.ValueSize];
IntPtr ptr = Marshal.AllocHGlobal(e.ValueSize);
for (int i = 0; i < keyframes.Count; i++)
if (e.ValueSize != 0)
{
var keyframe = keyframes[i];
Marshal.StructureToPtr(keyframe.Value, ptr, true);
Marshal.Copy(ptr, dataBuffer, 0, e.ValueSize);
stream.Write(keyframe.Time);
stream.Write(dataBuffer);
// POD value type - use raw memory
var dataBuffer = new byte[e.ValueSize];
IntPtr ptr = Marshal.AllocHGlobal(e.ValueSize);
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);
stream.Write(keyframe.Time);
stream.Write(dataBuffer);
}
Marshal.FreeHGlobal(ptr);
}
else
{
// Generic value - use Json storage (as UTF-8)
for (int i = 0; i < keyframes.Count; i++)
{
var keyframe = keyframes[i];
stream.Write(keyframe.Time);
var json = keyframe.Value != null ? FlaxEngine.Json.JsonSerializer.Serialize(keyframe.Value) : null;
var len = json?.Length ?? 0;
stream.Write(len);
if (len > 0)
stream.Write(Encoding.UTF8.GetBytes(json));
}
}
Marshal.FreeHGlobal(ptr);
}
private byte[] _keyframesEditingStartData;
@@ -281,7 +320,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
private void OnKeyframesEditingEnd()
{
var after = EditTrackAction.CaptureData(this);
if (!Utils.ArraysEqual(_keyframesEditingStartData, after))
if (!FlaxEngine.Utils.ArraysEqual(_keyframesEditingStartData, after))
Timeline.AddBatchedUndoAction(new EditTrackAction(Timeline, this, _keyframesEditingStartData, after));
_keyframesEditingStartData = null;
}
@@ -308,7 +347,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// <returns>The default value.</returns>
protected virtual object GetDefaultValue(Type propertyType)
{
return Activator.CreateInstance(propertyType);
var value = TypeUtils.GetDefaultValue(new ScriptType(propertyType));
if (value == null)
value = Activator.CreateInstance(propertyType);
return value;
}
/// <inheritdoc />

View File

@@ -424,6 +424,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{ typeof(Guid), KeyframesPropertyTrack.GetArchetype() },
{ typeof(DateTime), KeyframesPropertyTrack.GetArchetype() },
{ typeof(TimeSpan), KeyframesPropertyTrack.GetArchetype() },
{ typeof(LocalizedString), KeyframesPropertyTrack.GetArchetype() },
{ typeof(string), StringPropertyTrack.GetArchetype() },
};
}

View File

@@ -161,5 +161,20 @@ namespace FlaxEditor.Gizmo
}
throw new ArgumentException("Not added mode to activate.");
}
/// <summary>
/// Gets the gizmo of a given type or returns null if not added.
/// </summary>
/// <typeparam name="T">Type of the gizmo.</typeparam>
/// <returns>Found gizmo or null.</returns>
public T Get<T>() where T : GizmoBase
{
foreach (var e in this)
{
if (e is T asT)
return asT;
}
return null;
}
}
}

View File

@@ -46,7 +46,8 @@ namespace FlaxEditor.Gizmo
var plane = new Plane(Vector3.Zero, Vector3.UnitY);
var dst = CollisionsHelper.DistancePlanePoint(ref plane, ref viewPos);
float space = Editor.Instance.Options.Options.Viewport.ViewportGridScale, size;
var options = Editor.Instance.Options.Options;
float space = options.Viewport.ViewportGridScale, size;
if (dst <= 500.0f)
{
size = 8000;
@@ -62,8 +63,12 @@ namespace FlaxEditor.Gizmo
size = 100000;
}
Color color = Color.Gray * 0.7f;
float bigLineIntensity = 0.8f;
Color bigColor = Color.Gray * bigLineIntensity;
Color color = bigColor * 0.8f;
int count = (int)(size / space);
int midLine = count / 2;
int bigLinesMod = count / 8;
Vector3 start = new Vector3(0, 0, size * -0.5f);
Vector3 end = new Vector3(0, 0, size * 0.5f);
@@ -71,7 +76,12 @@ namespace FlaxEditor.Gizmo
for (int i = 0; i <= count; i++)
{
start.X = end.X = i * space + start.Z;
DebugDraw.DrawLine(start, end, color);
Color lineColor = color;
if (i == midLine)
lineColor = Color.Blue * bigLineIntensity;
else if (i % bigLinesMod == 0)
lineColor = bigColor;
DebugDraw.DrawLine(start, end, lineColor);
}
start = new Vector3(size * -0.5f, 0, 0);
@@ -80,7 +90,12 @@ namespace FlaxEditor.Gizmo
for (int i = 0; i <= count; i++)
{
start.Z = end.Z = i * space + start.X;
DebugDraw.DrawLine(start, end, color);
Color lineColor = color;
if (i == midLine)
lineColor = Color.Red * bigLineIntensity;
else if (i % bigLinesMod == 0)
lineColor = bigColor;
DebugDraw.DrawLine(start, end, lineColor);
}
DebugDraw.Draw(ref renderContext, input.View(), null, true);

View File

@@ -117,5 +117,10 @@ namespace FlaxEditor.Gizmo
/// </summary>
/// <param name="actor">The new actor to spawn.</param>
void Spawn(Actor actor);
/// <summary>
/// Opens the context menu at the current mouse location (using current selection).
/// </summary>
void OpenContextMenu();
}
}

View File

@@ -42,6 +42,11 @@ namespace FlaxEditor.Gizmo
/// </summary>
public Action Duplicate;
/// <summary>
/// Gets the array of selected objects.
/// </summary>
public List<SceneGraphNode> Selection => _selection;
/// <summary>
/// Gets the array of selected parent objects (as actors).
/// </summary>

View File

@@ -0,0 +1,891 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Viewport.Cameras;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor
{
/// <summary>
/// UI editor camera.
/// </summary>
[HideInEditor]
internal sealed class UIEditorCamera : ViewportCamera
{
public UIEditorRoot UIEditor;
public void ShowActors(IEnumerable<Actor> actors)
{
var root = UIEditor.UIRoot;
if (root == null)
return;
// Calculate bounds of all selected objects
var areaRect = Rectangle.Empty;
foreach (var actor in actors)
{
Rectangle bounds;
if (actor is UIControl uiControl && uiControl.HasControl && uiControl.IsActive)
{
var control = uiControl.Control;
bounds = control.EditorBounds;
var ul = control.PointToParent(root, bounds.UpperLeft);
var ur = control.PointToParent(root, bounds.UpperRight);
var bl = control.PointToParent(root, bounds.BottomLeft);
var br = control.PointToParent(root, bounds.BottomRight);
var min = Float2.Min(Float2.Min(ul, ur), Float2.Min(bl, br));
var max = Float2.Max(Float2.Max(ul, ur), Float2.Max(bl, br));
bounds = new Rectangle(min, Float2.Max(max - min, Float2.Zero));
}
else if (actor is UICanvas uiCanvas && uiCanvas.IsActive && uiCanvas.GUI.Parent == root)
{
bounds = uiCanvas.GUI.Bounds;
}
else
continue;
if (areaRect == Rectangle.Empty)
areaRect = bounds;
else
areaRect = Rectangle.Union(areaRect, bounds);
}
if (areaRect == Rectangle.Empty)
return;
// Add margin
areaRect = areaRect.MakeExpanded(100.0f);
// Show bounds
UIEditor.ViewScale = (UIEditor.Size / areaRect.Size).MinValue * 0.95f;
UIEditor.ViewCenterPosition = areaRect.Center;
}
public override void FocusSelection(GizmosCollection gizmos, ref Quaternion orientation)
{
ShowActors(gizmos.Get<TransformGizmo>().Selection, ref orientation);
}
public override void ShowActor(Actor actor)
{
ShowActors(new[] { actor });
}
public override void ShowActors(List<SceneGraphNode> selection, ref Quaternion orientation)
{
ShowActors(selection.ConvertAll(x => (Actor)x.EditableObject));
}
public override void UpdateView(float dt, ref Vector3 moveDelta, ref Float2 mouseDelta, out bool centerMouse)
{
centerMouse = false;
}
}
/// <summary>
/// Root control for UI Controls presentation in the game/prefab viewport.
/// </summary>
[HideInEditor]
internal class UIEditorRoot : InputsPassThrough
{
/// <summary>
/// View for the UI structure to be linked in for camera zoom and panning operations.
/// </summary>
private sealed class View : ContainerControl
{
public View(UIEditorRoot parent)
{
AutoFocus = false;
ClipChildren = false;
CullChildren = false;
Pivot = Float2.Zero;
Size = new Float2(1920, 1080);
Parent = parent;
}
public override bool RayCast(ref Float2 location, out Control hit)
{
// Ignore self
return RayCastChildren(ref location, out hit);
}
public override bool IntersectsContent(ref Float2 locationParent, out Float2 location)
{
location = PointFromParent(ref locationParent);
return true;
}
public override void DrawSelf()
{
var uiRoot = (UIEditorRoot)Parent;
if (!uiRoot.EnableBackground)
return;
// Draw canvas area
var bounds = new Rectangle(Float2.Zero, Size);
Render2D.FillRectangle(bounds, new Color(0, 0, 0, 0.2f));
}
}
/// <summary>
/// Cached placement of the widget used to size/edit control
/// </summary>
private struct Widget
{
public UIControl UIControl;
public Rectangle Bounds;
public Float2 ResizeAxis;
public CursorType Cursor;
}
private bool _mouseMovesControl, _mouseMovesView, _mouseMovesWidget;
private Float2 _mouseMovesPos, _moveSnapDelta;
private float _mouseMoveSum;
private UndoMultiBlock _undoBlock;
private View _view;
private float[] _gridTickSteps = Utilities.Utils.CurveTickSteps, _gridTickStrengths;
private List<Widget> _widgets;
private Widget _activeWidget;
/// <summary>
/// True if enable displaying UI editing background and grid elements.
/// </summary>
public virtual bool EnableBackground => false;
/// <summary>
/// True if enable selecting controls with mouse button.
/// </summary>
public virtual bool EnableSelecting => false;
/// <summary>
/// True if enable panning and zooming the view.
/// </summary>
public bool EnableCamera => _view != null && EnableBackground;
/// <summary>
/// Transform gizmo to use sync with (selection, snapping, transformation settings).
/// </summary>
public virtual TransformGizmo TransformGizmo => null;
/// <summary>
/// The root control for controls to be linked in.
/// </summary>
public readonly ContainerControl UIRoot;
internal Float2 ViewPosition
{
get => _view.Location / -ViewScale;
set => _view.Location = value * -ViewScale;
}
internal Float2 ViewCenterPosition
{
get => (_view.Location - Size * 0.5f) / -ViewScale;
set => _view.Location = Size * 0.5f + value * -ViewScale;
}
internal float ViewScale
{
get => _view?.Scale.X ?? 1;
set
{
if (_view == null)
return;
value = Mathf.Clamp(value, 0.1f, 4.0f);
_view.Scale = new Float2(value);
}
}
public UIEditorRoot(bool enableCamera = false)
{
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;
AutoFocus = false;
UIRoot = this;
CullChildren = false;
ClipChildren = true;
if (enableCamera)
{
_view = new View(this);
UIRoot = _view;
}
}
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
var transformGizmo = TransformGizmo;
var owner = transformGizmo?.Owner;
if (_widgets != null && _widgets.Count != 0 && button == MouseButton.Left)
{
foreach (var widget in _widgets)
{
if (widget.Bounds.Contains(ref location))
{
// Initialize widget movement
_activeWidget = widget;
_mouseMovesWidget = true;
_mouseMovesPos = location;
Cursor = widget.Cursor;
StartUndo();
Focus();
StartMouseCapture();
return true;
}
}
}
if (EnableSelecting && owner != null && !_mouseMovesControl && button == MouseButton.Left)
{
// Raycast the control under the mouse
var mousePos = PointFromWindow(RootWindow.MousePosition);
if (RayCastControl(ref mousePos, out var hitControl))
{
var uiControlNode = FindUIControlNode(hitControl);
if (uiControlNode != null)
{
// Select node (with additive mode)
var selection = new List<SceneGraphNode>();
if (Root.GetKey(KeyboardKeys.Control))
{
// Add/remove from selection
selection.AddRange(transformGizmo.Selection);
if (transformGizmo.Selection.Contains(uiControlNode))
selection.Remove(uiControlNode);
else
selection.Add(uiControlNode);
}
else
{
// Select
selection.Add(uiControlNode);
}
owner.Select(selection);
// Initialize control movement
_mouseMovesControl = true;
_mouseMovesPos = location;
_mouseMoveSum = 0.0f;
_moveSnapDelta = Float2.Zero;
Focus();
StartMouseCapture();
return true;
}
}
// Allow deselecting if user clicks on nothing
else
{
owner.Select(null);
}
}
if (EnableCamera && (button == MouseButton.Right || button == MouseButton.Middle))
{
// Initialize surface movement
_mouseMovesView = true;
_mouseMovesPos = location;
_mouseMoveSum = 0.0f;
Focus();
StartMouseCapture();
return true;
}
return Focus(this);
}
public override void OnMouseMove(Float2 location)
{
base.OnMouseMove(location);
// Change cursor if mouse is over active control widget
bool cursorChanged = false;
if (_widgets != null && _widgets.Count != 0 && !_mouseMovesControl && !_mouseMovesWidget && !_mouseMovesView)
{
foreach (var widget in _widgets)
{
if (widget.Bounds.Contains(ref location))
{
Cursor = widget.Cursor;
cursorChanged = true;
}
else if (Cursor != CursorType.Default && !cursorChanged)
{
Cursor = CursorType.Default;
}
}
}
var transformGizmo = TransformGizmo;
if (_mouseMovesControl && transformGizmo != null)
{
// Calculate transform delta
var delta = location - _mouseMovesPos;
if (transformGizmo.TranslationSnapEnable || transformGizmo.Owner.UseSnapping)
{
_moveSnapDelta += delta;
delta = Float2.SnapToGrid(_moveSnapDelta, new Float2(transformGizmo.TranslationSnapValue * ViewScale));
_moveSnapDelta -= delta;
}
// Move selected controls
if (delta.LengthSquared > 0.0f)
{
StartUndo();
var moved = false;
var moveLocation = _mouseMovesPos + delta;
var selection = transformGizmo.Selection;
for (var i = 0; i < selection.Count; i++)
{
if (IsValidControl(selection[i], out var uiControl))
{
// Move control (handle any control transformations by moving in editor's local-space)
var control = uiControl.Control;
var localLocation = control.LocalLocation;
var uiControlDelta = GetControlDelta(control, ref _mouseMovesPos, ref moveLocation);
control.LocalLocation = localLocation + uiControlDelta;
// Don't move if layout doesn't allow it
if (control.Parent != null)
control.Parent.PerformLayout();
else
control.PerformLayout();
// Check if control was moved (parent container could block it)
if (localLocation != control.LocalLocation)
moved = true;
}
}
_mouseMovesPos = location;
_mouseMoveSum += delta.Length;
if (moved)
Cursor = CursorType.SizeAll;
}
}
if (_mouseMovesWidget && _activeWidget.UIControl)
{
// Calculate transform delta
var resizeAxisAbs = _activeWidget.ResizeAxis.Absolute;
var resizeAxisPos = Float2.Clamp(_activeWidget.ResizeAxis, Float2.Zero, Float2.One);
var resizeAxisNeg = Float2.Clamp(-_activeWidget.ResizeAxis, Float2.Zero, Float2.One);
var delta = location - _mouseMovesPos;
// TODO: scale/size snapping?
delta *= resizeAxisAbs;
// Resize control via widget
var moveLocation = _mouseMovesPos + delta;
var control = _activeWidget.UIControl.Control;
var uiControlDelta = GetControlDelta(control, ref _mouseMovesPos, ref moveLocation);
control.LocalLocation += uiControlDelta * resizeAxisNeg;
control.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg;
// Don't move if layout doesn't allow it
if (control.Parent != null)
control.Parent.PerformLayout();
else
control.PerformLayout();
_mouseMovesPos = location;
}
if (_mouseMovesView)
{
// Move view
var delta = location - _mouseMovesPos;
if (delta.LengthSquared > 4.0f)
{
_mouseMovesPos = location;
_mouseMoveSum += delta.Length;
_view.Location += delta;
Cursor = CursorType.SizeAll;
}
}
}
public override bool OnMouseUp(Float2 location, MouseButton button)
{
EndMovingControls();
EndMovingWidget();
if (_mouseMovesView)
{
EndMovingView();
if (button == MouseButton.Right && _mouseMoveSum < 2.0f)
TransformGizmo.Owner.OpenContextMenu();
}
return base.OnMouseUp(location, button);
}
public override void OnMouseLeave()
{
EndMovingControls();
EndMovingView();
EndMovingWidget();
base.OnMouseLeave();
}
public override void OnLostFocus()
{
EndMovingControls();
EndMovingView();
EndMovingWidget();
base.OnLostFocus();
}
public override bool OnMouseWheel(Float2 location, float delta)
{
if (base.OnMouseWheel(location, delta))
return true;
if (EnableCamera && !_mouseMovesControl)
{
// Zoom view
var nextViewScale = ViewScale + delta * 0.1f;
if (delta > 0 && !_mouseMovesControl)
{
// Scale towards mouse when zooming in
var nextCenterPosition = ViewPosition + location / ViewScale;
ViewScale = nextViewScale;
ViewPosition = nextCenterPosition - (location / ViewScale);
}
else
{
// Scale while keeping center position when zooming out or when dragging view
var viewCenter = ViewCenterPosition;
ViewScale = nextViewScale;
ViewCenterPosition = viewCenter;
}
return true;
}
return false;
}
public override void Draw()
{
if (EnableBackground && _view != null)
{
// Draw background
Surface.VisjectSurface.DrawBackgroundDefault(Editor.Instance.UI.VisjectSurfaceBackground, Width, Height);
// Draw grid
var viewRect = GetClientArea();
var upperLeft = _view.PointFromParent(viewRect.Location);
var bottomRight = _view.PointFromParent(viewRect.Size);
var min = Float2.Min(upperLeft, bottomRight);
var max = Float2.Max(upperLeft, bottomRight);
var pixelRange = (max - min) * ViewScale;
Render2D.PushClip(ref viewRect);
DrawAxis(Float2.UnitX, viewRect, min.X, max.X, pixelRange.X);
DrawAxis(Float2.UnitY, viewRect, min.Y, max.Y, pixelRange.Y);
Render2D.PopClip();
}
base.Draw();
if (!_mouseMovesWidget)
{
// Clear widgets to collect them during drawing
_widgets?.Clear();
}
bool drawAnySelectedControl = false;
var transformGizmo = TransformGizmo;
var mousePos = PointFromWindow(RootWindow.MousePosition);
if (transformGizmo != null)
{
// Selected UI controls outline
var selection = transformGizmo.Selection;
for (var i = 0; i < selection.Count; i++)
{
if (IsValidControl(selection[i], out var controlActor))
{
DrawControl(controlActor, controlActor.Control, true, ref mousePos, ref drawAnySelectedControl, EnableSelecting);
}
}
}
if (EnableSelecting && !_mouseMovesControl && !_mouseMovesWidget && IsMouseOver)
{
// Highlight control under mouse for easier selecting (except if already selected)
if (RayCastControl(ref mousePos, out var hitControl) &&
(transformGizmo == null || !transformGizmo.Selection.Any(x => x.EditableObject is UIControl controlActor && controlActor.Control == hitControl)))
{
DrawControl(null, hitControl, false, ref mousePos, ref drawAnySelectedControl);
}
}
if (drawAnySelectedControl)
Render2D.PopTransform();
if (EnableBackground)
{
// Draw border
if (ContainsFocus)
{
Render2D.DrawRectangle(new Rectangle(1, 1, Width - 2, Height - 2), Editor.IsPlayMode ? Color.OrangeRed : Style.Current.BackgroundSelected);
}
}
}
public override void OnDestroy()
{
if (IsDisposing)
return;
EndMovingControls();
EndMovingView();
EndMovingWidget();
base.OnDestroy();
}
private Float2 GetControlDelta(Control control, ref Float2 start, ref Float2 end)
{
var pointOrigin = control.Parent ?? control;
var startPos = pointOrigin.PointFromParent(this, start);
var endPos = pointOrigin.PointFromParent(this, end);
return endPos - startPos;
}
private void DrawAxis(Float2 axis, Rectangle viewRect, float min, float max, float pixelRange)
{
var style = Style.Current;
var linesColor = style.ForegroundDisabled.RGBMultiplied(0.5f);
var labelsColor = style.ForegroundDisabled;
var labelsSize = 10.0f;
Utilities.Utils.DrawCurveTicks((float tick, float strength) =>
{
var p = _view.PointToParent(axis * tick);
// Draw line
var lineRect = new Rectangle
(
viewRect.Location + (p - 0.5f) * axis,
Float2.Lerp(viewRect.Size, Float2.One, axis)
);
Render2D.FillRectangle(lineRect, linesColor.AlphaMultiplied(strength));
// Draw label
string label = tick.ToString(System.Globalization.CultureInfo.InvariantCulture);
var labelRect = new Rectangle
(
viewRect.X + 4.0f + (p.X * axis.X),
viewRect.Y - labelsSize + (p.Y * axis.Y) + (viewRect.Size.Y * axis.X),
50,
labelsSize
);
Render2D.DrawText(style.FontSmall, label, labelRect, labelsColor.AlphaMultiplied(strength), TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 0.7f);
}, _gridTickSteps, ref _gridTickStrengths, min, max, pixelRange);
}
private void DrawControl(UIControl uiControl, Control control, bool selection, ref Float2 mousePos, ref bool drawAnySelectedControl, bool withWidgets = false)
{
if (!drawAnySelectedControl)
{
drawAnySelectedControl = true;
Render2D.PushTransform(ref _cachedTransform);
}
var options = Editor.Instance.Options.Options.Visual;
// Draw bounds
var bounds = control.EditorBounds;
var ul = control.PointToParent(this, bounds.UpperLeft);
var ur = control.PointToParent(this, bounds.UpperRight);
var bl = control.PointToParent(this, bounds.BottomLeft);
var br = control.PointToParent(this, bounds.BottomRight);
var color = selection ? options.SelectionOutlineColor0 : Style.Current.SelectionBorder;
#if false
// AABB
var min = Float2.Min(Float2.Min(ul, ur), Float2.Min(bl, br));
var max = Float2.Max(Float2.Max(ul, ur), Float2.Max(bl, br));
bounds = new Rectangle(min, Float2.Max(max - min, Float2.Zero));
Render2D.DrawRectangle(bounds, color, options.UISelectionOutlineSize);
#else
// OBB
Render2D.DrawLine(ul, ur, color, options.UISelectionOutlineSize);
Render2D.DrawLine(ur, br, color, options.UISelectionOutlineSize);
Render2D.DrawLine(br, bl, color, options.UISelectionOutlineSize);
Render2D.DrawLine(bl, ul, color, options.UISelectionOutlineSize);
#endif
if (withWidgets)
{
// Draw sizing widgets
if (_widgets == null)
_widgets = new List<Widget>();
var widgetSize = 8.0f;
var viewScale = ViewScale;
if (viewScale < 0.7f)
widgetSize *= viewScale;
var controlSize = control.Size.Absolute.MinValue / 50.0f;
if (controlSize < 1.0f)
widgetSize *= Mathf.Clamp(controlSize + 0.1f, 0.1f, 1.0f);
var cornerSize = new Float2(widgetSize);
DrawControlWidget(uiControl, ref ul, ref mousePos, ref cornerSize, new Float2(-1, -1), CursorType.SizeNWSE);
DrawControlWidget(uiControl, ref ur, ref mousePos, ref cornerSize, new Float2(1, -1), CursorType.SizeNESW);
DrawControlWidget(uiControl, ref bl, ref mousePos, ref cornerSize, new Float2(-1, 1), CursorType.SizeNESW);
DrawControlWidget(uiControl, ref br, ref mousePos, ref cornerSize, new Float2(1, 1), CursorType.SizeNWSE);
var edgeSizeV = new Float2(widgetSize * 2, widgetSize);
var edgeSizeH = new Float2(edgeSizeV.Y, edgeSizeV.X);
Float2.Lerp(ref ul, ref bl, 0.5f, out var el);
Float2.Lerp(ref ur, ref br, 0.5f, out var er);
Float2.Lerp(ref ul, ref ur, 0.5f, out var eu);
Float2.Lerp(ref bl, ref br, 0.5f, out var eb);
DrawControlWidget(uiControl, ref el, ref mousePos, ref edgeSizeH, new Float2(-1, 0), CursorType.SizeWE);
DrawControlWidget(uiControl, ref er, ref mousePos, ref edgeSizeH, new Float2(1, 0), CursorType.SizeWE);
DrawControlWidget(uiControl, ref eu, ref mousePos, ref edgeSizeV, new Float2(0, -1), CursorType.SizeNS);
DrawControlWidget(uiControl, ref eb, ref mousePos, ref edgeSizeV, new Float2(0, 1), CursorType.SizeNS);
// TODO: draw anchors
}
}
private void DrawControlWidget(UIControl uiControl, ref Float2 pos, ref Float2 mousePos, ref Float2 size, Float2 resizeAxis, CursorType cursor)
{
var style = Style.Current;
var rect = new Rectangle(pos - size * 0.5f, size);
if (rect.Contains(ref mousePos))
{
Render2D.FillRectangle(rect, style.Foreground);
}
else
{
Render2D.FillRectangle(rect, style.ForegroundGrey);
Render2D.DrawRectangle(rect, style.Foreground);
}
if (!_mouseMovesWidget && uiControl != null)
{
// Collect widget
_widgets.Add(new Widget
{
UIControl = uiControl,
Bounds = rect,
ResizeAxis = resizeAxis,
Cursor = cursor,
});
}
}
private bool IsValidControl(SceneGraphNode node, out UIControl uiControl)
{
uiControl = null;
if (node.EditableObject is UIControl controlActor)
uiControl = controlActor;
return uiControl != null &&
uiControl.Control != null &&
uiControl.Control.VisibleInHierarchy &&
uiControl.Control.RootWindow != null;
}
private bool RayCastControl(ref Float2 location, out Control hit)
{
#if false
// Raycast only controls with content (eg. skips transparent panels)
return RayCastChildren(ref location, out hit);
#else
// Find any control under mouse (hierarchical)
hit = GetChildAtRecursive(location);
if (hit is View || hit is CanvasContainer)
hit = null;
return hit != null;
#endif
}
private UIControlNode FindUIControlNode(Control control)
{
return FindUIControlNode(TransformGizmo.Owner.SceneGraphRoot, control);
}
private UIControlNode FindUIControlNode(SceneGraphNode node, Control control)
{
var result = node as UIControlNode;
if (result != null && ((UIControl)result.Actor).Control == control)
return result;
foreach (var e in node.ChildNodes)
{
result = FindUIControlNode(e, control);
if (result != null)
return result;
}
return null;
}
private void StartUndo()
{
var undo = TransformGizmo?.Owner?.Undo;
if (undo == null || _undoBlock != null)
return;
_undoBlock = new UndoMultiBlock(undo, TransformGizmo.Selection.ConvertAll(x => x.EditableObject), "Edit control");
}
private void EndUndo()
{
if (_undoBlock == null)
return;
_undoBlock.Dispose();
_undoBlock = null;
}
private void EndMovingControls()
{
if (!_mouseMovesControl)
return;
_mouseMovesControl = false;
EndMouseCapture();
Cursor = CursorType.Default;
EndUndo();
}
private void EndMovingView()
{
if (!_mouseMovesView)
return;
_mouseMovesView = false;
EndMouseCapture();
Cursor = CursorType.Default;
}
private void EndMovingWidget()
{
if (!_mouseMovesWidget)
return;
_mouseMovesWidget = false;
_activeWidget = new Widget();
EndMouseCapture();
Cursor = CursorType.Default;
EndUndo();
}
}
/// <summary>
/// Control that can optionally disable inputs to the children.
/// </summary>
[HideInEditor]
internal class InputsPassThrough : ContainerControl
{
private bool _isMouseOver;
/// <summary>
/// True if enable input events passing to the UI.
/// </summary>
public virtual bool EnableInputs => true;
public override bool RayCast(ref Float2 location, out Control hit)
{
return RayCastChildren(ref location, out hit);
}
public override bool ContainsPoint(ref Float2 location, bool precise = false)
{
if (precise)
return false;
return base.ContainsPoint(ref location, precise);
}
public override bool OnCharInput(char c)
{
if (!EnableInputs)
return false;
return base.OnCharInput(c);
}
public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
{
if (!EnableInputs)
return DragDropEffect.None;
return base.OnDragDrop(ref location, data);
}
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
if (!EnableInputs)
return DragDropEffect.None;
return base.OnDragEnter(ref location, data);
}
public override void OnDragLeave()
{
if (!EnableInputs)
return;
base.OnDragLeave();
}
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
if (!EnableInputs)
return DragDropEffect.None;
return base.OnDragMove(ref location, data);
}
public override bool OnKeyDown(KeyboardKeys key)
{
if (!EnableInputs)
return false;
return base.OnKeyDown(key);
}
public override void OnKeyUp(KeyboardKeys key)
{
if (!EnableInputs)
return;
base.OnKeyUp(key);
}
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
{
if (!EnableInputs)
return false;
return base.OnMouseDoubleClick(location, button);
}
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (!EnableInputs)
return false;
return base.OnMouseDown(location, button);
}
public override bool IsMouseOver => _isMouseOver;
public override void OnMouseEnter(Float2 location)
{
_isMouseOver = true;
if (!EnableInputs)
return;
base.OnMouseEnter(location);
}
public override void OnMouseLeave()
{
_isMouseOver = false;
if (!EnableInputs)
return;
base.OnMouseLeave();
}
public override void OnMouseMove(Float2 location)
{
if (!EnableInputs)
return;
base.OnMouseMove(location);
}
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (!EnableInputs)
return false;
return base.OnMouseUp(location, button);
}
public override bool OnMouseWheel(Float2 location, float delta)
{
if (!EnableInputs)
return false;
return base.OnMouseWheel(location, delta);
}
}
}

View File

@@ -263,22 +263,22 @@ DEFINE_INTERNAL_CALL(MString*) EditorInternal_GetShaderAssetSourceCode(BinaryAss
INTERNAL_CALL_CHECK_RETURN(obj, nullptr);
if (obj->WaitForLoaded())
DebugLog::ThrowNullReference();
auto lock = obj->Storage->Lock();
if (obj->LoadChunk(SHADER_FILE_CHUNK_SOURCE))
return nullptr;
// Decrypt source code
BytesContainer data;
obj->GetChunkData(SHADER_FILE_CHUNK_SOURCE, data);
auto source = data.Get<char>();
auto sourceLength = data.Length();
Encryption::DecryptBytes(data.Get(), data.Length());
source[sourceLength - 1] = 0;
Encryption::DecryptBytes((byte*)data.Get(), data.Length());
// Get source and encrypt it back
const StringAnsiView srcData((const char*)data.Get(), data.Length());
const String source(srcData);
const auto str = MUtils::ToString(source);
Encryption::EncryptBytes((byte*)data.Get(), data.Length());
const auto str = MUtils::ToString(srcData);
Encryption::EncryptBytes(data.Get(), data.Length());
return str;
}

View File

@@ -1106,6 +1106,7 @@ namespace FlaxEditor.Modules
Proxy.Add(new VisualScriptProxy());
Proxy.Add(new BehaviorTreeProxy());
Proxy.Add(new LocalizedStringTableProxy());
Proxy.Add(new WidgetProxy());
Proxy.Add(new FileProxy());
Proxy.Add(new SpawnableJsonAssetProxy<PhysicalMaterial>());

View File

@@ -101,7 +101,10 @@ namespace FlaxEditor.Modules
public void Select(List<SceneGraphNode> selection, bool additive = false)
{
if (selection == null)
throw new ArgumentNullException();
{
Deselect();
return;
}
// Prevent from selecting null nodes
selection.RemoveAll(x => x == null);

View File

@@ -6,6 +6,7 @@ using System.IO;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Modules
@@ -454,6 +455,41 @@ namespace FlaxEditor.Modules
Profiler.EndEvent();
}
private Dictionary<ContainerControl, Float2> _uiRootSizes;
internal void OnSaveStart(ContainerControl uiRoot)
{
// Force viewport UI to have fixed size during scene/prefabs saving to result in stable data (less mess in version control diffs)
if (_uiRootSizes == null)
_uiRootSizes = new Dictionary<ContainerControl, Float2>();
_uiRootSizes[uiRoot] = uiRoot.Size;
uiRoot.Size = new Float2(1920, 1080);
}
internal void OnSaveEnd(ContainerControl uiRoot)
{
// Restore cached size of the UI root container
if (_uiRootSizes != null && _uiRootSizes.Remove(uiRoot, out var size))
{
uiRoot.Size = size;
}
}
private void OnSceneSaving(Scene scene, Guid sceneId)
{
OnSaveStart(RootControl.GameRoot);
}
private void OnSceneSaved(Scene scene, Guid sceneId)
{
OnSaveEnd(RootControl.GameRoot);
}
private void OnSceneSaveError(Scene scene, Guid sceneId)
{
OnSaveEnd(RootControl.GameRoot);
}
private void OnSceneLoaded(Scene scene, Guid sceneId)
{
var startTime = DateTime.UtcNow;
@@ -659,6 +695,9 @@ namespace FlaxEditor.Modules
Root = new ScenesRootNode();
// Bind events
Level.SceneSaving += OnSceneSaving;
Level.SceneSaved += OnSceneSaved;
Level.SceneSaveError += OnSceneSaveError;
Level.SceneLoaded += OnSceneLoaded;
Level.SceneUnloading += OnSceneUnloading;
Level.ActorSpawned += OnActorSpawned;
@@ -673,6 +712,9 @@ namespace FlaxEditor.Modules
public override void OnExit()
{
// Unbind events
Level.SceneSaving -= OnSceneSaving;
Level.SceneSaved -= OnSceneSaved;
Level.SceneSaveError -= OnSceneSaveError;
Level.SceneLoaded -= OnSceneLoaded;
Level.SceneUnloading -= OnSceneUnloading;
Level.ActorSpawned -= OnActorSpawned;

View File

@@ -201,8 +201,8 @@ namespace FlaxEditor.Options
/// <returns>True if input has been processed, otherwise false.</returns>
public bool Process(Control control)
{
var root = control.Root;
return root.GetKey(Key) && ProcessModifiers(control);
var root = control?.Root;
return root != null && root.GetKey(Key) && ProcessModifiers(control);
}
/// <summary>

View File

@@ -2,6 +2,7 @@
using System.ComponentModel;
using FlaxEditor.GUI.Docking;
using FlaxEditor.Utilities;
using FlaxEngine;
namespace FlaxEditor.Options
@@ -116,6 +117,27 @@ namespace FlaxEditor.Options
BorderlessWindow,
}
/// <summary>
/// Options for formatting numerical values.
/// </summary>
public enum ValueFormattingType
{
/// <summary>
/// No formatting.
/// </summary>
None,
/// <summary>
/// Format using the base SI unit.
/// </summary>
BaseUnit,
/// <summary>
/// Format using a unit that matches the value best.
/// </summary>
AutoUnit,
}
/// <summary>
/// Gets or sets the Editor User Interface scale. Applied to all UI elements, windows and text. Can be used to scale the interface up on a bigger display. Editor restart required.
/// </summary>
@@ -174,6 +196,20 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(280), Tooltip("Editor content window orientation.")]
public FlaxEngine.GUI.Orientation ContentWindowOrientation { get; set; } = FlaxEngine.GUI.Orientation.Horizontal;
/// <summary>
/// Gets or sets the formatting option for numeric values in the editor.
/// </summary>
[DefaultValue(ValueFormattingType.None)]
[EditorDisplay("Interface"), EditorOrder(300)]
public ValueFormattingType ValueFormatting { get; set; }
/// <summary>
/// Gets or sets the option to put a space between numbers and units for unit formatting.
/// </summary>
[DefaultValue(false)]
[EditorDisplay("Interface"), EditorOrder(310)]
public bool SeparateValueAndUnit { get; set; }
/// <summary>
/// Gets or sets the timestamps prefix mode for output log messages.
/// </summary>

View File

@@ -200,6 +200,27 @@ namespace FlaxEditor.Options
EditorAssets.Cache.OnEditorOptionsChanged(Options);
// Units formatting options
bool useUnitsFormatting = Options.Interface.ValueFormatting != InterfaceOptions.ValueFormattingType.None;
bool automaticUnitsFormatting = Options.Interface.ValueFormatting == InterfaceOptions.ValueFormattingType.AutoUnit;
bool separateValueAndUnit = Options.Interface.SeparateValueAndUnit;
if (useUnitsFormatting != Utilities.Units.UseUnitsFormatting ||
automaticUnitsFormatting != Utilities.Units.AutomaticUnitsFormatting ||
separateValueAndUnit != Utilities.Units.SeparateValueAndUnit)
{
Utilities.Units.UseUnitsFormatting = useUnitsFormatting;
Utilities.Units.AutomaticUnitsFormatting = automaticUnitsFormatting;
Utilities.Units.SeparateValueAndUnit = separateValueAndUnit;
// Refresh UI in property panels
Editor.Windows.PropertiesWin?.Presenter.BuildLayoutOnUpdate();
foreach (var window in Editor.Windows.Windows)
{
if (window is Windows.Assets.PrefabWindow prefabWindow)
prefabWindow.Presenter.BuildLayoutOnUpdate();
}
}
// Send event
OptionsChanged?.Invoke(Options);
}

View File

@@ -308,11 +308,14 @@ namespace FlaxEditor.SceneGraph.Actors
var selection = Editor.Instance.SceneEditing.Selection;
if (selection.Count == 1 && selection[0] is SplinePointNode selectedPoint && selectedPoint.ParentNode == this)
{
if (Input.Keyboard.GetKey(KeyboardKeys.Shift))
var mouse = Input.Mouse;
var keyboard = Input.Keyboard;
if (keyboard.GetKey(KeyboardKeys.Shift))
EditSplineWithSnap(selectedPoint);
var canAddSplinePoint = Input.Mouse.PositionDelta == Float2.Zero && Input.Mouse.Position != Float2.Zero;
var requestAddSplinePoint = Input.Keyboard.GetKey(KeyboardKeys.Control) && Input.Mouse.GetButtonDown(MouseButton.Right);
var canAddSplinePoint = mouse.PositionDelta == Float2.Zero && mouse.Position != Float2.Zero;
var requestAddSplinePoint = Input.Keyboard.GetKey(KeyboardKeys.Control) && mouse.GetButtonDown(MouseButton.Right);
if (requestAddSplinePoint && canAddSplinePoint)
AddSplinePoint(selectedPoint);
}

View File

@@ -11,6 +11,7 @@ using System.Collections.Generic;
using FlaxEditor.Content;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Windows;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
namespace FlaxEditor.SceneGraph.Actors
@@ -84,76 +85,109 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.OnContextMenu(contextMenu, window);
contextMenu.AddButton("Add collider", OnAddMeshCollider).Enabled = ((StaticModel)Actor).Model != null;
contextMenu.AddButton("Add collider", () => OnAddMeshCollider(window)).Enabled = ((StaticModel)Actor).Model != null;
}
private void OnAddMeshCollider()
private void OnAddMeshCollider(EditorWindow window)
{
var model = ((StaticModel)Actor).Model;
if (!model)
return;
// Special case for in-built Editor models that can use analytical collision
var modelPath = model.Path;
if (modelPath.EndsWith("/Primitives/Cube.flax", StringComparison.Ordinal))
// Allow collider to be added to evey static model selection
SceneGraphNode[] selection = Array.Empty<SceneGraphNode>();
if (window is SceneTreeWindow)
{
var actor = new BoxCollider
{
StaticFlags = Actor.StaticFlags,
Transform = Actor.Transform,
};
Root.Spawn(actor, Actor);
return;
selection = Editor.Instance.SceneEditing.Selection.ToArray();
}
if (modelPath.EndsWith("/Primitives/Sphere.flax", StringComparison.Ordinal))
else if (window is PrefabWindow prefabWindow)
{
var actor = new SphereCollider
{
StaticFlags = Actor.StaticFlags,
Transform = Actor.Transform,
};
Root.Spawn(actor, Actor);
return;
}
if (modelPath.EndsWith("/Primitives/Plane.flax", StringComparison.Ordinal))
{
var actor = new BoxCollider
{
StaticFlags = Actor.StaticFlags,
Transform = Actor.Transform,
Size = new Float3(100.0f, 100.0f, 1.0f),
};
Root.Spawn(actor, Actor);
return;
}
if (modelPath.EndsWith("/Primitives/Capsule.flax", StringComparison.Ordinal))
{
var actor = new CapsuleCollider
{
StaticFlags = Actor.StaticFlags,
Transform = Actor.Transform,
Radius = 25.0f,
Height = 50.0f,
};
Editor.Instance.SceneEditing.Spawn(actor, Actor);
actor.LocalPosition = new Vector3(0, 50.0f, 0);
actor.LocalOrientation = Quaternion.Euler(0, 0, 90.0f);
return;
selection = prefabWindow.Selection.ToArray();
}
// Create collision data (or reuse) and add collision actor
Action<CollisionData> created = collisionData =>
var createdNodes = new List<SceneGraphNode>();
foreach (var node in selection)
{
var actor = new MeshCollider
if (node is not StaticModelNode staticModelNode)
continue;
var model = ((StaticModel)staticModelNode.Actor).Model;
if (!model)
continue;
// Special case for in-built Editor models that can use analytical collision
var modelPath = model.Path;
if (modelPath.EndsWith("/Primitives/Cube.flax", StringComparison.Ordinal))
{
StaticFlags = Actor.StaticFlags,
Transform = Actor.Transform,
CollisionData = collisionData,
var actor = new BoxCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
if (modelPath.EndsWith("/Primitives/Sphere.flax", StringComparison.Ordinal))
{
var actor = new SphereCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
if (modelPath.EndsWith("/Primitives/Plane.flax", StringComparison.Ordinal))
{
var actor = new BoxCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
Size = new Float3(100.0f, 100.0f, 1.0f),
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
if (modelPath.EndsWith("/Primitives/Capsule.flax", StringComparison.Ordinal))
{
var actor = new CapsuleCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
Radius = 25.0f,
Height = 50.0f,
};
Editor.Instance.SceneEditing.Spawn(actor, staticModelNode.Actor);
actor.LocalPosition = new Vector3(0, 50.0f, 0);
actor.LocalOrientation = Quaternion.Euler(0, 0, 90.0f);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
continue;
}
// Create collision data (or reuse) and add collision actor
Action<CollisionData> created = collisionData =>
{
var actor = new MeshCollider
{
StaticFlags = staticModelNode.Actor.StaticFlags,
Transform = staticModelNode.Actor.Transform,
CollisionData = collisionData,
};
staticModelNode.Root.Spawn(actor, staticModelNode.Actor);
createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor));
};
Root.Spawn(actor, Actor);
};
var collisionDataProxy = (CollisionDataProxy)Editor.Instance.ContentDatabase.GetProxy<CollisionData>();
collisionDataProxy.CreateCollisionDataFromModel(model, created);
var collisionDataProxy = (CollisionDataProxy)Editor.Instance.ContentDatabase.GetProxy<CollisionData>();
collisionDataProxy.CreateCollisionDataFromModel(model, created, selection.Length == 1);
}
// Select all created nodes
if (window is SceneTreeWindow)
{
Editor.Instance.SceneEditing.Select(createdNodes);
}
else if (window is PrefabWindow pWindow)
{
pWindow.Select(createdNodes);
}
}
}
}

View File

@@ -617,8 +617,9 @@ namespace FlaxEditor.Surface.Archetypes
public override void SetLocation(int index, Float2 location)
{
var dataA = (Float4)_node.Values[4 + index * 2];
var ranges = (Float4)_node.Values[0];
dataA.X = location.X;
dataA.X = Mathf.Clamp(location.X, ranges.X, ranges.Y);
_node.Values[4 + index * 2] = dataA;
_node.Surface.MarkAsEdited();
@@ -750,9 +751,10 @@ namespace FlaxEditor.Surface.Archetypes
public override void SetLocation(int index, Float2 location)
{
var dataA = (Float4)_node.Values[4 + index * 2];
var ranges = (Float4)_node.Values[0];
dataA.X = location.X;
dataA.Y = location.Y;
dataA.X = Mathf.Clamp(location.X, ranges.X, ranges.Y);
dataA.Y = Mathf.Clamp(location.Y, ranges.Z, ranges.W);
_node.Values[4 + index * 2] = dataA;
_node.Surface.MarkAsEdited();

View File

@@ -117,17 +117,9 @@ namespace FlaxEditor.Surface
editor.Panel.Tag = attributeType;
_presenter = editor;
using (var stream = new MemoryStream())
{
// Ensure we are in the correct load context (https://github.com/dotnet/runtime/issues/42041)
using var ctx = AssemblyLoadContext.EnterContextualReflection(typeof(Editor).Assembly);
// Cache 'previous' state to check if attributes were edited after operation
_oldData = SurfaceMeta.GetAttributesData(attributes);
var formatter = new BinaryFormatter();
#pragma warning disable SYSLIB0011
formatter.Serialize(stream, attributes);
#pragma warning restore SYSLIB0011
_oldData = stream.ToArray();
}
editor.Select(new Proxy
{
Value = attributes,
@@ -145,20 +137,11 @@ namespace FlaxEditor.Surface
return;
}
}
using (var stream = new MemoryStream())
{
// Ensure we are in the correct load context (https://github.com/dotnet/runtime/issues/42041)
using var ctx = AssemblyLoadContext.EnterContextualReflection(typeof(Editor).Assembly);
var formatter = new BinaryFormatter();
#pragma warning disable SYSLIB0011
formatter.Serialize(stream, newValue);
#pragma warning restore SYSLIB0011
var newData = stream.ToArray();
if (!_oldData.SequenceEqual(newData))
{
Edited?.Invoke(newValue);
}
var newData = SurfaceMeta.GetAttributesData(newValue);
if (!_oldData.SequenceEqual(newData))
{
Edited?.Invoke(newValue);
}
Hide();

View File

@@ -4,9 +4,9 @@ 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 System.Text;
using FlaxEngine;
namespace FlaxEditor.Surface
@@ -39,28 +39,48 @@ namespace FlaxEditor.Surface
public readonly List<Entry> Entries = new List<Entry>();
/// <summary>
/// The attribute meta type identifier.
/// The attribute meta type identifier. Uses byte[] as storage for Attribute[] serialized with BinaryFormatter (deprecated in .NET 5).
/// [Deprecated on 8.12.2023, expires on 8.12.2025]
/// </summary>
public const int AttributeMetaTypeID = 12;
public const int OldAttributeMetaTypeID = 12;
/// <summary>
/// The attribute meta type identifier. Uses byte[] as storage for Attribute[] serialized with JsonSerializer.
/// </summary>
public const int AttributeMetaTypeID = 13;
/// <summary>
/// Gets the attributes collection from the data.
/// </summary>
/// <param name="data">The graph metadata.</param>
/// <param name="data">The graph metadata serialized with JsonSerializer.</param>
/// <param name="oldData">The graph metadata serialized with BinaryFormatter.</param>
/// <returns>The attributes collection.</returns>
public static Attribute[] GetAttributes(byte[] data)
public static Attribute[] GetAttributes(byte[] data, byte[] oldData)
{
if (data != null && data.Length != 0)
{
using (var stream = new MemoryStream(data))
try
{
var json = Encoding.Unicode.GetString(data);
return FlaxEngine.Json.JsonSerializer.Deserialize<Attribute[]>(json);
}
catch (Exception ex)
{
Editor.LogError("Failed to deserialize Visject attributes array.");
Editor.LogWarning(ex);
}
}
if (oldData != null && oldData.Length != 0)
{
// [Deprecated on 8.12.2023, expires on 8.12.2025]
using (var stream = new MemoryStream(oldData))
{
try
{
// Ensure we are in the correct load context (https://github.com/dotnet/runtime/issues/42041)
using var ctx = AssemblyLoadContext.EnterContextualReflection(typeof(Editor).Assembly);
var formatter = new BinaryFormatter();
#pragma warning disable SYSLIB0011
var formatter = new BinaryFormatter();
return (Attribute[])formatter.Deserialize(stream);
#pragma warning restore SYSLIB0011
}
@@ -74,6 +94,21 @@ namespace FlaxEditor.Surface
return Utils.GetEmptyArray<Attribute>();
}
/// <summary>
/// Serializes surface attributes into byte[] data using the current format.
/// </summary>
/// <param name="attributes">The input attributes.</param>
/// <returns>The result array with bytes. Can be empty but not null.</returns>
internal static byte[] GetAttributesData(Attribute[] attributes)
{
if (attributes != null && attributes.Length != 0)
{
var json = FlaxEngine.Json.JsonSerializer.Serialize(attributes);
return Encoding.Unicode.GetBytes(json);
}
return Utils.GetEmptyArray<byte>();
}
/// <summary>
/// Determines whether the specified attribute was defined for this member.
/// </summary>
@@ -93,7 +128,8 @@ namespace FlaxEditor.Surface
public static Attribute[] GetAttributes(GraphParameter parameter)
{
var data = parameter.GetMetaData(AttributeMetaTypeID);
return GetAttributes(data);
var dataOld = parameter.GetMetaData(OldAttributeMetaTypeID);
return GetAttributes(data, dataOld);
}
/// <summary>
@@ -102,12 +138,7 @@ namespace FlaxEditor.Surface
/// <returns>The attributes collection.</returns>
public Attribute[] GetAttributes()
{
for (int i = 0; i < Entries.Count; i++)
{
if (Entries[i].TypeID == AttributeMetaTypeID)
return GetAttributes(Entries[i].Data);
}
return Utils.GetEmptyArray<Attribute>();
return GetAttributes(GetEntry(AttributeMetaTypeID).Data, GetEntry(OldAttributeMetaTypeID).Data);
}
/// <summary>
@@ -119,25 +150,12 @@ namespace FlaxEditor.Surface
if (attributes == null || attributes.Length == 0)
{
RemoveEntry(AttributeMetaTypeID);
RemoveEntry(OldAttributeMetaTypeID);
}
else
{
for (int i = 0; i < attributes.Length; i++)
{
if (attributes[i] == null)
throw new NullReferenceException("One of the Visject attributes is null.");
}
using (var stream = new MemoryStream())
{
// Ensure we are in the correct load context (https://github.com/dotnet/runtime/issues/42041)
using var ctx = AssemblyLoadContext.EnterContextualReflection(typeof(Editor).Assembly);
var formatter = new BinaryFormatter();
#pragma warning disable SYSLIB0011
formatter.Serialize(stream, attributes);
#pragma warning restore SYSLIB0011
AddEntry(AttributeMetaTypeID, stream.ToArray());
}
AddEntry(AttributeMetaTypeID, GetAttributesData(attributes));
RemoveEntry(OldAttributeMetaTypeID);
}
}
@@ -180,11 +198,11 @@ namespace FlaxEditor.Surface
/// <returns>True if cannot save data</returns>
public void Save(BinaryWriter stream)
{
stream.Write(Entries.Count);
for (int i = 0; i < Entries.Count; i++)
var entries = Entries;
stream.Write(entries.Count);
for (int i = 0; i < entries.Count; i++)
{
Entry e = Entries[i];
Entry e = entries[i];
stream.Write(e.TypeID);
stream.Write((long)0);
@@ -214,14 +232,12 @@ namespace FlaxEditor.Surface
/// <returns>Entry</returns>
public Entry GetEntry(int typeID)
{
for (int i = 0; i < Entries.Count; i++)
var entries = Entries;
for (int i = 0; i < entries.Count; i++)
{
if (Entries[i].TypeID == typeID)
{
return Entries[i];
}
if (entries[i].TypeID == typeID)
return entries[i];
}
return new Entry();
}

View File

@@ -64,7 +64,11 @@ namespace FlaxEditor.Surface
/// </summary>
protected virtual void DrawBackground()
{
var background = Style.Background;
DrawBackgroundDefault(Style.Background, Width, Height);
}
internal static void DrawBackgroundDefault(Texture background, float width, float height)
{
if (background && background.ResidentMipLevels > 0)
{
var bSize = background.Size;
@@ -77,8 +81,8 @@ namespace FlaxEditor.Surface
if (pos.Y > 0)
pos.Y -= bh;
int maxI = Mathf.CeilToInt(Width / bw + 1.0f);
int maxJ = Mathf.CeilToInt(Height / bh + 1.0f);
int maxI = Mathf.CeilToInt(width / bw + 1.0f);
int maxJ = Mathf.CeilToInt(height / bh + 1.0f);
for (int i = 0; i < maxI; i++)
{

View File

@@ -10,6 +10,7 @@
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Core/Config/GameSettings.h"
#include "Engine/Core/Config/BuildSettings.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/AssetReference.h"
#include "Engine/Content/Assets/Texture.h"
@@ -17,7 +18,18 @@
#if PLATFORM_MAC
#include "Engine/Platform/Apple/ApplePlatformSettings.h"
#endif
#include <fstream>
String EditorUtilities::GetOutputName()
{
const auto gameSettings = GameSettings::Get();
const auto buildSettings = BuildSettings::Get();
String outputName = buildSettings->OutputName;
outputName.Replace(TEXT("${PROJECT_NAME}"), *gameSettings->ProductName, StringSearchCase::IgnoreCase);
outputName.Replace(TEXT("${COMPANY_NAME}"), *gameSettings->CompanyName, StringSearchCase::IgnoreCase);
if (outputName.IsEmpty())
outputName = TEXT("FlaxGame");
return outputName;
}
bool EditorUtilities::FormatAppPackageName(String& packageName)
{

View File

@@ -22,6 +22,7 @@ public:
SplashScreen,
};
static String GetOutputName();
static bool FormatAppPackageName(String& packageName);
static bool GetApplicationImage(const Guid& imageId, TextureData& imageData, ApplicationImageType type = ApplicationImageType::Icon);
static bool GetTexture(const Guid& textureId, TextureData& textureData);

View File

@@ -121,6 +121,37 @@ namespace FlaxEditor.Utilities
["e"] = Math.E,
["infinity"] = double.MaxValue,
["-infinity"] = -double.MaxValue,
["m"] = Units.Meters2Units,
["cm"] = Units.Meters2Units / 100,
["km"] = Units.Meters2Units * 1000,
["s"] = 1,
["ms"] = 0.001,
["min"] = 60,
["h"] = 3600,
["cm²"] = (Units.Meters2Units / 100) * (Units.Meters2Units / 100),
["cm³"] = (Units.Meters2Units / 100) * (Units.Meters2Units / 100) * (Units.Meters2Units / 100),
["dm²"] = (Units.Meters2Units / 10) * (Units.Meters2Units / 10),
["dm³"] = (Units.Meters2Units / 10) * (Units.Meters2Units / 10) * (Units.Meters2Units / 10),
["l"] = (Units.Meters2Units / 10) * (Units.Meters2Units / 10) * (Units.Meters2Units / 10),
["m²"] = Units.Meters2Units * Units.Meters2Units,
["m³"] = Units.Meters2Units * Units.Meters2Units * Units.Meters2Units,
["kg"] = 1,
["g"] = 0.001,
["n"] = Units.Meters2Units
};
/// <summary>
/// List known units which cannot be handled as a variable easily because they contain operator symbols (mostly a forward slash). The value is the factor to calculate game units.
/// </summary>
private static readonly IDictionary<string, double> UnitSymbols = new Dictionary<string, double>
{
["cm/s"] = Units.Meters2Units / 100,
["cm/s²"] = Units.Meters2Units / 100,
["m/s"] = Units.Meters2Units,
["m/s²"] = Units.Meters2Units,
["km/h"] = 1 / 3.6 * Units.Meters2Units,
// Nm is here because these values are compared case-sensitive, and we don't want to confuse nanometers and Newtonmeters
["Nm"] = Units.Meters2Units * Units.Meters2Units,
};
/// <summary>
@@ -156,7 +187,7 @@ namespace FlaxEditor.Utilities
if (Operators.ContainsKey(str))
return TokenType.Operator;
if (char.IsLetter(c))
if (char.IsLetter(c) || c == '²' || c == '³')
return TokenType.Variable;
throw new ParsingException("wrong character");
@@ -170,7 +201,24 @@ namespace FlaxEditor.Utilities
public static IEnumerable<Token> Tokenize(string text)
{
// Prepare text
text = text.Replace(',', '.');
text = text.Replace(',', '.').Replace("°", "");
foreach (var kv in UnitSymbols)
{
int idx;
do
{
idx = text.IndexOf(kv.Key, StringComparison.InvariantCulture);
if (idx > 0)
{
if (DetermineType(text[idx - 1]) != TokenType.Number)
throw new ParsingException($"unit found without a number: {kv.Key} at {idx} in {text}");
if (Mathf.Abs(kv.Value - 1) < Mathf.Epsilon)
text = text.Remove(idx, kv.Key.Length);
else
text = text.Replace(kv.Key, "*" + kv.Value);
}
} while (idx > 0);
}
// Necessary to correctly parse negative numbers
var previous = TokenType.WhiteSpace;
@@ -240,6 +288,11 @@ namespace FlaxEditor.Utilities
}
else if (type == TokenType.Variable)
{
if (previous == TokenType.Number)
{
previous = TokenType.Operator;
yield return new Token(TokenType.Operator, "*");
}
// Continue till the end of the variable
while (i + 1 < text.Length && DetermineType(text[i + 1]) == TokenType.Variable)
{
@@ -335,7 +388,7 @@ namespace FlaxEditor.Utilities
}
else
{
throw new ParsingException("unknown variable");
throw new ParsingException($"unknown variable : {token.Value}");
}
}
else
@@ -372,6 +425,15 @@ namespace FlaxEditor.Utilities
}
}
// if stack has more than one item we're not finished with evaluating
// we assume the remaining values are all factors to be multiplied
if (stack.Count > 1)
{
var v1 = stack.Pop();
while (stack.Count > 0)
v1 *= stack.Pop();
return v1;
}
return stack.Pop();
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
namespace FlaxEditor.Utilities;
/// <summary>
/// Units display utilities for Editor.
/// </summary>
public class Units
{
/// <summary>
/// Factor of units per meter.
/// </summary>
public static readonly float Meters2Units = 100f;
/// <summary>
/// False to always show game units without any postfix.
/// </summary>
public static bool UseUnitsFormatting = true;
/// <summary>
/// Add a space between numbers and units for readability.
/// </summary>
public static bool SeparateValueAndUnit = true;
/// <summary>
/// If set to true, the distance unit is chosen on the magnitude, otherwise it's meters.
/// </summary>
public static bool AutomaticUnitsFormatting = true;
/// <summary>
/// Return the unit according to user settings.
/// </summary>
/// <param name="unit">The unit name.</param>
/// <returns>The formatted text.</returns>
public static string Unit(string unit)
{
if (SeparateValueAndUnit)
return $" {unit}";
return unit;
}
}

View File

@@ -243,6 +243,63 @@ namespace FlaxEditor.Utilities
500000, 1000000, 5000000, 10000000, 100000000
};
internal delegate void DrawCurveTick(float tick, float strength);
internal static Int2 DrawCurveTicks(DrawCurveTick drawTick, float[] tickSteps, ref float[] tickStrengths, float min, float max, float pixelRange, float minDistanceBetweenTicks = 20, float maxDistanceBetweenTicks = 60)
{
if (tickStrengths == null || tickStrengths.Length != tickSteps.Length)
tickStrengths = new float[tickSteps.Length];
// Find the strength for each modulo number tick marker
var pixelsInRange = pixelRange / (max - min);
var smallestTick = 0;
var biggestTick = tickSteps.Length - 1;
for (int i = tickSteps.Length - 1; i >= 0; i--)
{
// Calculate how far apart these modulo tick steps are spaced
float tickSpacing = tickSteps[i] * pixelsInRange;
// Calculate the strength of the tick markers based on the spacing
tickStrengths[i] = Mathf.Saturate((tickSpacing - minDistanceBetweenTicks) / (maxDistanceBetweenTicks - minDistanceBetweenTicks));
// Beyond threshold the ticks don't get any bigger or fatter
if (tickStrengths[i] >= 1)
biggestTick = i;
// Do not show small tick markers
if (tickSpacing <= minDistanceBetweenTicks)
{
smallestTick = i;
break;
}
}
var tickLevels = biggestTick - smallestTick + 1;
// Draw all tick levels
for (int level = 0; level < tickLevels; level++)
{
float strength = tickStrengths[smallestTick + level];
if (strength <= Mathf.Epsilon)
continue;
// Draw all ticks
int l = Mathf.Clamp(smallestTick + level, 0, tickSteps.Length - 1);
var lStep = tickSteps[l];
var lNextStep = tickSteps[l + 1];
int startTick = Mathf.FloorToInt(min / lStep);
int endTick = Mathf.CeilToInt(max / lStep);
for (int i = startTick; i <= endTick; i++)
{
if (l < biggestTick && (i % Mathf.RoundToInt(lNextStep / lStep) == 0))
continue;
var tick = i * lStep;
drawTick(tick, strength);
}
}
return new Int2(smallestTick, biggestTick);
}
/// <summary>
/// Determines whether the specified path string contains any invalid character.
/// </summary>
@@ -1187,6 +1244,71 @@ namespace FlaxEditor.Utilities
return StringUtils.GetPathWithoutExtension(path);
}
private static string InternalFormat(double value, string format, FlaxEngine.Utils.ValueCategory category)
{
switch (category)
{
case FlaxEngine.Utils.ValueCategory.Distance:
if (!Units.AutomaticUnitsFormatting)
return (value / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("m");
var absValue = Mathf.Abs(value);
// in case a unit != cm this would be (value / Meters2Units * 100)
if (absValue < Units.Meters2Units)
return value.ToString(format, CultureInfo.InvariantCulture) + Units.Unit("cm");
if (absValue < Units.Meters2Units * 1000)
return (value / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("m");
return (value / 1000 / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("km");
case FlaxEngine.Utils.ValueCategory.Angle: return value.ToString(format, CultureInfo.InvariantCulture) + "°";
case FlaxEngine.Utils.ValueCategory.Time: return value.ToString(format, CultureInfo.InvariantCulture) + Units.Unit("s");
// some fonts have a symbol for that: "\u33A7"
case FlaxEngine.Utils.ValueCategory.Speed: return (value / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("m/s");
case FlaxEngine.Utils.ValueCategory.Acceleration: return (value / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("m/s²");
case FlaxEngine.Utils.ValueCategory.Area: return (value / Units.Meters2Units / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("m²");
case FlaxEngine.Utils.ValueCategory.Volume: return (value / Units.Meters2Units / Units.Meters2Units / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("m³");
case FlaxEngine.Utils.ValueCategory.Mass: return value.ToString(format, CultureInfo.InvariantCulture) + Units.Unit("kg");
case FlaxEngine.Utils.ValueCategory.Force: return (value / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("N");
case FlaxEngine.Utils.ValueCategory.Torque: return (value / Units.Meters2Units / Units.Meters2Units).ToString(format, CultureInfo.InvariantCulture) + Units.Unit("Nm");
case FlaxEngine.Utils.ValueCategory.None:
default: return FormatFloat(value);
}
}
/// <summary>
/// Format a float value either as-is, with a distance unit or with a degree sign.
/// </summary>
/// <param name="value">The value to format.</param>
/// <param name="category">The value type: none means just a number, distance will format in cm/m/km, angle with an appended degree sign.</param>
/// <returns>The formatted string.</returns>
public static string FormatFloat(float value, FlaxEngine.Utils.ValueCategory category)
{
if (float.IsPositiveInfinity(value) || value == float.MaxValue)
return "Infinity";
if (float.IsNegativeInfinity(value) || value == float.MinValue)
return "-Infinity";
if (!Units.UseUnitsFormatting || category == FlaxEngine.Utils.ValueCategory.None)
return FormatFloat(value);
const string format = "G7";
return InternalFormat(value, format, category);
}
/// <summary>
/// Format a double value either as-is, with a distance unit or with a degree sign
/// </summary>
/// <param name="value">The value to format.</param>
/// <param name="category">The value type: none means just a number, distance will format in cm/m/km, angle with an appended degree sign.</param>
/// <returns>The formatted string.</returns>
public static string FormatFloat(double value, FlaxEngine.Utils.ValueCategory category)
{
if (double.IsPositiveInfinity(value) || value == double.MaxValue)
return "Infinity";
if (double.IsNegativeInfinity(value) || value == double.MinValue)
return "-Infinity";
if (!Units.UseUnitsFormatting || category == FlaxEngine.Utils.ValueCategory.None)
return FormatFloat(value);
const string format = "G15";
return InternalFormat(value, format, category);
}
/// <summary>
/// Formats the floating point value (double precision) into the readable text representation.
/// </summary>
@@ -1198,7 +1320,7 @@ namespace FlaxEditor.Utilities
return "Infinity";
if (float.IsNegativeInfinity(value) || value == float.MinValue)
return "-Infinity";
string str = value.ToString("r", CultureInfo.InvariantCulture);
string str = value.ToString("R", CultureInfo.InvariantCulture);
return FormatFloat(str, value < 0);
}
@@ -1213,7 +1335,7 @@ namespace FlaxEditor.Utilities
return "Infinity";
if (double.IsNegativeInfinity(value) || value == double.MinValue)
return "-Infinity";
string str = value.ToString("r", CultureInfo.InvariantCulture);
string str = value.ToString("R", CultureInfo.InvariantCulture);
return FormatFloat(str, value < 0);
}

View File

@@ -187,6 +187,8 @@ void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Scene
void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Actor* actor, Mesh::DrawInfo& draw)
{
if (!actor || !actor->IsActiveInHierarchy())
return;
auto& view = renderContext.View;
const BoundingFrustum frustum = view.Frustum;
Matrix m1, m2, world;
@@ -208,8 +210,7 @@ void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Actor
draw.DrawState = &drawState;
draw.Deformation = nullptr;
// Support custom icons through types, but not onces that were added through actors,
// since they cant register while in prefab view anyway
// Support custom icons through types, but not ones that were added through actors, since they cant register while in prefab view anyway
if (ActorTypeToTexture.TryGet(actor->GetTypeHandle(), texture))
{
// Use custom texture

View File

@@ -6,9 +6,7 @@ using Real = System.Double;
using Real = System.Single;
#endif
using System.Collections.Generic;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEngine;
namespace FlaxEditor.Viewport.Cameras
@@ -85,86 +83,8 @@ namespace FlaxEditor.Viewport.Cameras
_moveStartTime = Time.UnscaledGameTime;
}
/// <summary>
/// Moves the viewport to visualize the actor.
/// </summary>
/// <param name="actor">The actor to preview.</param>
public void ShowActor(Actor actor)
{
Editor.GetActorEditorSphere(actor, out BoundingSphere sphere);
ShowSphere(ref sphere);
}
/// <summary>
/// Moves the viewport to visualize selected actors.
/// </summary>
/// <param name="actor">The actors to show.</param>
/// <param name="orientation">The used orientation.</param>
public void ShowActor(Actor actor, ref Quaternion orientation)
{
Editor.GetActorEditorSphere(actor, out BoundingSphere sphere);
ShowSphere(ref sphere, ref orientation);
}
/// <summary>
/// Moves the viewport to visualize selected actors.
/// </summary>
/// <param name="selection">The actors to show.</param>
public void ShowActors(List<SceneGraphNode> selection)
{
if (selection.Count == 0)
return;
BoundingSphere mergesSphere = BoundingSphere.Empty;
for (int i = 0; i < selection.Count; i++)
{
selection[i].GetEditorSphere(out var sphere);
BoundingSphere.Merge(ref mergesSphere, ref sphere, out mergesSphere);
}
if (mergesSphere == BoundingSphere.Empty)
return;
ShowSphere(ref mergesSphere);
}
/// <summary>
/// Moves the viewport to visualize selected actors.
/// </summary>
/// <param name="selection">The actors to show.</param>
/// <param name="orientation">The used orientation.</param>
public void ShowActors(List<SceneGraphNode> selection, ref Quaternion orientation)
{
if (selection.Count == 0)
return;
BoundingSphere mergesSphere = BoundingSphere.Empty;
for (int i = 0; i < selection.Count; i++)
{
selection[i].GetEditorSphere(out var sphere);
BoundingSphere.Merge(ref mergesSphere, ref sphere, out mergesSphere);
}
if (mergesSphere == BoundingSphere.Empty)
return;
ShowSphere(ref mergesSphere, ref orientation);
}
/// <summary>
/// Moves the camera to visualize given world area defined by the sphere.
/// </summary>
/// <param name="sphere">The sphere.</param>
public void ShowSphere(ref BoundingSphere sphere)
{
var q = new Quaternion(0.424461186f, -0.0940724313f, 0.0443938486f, 0.899451137f);
ShowSphere(ref sphere, ref q);
}
/// <summary>
/// Moves the camera to visualize given world area defined by the sphere.
/// </summary>
/// <param name="sphere">The sphere.</param>
/// <param name="orientation">The camera orientation.</param>
public void ShowSphere(ref BoundingSphere sphere, ref Quaternion orientation)
/// <inheritdoc />
public override void ShowSphere(ref BoundingSphere sphere, ref Quaternion orientation)
{
Vector3 position;
if (Viewport.UseOrthographicProjection)

View File

@@ -6,6 +6,9 @@ using Real = System.Double;
using Real = System.Single;
#endif
using System.Collections.Generic;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEngine;
namespace FlaxEditor.Viewport.Cameras
@@ -33,6 +36,90 @@ namespace FlaxEditor.Viewport.Cameras
/// </summary>
public virtual bool UseMovementSpeed => true;
/// <summary>
/// Focuses the viewport on the current selection of the gizmo.
/// </summary>
/// <param name="gizmos">The gizmo collection (from viewport).</param>
/// <param name="orientation">The target view orientation.</param>
public virtual void FocusSelection(GizmosCollection gizmos, ref Quaternion orientation)
{
var transformGizmo = gizmos.Get<TransformGizmo>();
if (transformGizmo == null || transformGizmo.SelectedParents.Count == 0)
return;
if (gizmos.Active != null)
{
var gizmoBounds = gizmos.Active.FocusBounds;
if (gizmoBounds != BoundingSphere.Empty)
{
ShowSphere(ref gizmoBounds, ref orientation);
return;
}
}
ShowActors(transformGizmo.SelectedParents, ref orientation);
}
/// <summary>
/// Moves the viewport to visualize the actor.
/// </summary>
/// <param name="actor">The actor to preview.</param>
public virtual void ShowActor(Actor actor)
{
Editor.GetActorEditorSphere(actor, out BoundingSphere sphere);
ShowSphere(ref sphere);
}
/// <summary>
/// Moves the viewport to visualize selected actors.
/// </summary>
/// <param name="selection">The actors to show.</param>
public void ShowActors(List<SceneGraphNode> selection)
{
var q = new Quaternion(0.424461186f, -0.0940724313f, 0.0443938486f, 0.899451137f);
ShowActors(selection, ref q);
}
/// <summary>
/// Moves the viewport to visualize selected actors.
/// </summary>
/// <param name="selection">The actors to show.</param>
/// <param name="orientation">The used orientation.</param>
public virtual void ShowActors(List<SceneGraphNode> selection, ref Quaternion orientation)
{
if (selection.Count == 0)
return;
BoundingSphere mergesSphere = BoundingSphere.Empty;
for (int i = 0; i < selection.Count; i++)
{
selection[i].GetEditorSphere(out var sphere);
BoundingSphere.Merge(ref mergesSphere, ref sphere, out mergesSphere);
}
if (mergesSphere == BoundingSphere.Empty)
return;
ShowSphere(ref mergesSphere, ref orientation);
}
/// <summary>
/// Moves the camera to visualize given world area defined by the sphere.
/// </summary>
/// <param name="sphere">The sphere.</param>
public void ShowSphere(ref BoundingSphere sphere)
{
var q = new Quaternion(0.424461186f, -0.0940724313f, 0.0443938486f, 0.899451137f);
ShowSphere(ref sphere, ref q);
}
/// <summary>
/// Moves the camera to visualize given world area defined by the sphere.
/// </summary>
/// <param name="sphere">The sphere.</param>
/// <param name="orientation">The camera orientation.</param>
public virtual void ShowSphere(ref BoundingSphere sphere, ref Quaternion orientation)
{
SetArcBallView(orientation, sphere.Center, sphere.Radius);
}
/// <summary>
/// Sets view orientation and position to match the arc ball camera style view for the given target object bounds.
/// </summary>

View File

@@ -1,9 +1,12 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Viewport.Widgets;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -41,6 +44,7 @@ namespace FlaxEditor.Viewport
Gizmos[i].Update(deltaTime);
}
}
/// <inheritdoc />
public EditorViewport Viewport => this;
@@ -66,19 +70,19 @@ namespace FlaxEditor.Viewport
public bool IsControlDown => _input.IsControlDown;
/// <inheritdoc />
public bool SnapToGround => Editor.Instance.Options.Options.Input.SnapToGround.Process(Root);
public bool SnapToGround => ContainsFocus && Editor.Instance.Options.Options.Input.SnapToGround.Process(Root);
/// <inheritdoc />
public bool SnapToVertex => Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root);
public bool SnapToVertex => ContainsFocus && Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root);
/// <inheritdoc />
public Float2 MouseDelta => _mouseDelta * 1000;
/// <inheritdoc />
public bool UseSnapping => Root.GetKey(KeyboardKeys.Control);
public bool UseSnapping => Root?.GetKey(KeyboardKeys.Control) ?? false;
/// <inheritdoc />
public bool UseDuplicate => Root.GetKey(KeyboardKeys.Shift);
public bool UseDuplicate => Root?.GetKey(KeyboardKeys.Shift) ?? false;
/// <inheritdoc />
public Undo Undo { get; }
@@ -92,6 +96,9 @@ namespace FlaxEditor.Viewport
/// <inheritdoc />
public abstract void Spawn(Actor actor);
/// <inheritdoc />
public abstract void OpenContextMenu();
/// <inheritdoc />
protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false;
@@ -121,5 +128,284 @@ namespace FlaxEditor.Viewport
base.OnDestroy();
}
internal static void AddGizmoViewportWidgets(EditorViewport viewport, TransformGizmo transformGizmo, bool useProjectCache = false)
{
var editor = Editor.Instance;
var inputOptions = editor.Options.Options.Input;
if (useProjectCache)
{
// Initialize snapping enabled from cached values
if (editor.ProjectCache.TryGetCustomData("TranslateSnapState", out var cachedState))
transformGizmo.TranslationSnapEnable = bool.Parse(cachedState);
if (editor.ProjectCache.TryGetCustomData("RotationSnapState", out cachedState))
transformGizmo.RotationSnapEnabled = bool.Parse(cachedState);
if (editor.ProjectCache.TryGetCustomData("ScaleSnapState", out cachedState))
transformGizmo.ScaleSnapEnabled = bool.Parse(cachedState);
if (editor.ProjectCache.TryGetCustomData("TranslateSnapValue", out cachedState))
transformGizmo.TranslationSnapValue = float.Parse(cachedState);
if (editor.ProjectCache.TryGetCustomData("RotationSnapValue", out cachedState))
transformGizmo.RotationSnapValue = float.Parse(cachedState);
if (editor.ProjectCache.TryGetCustomData("ScaleSnapValue", out cachedState))
transformGizmo.ScaleSnapValue = float.Parse(cachedState);
if (editor.ProjectCache.TryGetCustomData("TransformSpaceState", out cachedState) && Enum.TryParse(cachedState, out TransformGizmoBase.TransformSpace space))
transformGizmo.ActiveTransformSpace = space;
}
// Transform space widget
var transformSpaceWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var transformSpaceToggle = new ViewportWidgetButton(string.Empty, editor.Icons.Globe32, null, true)
{
Checked = transformGizmo.ActiveTransformSpace == TransformGizmoBase.TransformSpace.World,
TooltipText = $"Gizmo transform space (world or local) ({inputOptions.ToggleTransformSpace})",
Parent = transformSpaceWidget
};
transformSpaceToggle.Toggled += _ =>
{
transformGizmo.ToggleTransformSpace();
if (useProjectCache)
editor.ProjectCache.SetCustomData("TransformSpaceState", transformGizmo.ActiveTransformSpace.ToString());
};
transformSpaceWidget.Parent = viewport;
// Scale snapping widget
var scaleSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableScaleSnapping = new ViewportWidgetButton(string.Empty, editor.Icons.ScaleSnap32, null, true)
{
Checked = transformGizmo.ScaleSnapEnabled,
TooltipText = "Enable scale snapping",
Parent = scaleSnappingWidget
};
enableScaleSnapping.Toggled += _ =>
{
transformGizmo.ScaleSnapEnabled = !transformGizmo.ScaleSnapEnabled;
if (useProjectCache)
editor.ProjectCache.SetCustomData("ScaleSnapState", transformGizmo.ScaleSnapEnabled.ToString());
};
var scaleSnappingCM = new ContextMenu();
var scaleSnapping = new ViewportWidgetButton(transformGizmo.ScaleSnapValue.ToString(), SpriteHandle.Invalid, scaleSnappingCM)
{
TooltipText = "Scale snapping values"
};
for (int i = 0; i < ScaleSnapValues.Length; i++)
{
var v = ScaleSnapValues[i];
var button = scaleSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
scaleSnappingCM.ButtonClicked += button =>
{
var v = (float)button.Tag;
transformGizmo.ScaleSnapValue = v;
scaleSnapping.Text = v.ToString();
if (useProjectCache)
editor.ProjectCache.SetCustomData("ScaleSnapValue", transformGizmo.ScaleSnapValue.ToString("N"));
};
scaleSnappingCM.VisibleChanged += control =>
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(transformGizmo.ScaleSnapValue - v) < 0.001f ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
}
}
};
scaleSnapping.Parent = scaleSnappingWidget;
scaleSnappingWidget.Parent = viewport;
// Rotation snapping widget
var rotateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableRotateSnapping = new ViewportWidgetButton(string.Empty, editor.Icons.RotateSnap32, null, true)
{
Checked = transformGizmo.RotationSnapEnabled,
TooltipText = "Enable rotation snapping",
Parent = rotateSnappingWidget
};
enableRotateSnapping.Toggled += _ =>
{
transformGizmo.RotationSnapEnabled = !transformGizmo.RotationSnapEnabled;
if (useProjectCache)
editor.ProjectCache.SetCustomData("RotationSnapState", transformGizmo.RotationSnapEnabled.ToString());
};
var rotateSnappingCM = new ContextMenu();
var rotateSnapping = new ViewportWidgetButton(transformGizmo.RotationSnapValue.ToString(), SpriteHandle.Invalid, rotateSnappingCM)
{
TooltipText = "Rotation snapping values"
};
for (int i = 0; i < RotateSnapValues.Length; i++)
{
var v = RotateSnapValues[i];
var button = rotateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
rotateSnappingCM.ButtonClicked += button =>
{
var v = (float)button.Tag;
transformGizmo.RotationSnapValue = v;
rotateSnapping.Text = v.ToString();
if (useProjectCache)
editor.ProjectCache.SetCustomData("RotationSnapValue", transformGizmo.RotationSnapValue.ToString("N"));
};
rotateSnappingCM.VisibleChanged += control =>
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(transformGizmo.RotationSnapValue - v) < 0.001f ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
}
}
};
rotateSnapping.Parent = rotateSnappingWidget;
rotateSnappingWidget.Parent = viewport;
// Translation snapping widget
var translateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableTranslateSnapping = new ViewportWidgetButton(string.Empty, editor.Icons.Grid32, null, true)
{
Checked = transformGizmo.TranslationSnapEnable,
TooltipText = "Enable position snapping",
Parent = translateSnappingWidget
};
enableTranslateSnapping.Toggled += _ =>
{
transformGizmo.TranslationSnapEnable = !transformGizmo.TranslationSnapEnable;
if (useProjectCache)
editor.ProjectCache.SetCustomData("TranslateSnapState", transformGizmo.TranslationSnapEnable.ToString());
};
var translateSnappingCM = new ContextMenu();
var translateSnapping = new ViewportWidgetButton(transformGizmo.TranslationSnapValue.ToString(), SpriteHandle.Invalid, translateSnappingCM)
{
TooltipText = "Position snapping values"
};
if (transformGizmo.TranslationSnapValue < 0.0f)
translateSnapping.Text = "Bounding Box";
for (int i = 0; i < TranslateSnapValues.Length; i++)
{
var v = TranslateSnapValues[i];
var button = translateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
var buttonBB = translateSnappingCM.AddButton("Bounding Box").LinkTooltip("Snaps the selection based on it's bounding volume");
buttonBB.Tag = -1.0f;
translateSnappingCM.ButtonClicked += button =>
{
var v = (float)button.Tag;
transformGizmo.TranslationSnapValue = v;
if (v < 0.0f)
translateSnapping.Text = "Bounding Box";
else
translateSnapping.Text = v.ToString();
if (useProjectCache)
editor.ProjectCache.SetCustomData("TranslateSnapValue", transformGizmo.TranslationSnapValue.ToString("N"));
};
translateSnappingCM.VisibleChanged += control =>
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(transformGizmo.TranslationSnapValue - v) < 0.001f ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
}
}
};
translateSnapping.Parent = translateSnappingWidget;
translateSnappingWidget.Parent = viewport;
// Gizmo mode widget
var gizmoMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var gizmoModeTranslate = new ViewportWidgetButton(string.Empty, editor.Icons.Translate32, null, true)
{
Tag = TransformGizmoBase.Mode.Translate,
TooltipText = $"Translate gizmo mode ({inputOptions.TranslateMode})",
Checked = true,
Parent = gizmoMode
};
gizmoModeTranslate.Toggled += _ => transformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate;
var gizmoModeRotate = new ViewportWidgetButton(string.Empty, editor.Icons.Rotate32, null, true)
{
Tag = TransformGizmoBase.Mode.Rotate,
TooltipText = $"Rotate gizmo mode ({inputOptions.RotateMode})",
Parent = gizmoMode
};
gizmoModeRotate.Toggled += _ => transformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate;
var gizmoModeScale = new ViewportWidgetButton(string.Empty, editor.Icons.Scale32, null, true)
{
Tag = TransformGizmoBase.Mode.Scale,
TooltipText = $"Scale gizmo mode ({inputOptions.ScaleMode})",
Parent = gizmoMode
};
gizmoModeScale.Toggled += _ => transformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale;
gizmoMode.Parent = viewport;
transformGizmo.ModeChanged += () =>
{
var mode = transformGizmo.ActiveMode;
gizmoModeTranslate.Checked = mode == TransformGizmoBase.Mode.Translate;
gizmoModeRotate.Checked = mode == TransformGizmoBase.Mode.Rotate;
gizmoModeScale.Checked = mode == TransformGizmoBase.Mode.Scale;
};
// Setup input actions
viewport.InputActions.Add(options => options.TranslateMode, () => transformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate);
viewport.InputActions.Add(options => options.RotateMode, () => transformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
viewport.InputActions.Add(options => options.ScaleMode, () => transformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
viewport.InputActions.Add(options => options.ToggleTransformSpace, () =>
{
transformGizmo.ToggleTransformSpace();
if (useProjectCache)
editor.ProjectCache.SetCustomData("TransformSpaceState", transformGizmo.ActiveTransformSpace.ToString());
transformSpaceToggle.Checked = !transformSpaceToggle.Checked;
});
}
internal static readonly float[] TranslateSnapValues =
{
0.1f,
0.5f,
1.0f,
5.0f,
10.0f,
100.0f,
1000.0f,
};
internal static readonly float[] RotateSnapValues =
{
1.0f,
5.0f,
10.0f,
15.0f,
30.0f,
45.0f,
60.0f,
90.0f,
};
internal static readonly float[] ScaleSnapValues =
{
0.05f,
0.1f,
0.25f,
0.5f,
1.0f,
2.0f,
4.0f,
6.0f,
8.0f,
};
}
}

View File

@@ -10,7 +10,6 @@ using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Viewport.Widgets;
using FlaxEngine;
using FlaxEngine.GUI;
using Newtonsoft.Json;
using JsonSerializer = FlaxEngine.Json.JsonSerializer;
namespace FlaxEditor.Viewport
@@ -154,6 +153,7 @@ namespace FlaxEditor.Viewport
// Input
internal bool _disableInputUpdate;
private bool _isControllingMouse, _isViewportControllingMouse, _wasVirtualMouseRightDown, _isVirtualMouseRightDown;
private int _deltaFilteringStep;
private Float2 _startPos;
@@ -704,9 +704,9 @@ namespace FlaxEditor.Viewport
// Camera Viewpoints
{
var cameraView = cameraCM.AddChildMenu("Viewpoints").ContextMenu;
for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++)
for (int i = 0; i < CameraViewpointValues.Length; i++)
{
var co = EditorViewportCameraViewpointValues[i];
var co = CameraViewpointValues[i];
var button = cameraView.AddButton(co.Name);
button.Tag = co.Orientation;
}
@@ -899,9 +899,9 @@ namespace FlaxEditor.Viewport
viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32;
viewFlags.AddButton("Disable flags", () => Task.ViewFlags = ViewFlags.None).Icon = Editor.Instance.Icons.Rotate32;
viewFlags.AddSeparator();
for (int i = 0; i < EditorViewportViewFlagsValues.Length; i++)
for (int i = 0; i < ViewFlagsValues.Length; i++)
{
var v = EditorViewportViewFlagsValues[i];
var v = ViewFlagsValues[i];
var button = viewFlags.AddButton(v.Name);
button.CloseMenuOnClick = false;
button.Tag = v.Mode;
@@ -933,9 +933,9 @@ namespace FlaxEditor.Viewport
}
});
debugView.AddSeparator();
for (int i = 0; i < EditorViewportViewModeValues.Length; i++)
for (int i = 0; i < ViewModeValues.Length; i++)
{
ref var v = ref EditorViewportViewModeValues[i];
ref var v = ref ViewModeValues[i];
if (v.Options != null)
{
var childMenu = debugView.AddChildMenu(v.Name).ContextMenu;
@@ -989,12 +989,12 @@ namespace FlaxEditor.Viewport
#endregion View mode widget
}
InputActions.Add(options => options.ViewpointTop, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Top").Orientation)));
InputActions.Add(options => options.ViewpointBottom, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Bottom").Orientation)));
InputActions.Add(options => options.ViewpointFront, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Front").Orientation)));
InputActions.Add(options => options.ViewpointBack, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Back").Orientation)));
InputActions.Add(options => options.ViewpointRight, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Right").Orientation)));
InputActions.Add(options => options.ViewpointLeft, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Left").Orientation)));
InputActions.Add(options => options.ViewpointTop, () => OrientViewport(Quaternion.Euler(CameraViewpointValues.First(vp => vp.Name == "Top").Orientation)));
InputActions.Add(options => options.ViewpointBottom, () => OrientViewport(Quaternion.Euler(CameraViewpointValues.First(vp => vp.Name == "Bottom").Orientation)));
InputActions.Add(options => options.ViewpointFront, () => OrientViewport(Quaternion.Euler(CameraViewpointValues.First(vp => vp.Name == "Front").Orientation)));
InputActions.Add(options => options.ViewpointBack, () => OrientViewport(Quaternion.Euler(CameraViewpointValues.First(vp => vp.Name == "Back").Orientation)));
InputActions.Add(options => options.ViewpointRight, () => OrientViewport(Quaternion.Euler(CameraViewpointValues.First(vp => vp.Name == "Right").Orientation)));
InputActions.Add(options => options.ViewpointLeft, () => OrientViewport(Quaternion.Euler(CameraViewpointValues.First(vp => vp.Name == "Left").Orientation)));
InputActions.Add(options => options.CameraToggleRotation, () => _isVirtualMouseRightDown = !_isVirtualMouseRightDown);
InputActions.Add(options => options.CameraIncreaseMoveSpeed, () => AdjustCameraMoveSpeed(1));
InputActions.Add(options => options.CameraDecreaseMoveSpeed, () => AdjustCameraMoveSpeed(-1));
@@ -1497,6 +1497,9 @@ namespace FlaxEditor.Viewport
{
base.Update(deltaTime);
if (_disableInputUpdate)
return;
// Update camera
bool useMovementSpeed = false;
if (_camera != null)
@@ -1535,7 +1538,7 @@ namespace FlaxEditor.Viewport
}
bool useMouse = IsControllingMouse || (Mathf.IsInRange(_viewMousePos.X, 0, Width) && Mathf.IsInRange(_viewMousePos.Y, 0, Height));
_prevInput = _input;
var hit = GetChildAt(_viewMousePos, c => c.Visible && !(c is CanvasRootControl));
var hit = GetChildAt(_viewMousePos, c => c.Visible && !(c is CanvasRootControl) && !(c is UIEditorRoot));
if (canUseInput && ContainsFocus && hit == null)
_input.Gather(win.Window, useMouse);
else
@@ -1868,7 +1871,7 @@ namespace FlaxEditor.Viewport
}
}
private readonly CameraViewpoint[] EditorViewportCameraViewpointValues =
private readonly CameraViewpoint[] CameraViewpointValues =
{
new CameraViewpoint("Front", new Float3(0, 180, 0)),
new CameraViewpoint("Back", new Float3(0, 0, 0)),
@@ -1899,7 +1902,7 @@ namespace FlaxEditor.Viewport
}
}
private static readonly ViewModeOptions[] EditorViewportViewModeValues =
private static readonly ViewModeOptions[] ViewModeValues =
{
new ViewModeOptions(ViewMode.Default, "Default"),
new ViewModeOptions(ViewMode.Unlit, "Unlit"),
@@ -1971,7 +1974,7 @@ namespace FlaxEditor.Viewport
}
}
private static readonly ViewFlagOptions[] EditorViewportViewFlagsValues =
private static readonly ViewFlagOptions[] ViewFlagsValues =
{
new ViewFlagOptions(ViewFlags.AntiAliasing, "Anti Aliasing"),
new ViewFlagOptions(ViewFlags.Shadows, "Shadows"),
@@ -2006,16 +2009,13 @@ namespace FlaxEditor.Viewport
{
if (cm.Visible == false)
return;
var ccm = (ContextMenu)cm;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b && b.Tag != null)
{
var v = (ViewFlags)b.Tag;
b.Icon = (Task.View.Flags & v) != 0
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
b.Icon = (Task.View.Flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
}
}
}
@@ -2024,7 +2024,6 @@ namespace FlaxEditor.Viewport
{
if (cm.Visible == false)
return;
var ccm = (ContextMenu)cm;
var layersMask = Task.ViewLayersMask;
foreach (var e in ccm.Items)

View File

@@ -2,15 +2,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
using FlaxEditor.Scripting;
using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Viewport.Modes;
using FlaxEditor.Viewport.Widgets;
using FlaxEditor.Windows;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -28,13 +25,6 @@ namespace FlaxEditor.Viewport
private readonly ContextMenuButton _showGridButton;
private readonly ContextMenuButton _showNavigationButton;
private readonly ViewportWidgetButton _gizmoModeTranslate;
private readonly ViewportWidgetButton _gizmoModeRotate;
private readonly ViewportWidgetButton _gizmoModeScale;
private readonly ViewportWidgetButton _translateSnapping;
private readonly ViewportWidgetButton _rotateSnapping;
private readonly ViewportWidgetButton _scaleSnapping;
private SelectionOutline _customSelectionOutline;
@@ -196,7 +186,6 @@ namespace FlaxEditor.Viewport
{
_editor = editor;
DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem);
var inputOptions = editor.Options.Options.Input;
// Prepare rendering task
Task.ActorsSource = ActorsSources.Scenes;
@@ -222,8 +211,7 @@ namespace FlaxEditor.Viewport
// Add transformation gizmo
TransformGizmo = new TransformGizmo(this);
TransformGizmo.ApplyTransformation += ApplyTransform;
TransformGizmo.ModeChanged += OnGizmoModeChanged;
TransformGizmo.Duplicate += Editor.Instance.SceneEditing.Duplicate;
TransformGizmo.Duplicate += _editor.SceneEditing.Duplicate;
Gizmos.Active = TransformGizmo;
// Add grid
@@ -232,144 +220,8 @@ namespace FlaxEditor.Viewport
editor.SceneEditing.SelectionChanged += OnSelectionChanged;
// Initialize snapping enabled from cached values
if (_editor.ProjectCache.TryGetCustomData("TranslateSnapState", out var cachedState))
TransformGizmo.TranslationSnapEnable = bool.Parse(cachedState);
if (_editor.ProjectCache.TryGetCustomData("RotationSnapState", out cachedState))
TransformGizmo.RotationSnapEnabled = bool.Parse(cachedState);
if (_editor.ProjectCache.TryGetCustomData("ScaleSnapState", out cachedState))
TransformGizmo.ScaleSnapEnabled = bool.Parse(cachedState);
if (_editor.ProjectCache.TryGetCustomData("TranslateSnapValue", out cachedState))
TransformGizmo.TranslationSnapValue = float.Parse(cachedState);
if (_editor.ProjectCache.TryGetCustomData("RotationSnapValue", out cachedState))
TransformGizmo.RotationSnapValue = float.Parse(cachedState);
if (_editor.ProjectCache.TryGetCustomData("ScaleSnapValue", out cachedState))
TransformGizmo.ScaleSnapValue = float.Parse(cachedState);
if (_editor.ProjectCache.TryGetCustomData("TransformSpaceState", out cachedState) && Enum.TryParse(cachedState, out TransformGizmoBase.TransformSpace space))
TransformGizmo.ActiveTransformSpace = space;
// Transform space widget
var transformSpaceWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var transformSpaceToggle = new ViewportWidgetButton(string.Empty, editor.Icons.Globe32, null, true)
{
Checked = TransformGizmo.ActiveTransformSpace == TransformGizmoBase.TransformSpace.World,
TooltipText = $"Gizmo transform space (world or local) ({inputOptions.ToggleTransformSpace})",
Parent = transformSpaceWidget
};
transformSpaceToggle.Toggled += OnTransformSpaceToggle;
transformSpaceWidget.Parent = this;
// Scale snapping widget
var scaleSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableScaleSnapping = new ViewportWidgetButton(string.Empty, editor.Icons.ScaleSnap32, null, true)
{
Checked = TransformGizmo.ScaleSnapEnabled,
TooltipText = "Enable scale snapping",
Parent = scaleSnappingWidget
};
enableScaleSnapping.Toggled += OnScaleSnappingToggle;
var scaleSnappingCM = new ContextMenu();
_scaleSnapping = new ViewportWidgetButton(TransformGizmo.ScaleSnapValue.ToString(), SpriteHandle.Invalid, scaleSnappingCM)
{
TooltipText = "Scale snapping values"
};
for (int i = 0; i < EditorViewportScaleSnapValues.Length; i++)
{
var v = EditorViewportScaleSnapValues[i];
var button = scaleSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
scaleSnappingCM.ButtonClicked += OnWidgetScaleSnapClick;
scaleSnappingCM.VisibleChanged += OnWidgetScaleSnapShowHide;
_scaleSnapping.Parent = scaleSnappingWidget;
scaleSnappingWidget.Parent = this;
// Rotation snapping widget
var rotateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableRotateSnapping = new ViewportWidgetButton(string.Empty, editor.Icons.RotateSnap32, null, true)
{
Checked = TransformGizmo.RotationSnapEnabled,
TooltipText = "Enable rotation snapping",
Parent = rotateSnappingWidget
};
enableRotateSnapping.Toggled += OnRotateSnappingToggle;
var rotateSnappingCM = new ContextMenu();
_rotateSnapping = new ViewportWidgetButton(TransformGizmo.RotationSnapValue.ToString(), SpriteHandle.Invalid, rotateSnappingCM)
{
TooltipText = "Rotation snapping values"
};
for (int i = 0; i < EditorViewportRotateSnapValues.Length; i++)
{
var v = EditorViewportRotateSnapValues[i];
var button = rotateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
rotateSnappingCM.ButtonClicked += OnWidgetRotateSnapClick;
rotateSnappingCM.VisibleChanged += OnWidgetRotateSnapShowHide;
_rotateSnapping.Parent = rotateSnappingWidget;
rotateSnappingWidget.Parent = this;
// Translation snapping widget
var translateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableTranslateSnapping = new ViewportWidgetButton(string.Empty, editor.Icons.Grid32, null, true)
{
Checked = TransformGizmo.TranslationSnapEnable,
TooltipText = "Enable position snapping",
Parent = translateSnappingWidget
};
enableTranslateSnapping.Toggled += OnTranslateSnappingToggle;
var translateSnappingCM = new ContextMenu();
_translateSnapping = new ViewportWidgetButton(TransformGizmo.TranslationSnapValue.ToString(), SpriteHandle.Invalid, translateSnappingCM)
{
TooltipText = "Position snapping values"
};
if (TransformGizmo.TranslationSnapValue < 0.0f)
_translateSnapping.Text = "Bounding Box";
for (int i = 0; i < EditorViewportTranslateSnapValues.Length; i++)
{
var v = EditorViewportTranslateSnapValues[i];
var button = translateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
var buttonBB = translateSnappingCM.AddButton("Bounding Box").LinkTooltip("Snaps the selection based on it's bounding volume");
buttonBB.Tag = -1.0f;
translateSnappingCM.ButtonClicked += OnWidgetTranslateSnapClick;
translateSnappingCM.VisibleChanged += OnWidgetTranslateSnapShowHide;
_translateSnapping.Parent = translateSnappingWidget;
translateSnappingWidget.Parent = this;
// Gizmo mode widget
var gizmoMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
_gizmoModeTranslate = new ViewportWidgetButton(string.Empty, editor.Icons.Translate32, null, true)
{
Tag = TransformGizmoBase.Mode.Translate,
TooltipText = $"Translate gizmo mode ({inputOptions.TranslateMode})",
Checked = true,
Parent = gizmoMode
};
_gizmoModeTranslate.Toggled += OnGizmoModeToggle;
_gizmoModeRotate = new ViewportWidgetButton(string.Empty, editor.Icons.Rotate32, null, true)
{
Tag = TransformGizmoBase.Mode.Rotate,
TooltipText = $"Rotate gizmo mode ({inputOptions.RotateMode})",
Parent = gizmoMode
};
_gizmoModeRotate.Toggled += OnGizmoModeToggle;
_gizmoModeScale = new ViewportWidgetButton(string.Empty, editor.Icons.Scale32, null, true)
{
Tag = TransformGizmoBase.Mode.Scale,
TooltipText = $"Scale gizmo mode ({inputOptions.ScaleMode})",
Parent = gizmoMode
};
_gizmoModeScale.Toggled += OnGizmoModeToggle;
gizmoMode.Parent = this;
// Gizmo widgets
AddGizmoViewportWidgets(this, TransformGizmo);
// Show grid widget
_showGridButton = ViewWidgetShowMenu.AddButton("Grid", () => Grid.Enabled = !Grid.Enabled);
@@ -400,14 +252,6 @@ namespace FlaxEditor.Viewport
}
// Setup input actions
InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate);
InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
InputActions.Add(options => options.ToggleTransformSpace, () =>
{
OnTransformSpaceToggle(transformSpaceToggle);
transformSpaceToggle.Checked = !transformSpaceToggle.Checked;
});
InputActions.Add(options => options.LockFocusSelection, LockFocusSelection);
InputActions.Add(options => options.FocusSelection, FocusSelection);
InputActions.Add(options => options.RotateSelection, RotateSelection);
@@ -486,7 +330,7 @@ namespace FlaxEditor.Viewport
};
// Spawn
Editor.Instance.SceneEditing.Spawn(actor, parent);
_editor.SceneEditing.Spawn(actor, parent);
}
private void OnBegin(RenderTask task, GPUContext context)
@@ -552,7 +396,7 @@ namespace FlaxEditor.Viewport
var task = renderContext.Task;
// Render editor primitives, gizmo and debug shapes in debug view modes
// Note: can use Output buffer as both input and output because EditorPrimitives is using a intermediate buffers
// Note: can use Output buffer as both input and output because EditorPrimitives is using an intermediate buffer
if (EditorPrimitives && EditorPrimitives.CanRender())
{
EditorPrimitives.Render(context, ref renderContext, task.Output, task.Output);
@@ -581,161 +425,6 @@ namespace FlaxEditor.Viewport
}
}
private void OnGizmoModeToggle(ViewportWidgetButton button)
{
TransformGizmo.ActiveMode = (TransformGizmoBase.Mode)(int)button.Tag;
}
private void OnTranslateSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.TranslationSnapEnable = !TransformGizmo.TranslationSnapEnable;
_editor.ProjectCache.SetCustomData("TranslateSnapState", TransformGizmo.TranslationSnapEnable.ToString());
}
private void OnRotateSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.RotationSnapEnabled = !TransformGizmo.RotationSnapEnabled;
_editor.ProjectCache.SetCustomData("RotationSnapState", TransformGizmo.RotationSnapEnabled.ToString());
}
private void OnScaleSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.ScaleSnapEnabled = !TransformGizmo.ScaleSnapEnabled;
_editor.ProjectCache.SetCustomData("ScaleSnapState", TransformGizmo.ScaleSnapEnabled.ToString());
}
private void OnTransformSpaceToggle(ViewportWidgetButton button)
{
TransformGizmo.ToggleTransformSpace();
_editor.ProjectCache.SetCustomData("TransformSpaceState", TransformGizmo.ActiveTransformSpace.ToString());
}
private void OnGizmoModeChanged()
{
// Update all viewport widgets status
var mode = TransformGizmo.ActiveMode;
_gizmoModeTranslate.Checked = mode == TransformGizmoBase.Mode.Translate;
_gizmoModeRotate.Checked = mode == TransformGizmoBase.Mode.Rotate;
_gizmoModeScale.Checked = mode == TransformGizmoBase.Mode.Scale;
}
private static readonly float[] EditorViewportScaleSnapValues =
{
0.05f,
0.1f,
0.25f,
0.5f,
1.0f,
2.0f,
4.0f,
6.0f,
8.0f,
};
private void OnWidgetScaleSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.ScaleSnapValue = v;
_scaleSnapping.Text = v.ToString();
_editor.ProjectCache.SetCustomData("ScaleSnapValue", TransformGizmo.ScaleSnapValue.ToString("N"));
}
private void OnWidgetScaleSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.ScaleSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private static readonly float[] EditorViewportRotateSnapValues =
{
1.0f,
5.0f,
10.0f,
15.0f,
30.0f,
45.0f,
60.0f,
90.0f,
};
private void OnWidgetRotateSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.RotationSnapValue = v;
_rotateSnapping.Text = v.ToString();
_editor.ProjectCache.SetCustomData("RotationSnapValue", TransformGizmo.RotationSnapValue.ToString("N"));
}
private void OnWidgetRotateSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.RotationSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private static readonly float[] EditorViewportTranslateSnapValues =
{
0.1f,
0.5f,
1.0f,
5.0f,
10.0f,
100.0f,
1000.0f,
};
private void OnWidgetTranslateSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.TranslationSnapValue = v;
if (v < 0.0f)
_translateSnapping.Text = "Bounding Box";
else
_translateSnapping.Text = v.ToString();
_editor.ProjectCache.SetCustomData("TranslateSnapValue", TransformGizmo.TranslationSnapValue.ToString("N"));
}
private void OnWidgetTranslateSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.TranslationSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private void OnSelectionChanged()
{
var selection = _editor.SceneEditing.Selection;
@@ -761,7 +450,7 @@ namespace FlaxEditor.Viewport
Vector3 gizmoPosition = TransformGizmo.Position;
// Rotate selected objects
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
bool isPlayMode = _editor.StateMachine.IsPlayMode;
TransformGizmo.StartTransforming();
for (int i = 0; i < selection.Count; i++)
{
@@ -819,14 +508,7 @@ namespace FlaxEditor.Viewport
/// <param name="orientation">The target view orientation.</param>
public void FocusSelection(ref Quaternion orientation)
{
if (TransformGizmo.SelectedParents.Count == 0)
return;
var gizmoBounds = Gizmos.Active.FocusBounds;
if (gizmoBounds != BoundingSphere.Empty)
((FPSCamera)ViewportCamera).ShowSphere(ref gizmoBounds, ref orientation);
else
((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orientation);
ViewportCamera.FocusSelection(Gizmos, ref orientation);
}
/// <summary>
@@ -843,7 +525,7 @@ namespace FlaxEditor.Viewport
Vector3 gizmoPosition = TransformGizmo.Position;
// Transform selected objects
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
bool isPlayMode = _editor.StateMachine.IsPlayMode;
for (int i = 0; i < selection.Count; i++)
{
var obj = selection[i];
@@ -985,7 +667,14 @@ namespace FlaxEditor.Viewport
{
var parent = actor.Parent ?? Level.GetScene(0);
actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parent.GetChild(x) == null);
Editor.Instance.SceneEditing.Spawn(actor);
_editor.SceneEditing.Spawn(actor);
}
/// <inheritdoc />
public override void OpenContextMenu()
{
var mouse = PointFromWindow(Root.MousePosition);
_editor.Windows.SceneWin.ShowContextMenu(this, mouse);
}
/// <inheritdoc />

View File

@@ -5,12 +5,10 @@ using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
using FlaxEditor.Scripting;
using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Viewport.Previews;
using FlaxEditor.Viewport.Widgets;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -30,7 +28,6 @@ namespace FlaxEditor.Viewport
{
public PrefabWindowViewport Viewport;
/// <inheritdoc />
public override bool CanRender()
{
return (Task.View.Flags & ViewFlags.EditorSprites) == ViewFlags.EditorSprites && Enabled;
@@ -42,19 +39,34 @@ namespace FlaxEditor.Viewport
}
}
[HideInEditor]
private sealed class PrefabUIEditorRoot : UIEditorRoot
{
private readonly PrefabWindowViewport _viewport;
private bool UI => _viewport._hasUILinkedCached;
public PrefabUIEditorRoot(PrefabWindowViewport viewport)
: base(true)
{
_viewport = viewport;
Parent = viewport;
}
public override bool EnableInputs => !UI;
public override bool EnableSelecting => UI;
public override bool EnableBackground => UI;
public override TransformGizmo TransformGizmo => _viewport.TransformGizmo;
}
private readonly PrefabWindow _window;
private UpdateDelegate _update;
private readonly ViewportWidgetButton _gizmoModeTranslate;
private readonly ViewportWidgetButton _gizmoModeRotate;
private readonly ViewportWidgetButton _gizmoModeScale;
private ViewportWidgetButton _translateSnappng;
private ViewportWidgetButton _rotateSnapping;
private ViewportWidgetButton _scaleSnapping;
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private PrefabSpritesRenderer _spritesRenderer;
private IntPtr _tempDebugDrawContext;
private bool _hasUILinkedCached;
private PrefabUIEditorRoot _uiRoot;
/// <summary>
/// Drag and drop handlers
@@ -107,144 +119,74 @@ namespace FlaxEditor.Viewport
// Add transformation gizmo
TransformGizmo = new TransformGizmo(this);
TransformGizmo.ApplyTransformation += ApplyTransform;
TransformGizmo.ModeChanged += OnGizmoModeChanged;
TransformGizmo.Duplicate += _window.Duplicate;
Gizmos.Active = TransformGizmo;
// Transform space widget
var transformSpaceWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var transformSpaceToggle = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Globe32, null, true)
{
Checked = TransformGizmo.ActiveTransformSpace == TransformGizmoBase.TransformSpace.World,
TooltipText = $"Gizmo transform space (world or local) ({inputOptions.ToggleTransformSpace})",
Parent = transformSpaceWidget
};
transformSpaceToggle.Toggled += OnTransformSpaceToggle;
transformSpaceWidget.Parent = this;
// Use custom root for UI controls
_uiRoot = new PrefabUIEditorRoot(this);
_uiRoot.IndexInParent = 0; // Move viewport down below other widgets in the viewport
_uiParentLink = _uiRoot.UIRoot;
// Scale snapping widget
var scaleSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableScaleSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.ScaleSnap32, null, true)
{
Checked = TransformGizmo.ScaleSnapEnabled,
TooltipText = "Enable scale snapping",
Parent = scaleSnappingWidget
};
enableScaleSnapping.Toggled += OnScaleSnappingToggle;
var scaleSnappingCM = new ContextMenu();
_scaleSnapping = new ViewportWidgetButton(TransformGizmo.ScaleSnapValue.ToString(), SpriteHandle.Invalid, scaleSnappingCM)
{
TooltipText = "Scale snapping values"
};
for (int i = 0; i < EditorViewportScaleSnapValues.Length; i++)
{
var v = EditorViewportScaleSnapValues[i];
var button = scaleSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
scaleSnappingCM.ButtonClicked += OnWidgetScaleSnapClick;
scaleSnappingCM.VisibleChanged += OnWidgetScaleSnapShowHide;
_scaleSnapping.Parent = scaleSnappingWidget;
scaleSnappingWidget.Parent = this;
// Rotation snapping widget
var rotateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableRotateSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.RotateSnap32, null, true)
{
Checked = TransformGizmo.RotationSnapEnabled,
TooltipText = "Enable rotation snapping",
Parent = rotateSnappingWidget
};
enableRotateSnapping.Toggled += OnRotateSnappingToggle;
var rotateSnappingCM = new ContextMenu();
_rotateSnapping = new ViewportWidgetButton(TransformGizmo.RotationSnapValue.ToString(), SpriteHandle.Invalid, rotateSnappingCM)
{
TooltipText = "Rotation snapping values"
};
for (int i = 0; i < EditorViewportRotateSnapValues.Length; i++)
{
var v = EditorViewportRotateSnapValues[i];
var button = rotateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
rotateSnappingCM.ButtonClicked += OnWidgetRotateSnapClick;
rotateSnappingCM.VisibleChanged += OnWidgetRotateSnapShowHide;
_rotateSnapping.Parent = rotateSnappingWidget;
rotateSnappingWidget.Parent = this;
// Translation snapping widget
var translateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
var enableTranslateSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Grid32, null, true)
{
Checked = TransformGizmo.TranslationSnapEnable,
TooltipText = "Enable position snapping",
Parent = translateSnappingWidget
};
enableTranslateSnapping.Toggled += OnTranslateSnappingToggle;
var translateSnappingCM = new ContextMenu();
_translateSnappng = new ViewportWidgetButton(TransformGizmo.TranslationSnapValue.ToString(), SpriteHandle.Invalid, translateSnappingCM)
{
TooltipText = "Position snapping values"
};
for (int i = 0; i < EditorViewportTranslateSnapValues.Length; i++)
{
var v = EditorViewportTranslateSnapValues[i];
var button = translateSnappingCM.AddButton(v.ToString());
button.Tag = v;
}
translateSnappingCM.ButtonClicked += OnWidgetTranslateSnapClick;
translateSnappingCM.VisibleChanged += OnWidgetTranslateSnapShowHide;
_translateSnappng.Parent = translateSnappingWidget;
translateSnappingWidget.Parent = this;
// Gizmo mode widget
var gizmoMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
_gizmoModeTranslate = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Translate32, null, true)
{
Tag = TransformGizmoBase.Mode.Translate,
TooltipText = $"Translate gizmo mode ({inputOptions.TranslateMode})",
Checked = true,
Parent = gizmoMode
};
_gizmoModeTranslate.Toggled += OnGizmoModeToggle;
_gizmoModeRotate = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Rotate32, null, true)
{
Tag = TransformGizmoBase.Mode.Rotate,
TooltipText = $"Rotate gizmo mode ({inputOptions.RotateMode})",
Parent = gizmoMode
};
_gizmoModeRotate.Toggled += OnGizmoModeToggle;
_gizmoModeScale = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Scale32, null, true)
{
Tag = TransformGizmoBase.Mode.Scale,
TooltipText = $"Scale gizmo mode ({inputOptions.ScaleMode})",
Parent = gizmoMode
};
_gizmoModeScale.Toggled += OnGizmoModeToggle;
gizmoMode.Parent = this;
EditorGizmoViewport.AddGizmoViewportWidgets(this, TransformGizmo);
// Setup input actions
InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate);
InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
InputActions.Add(options => options.ToggleTransformSpace, () =>
{
OnTransformSpaceToggle(transformSpaceToggle);
transformSpaceToggle.Checked = !transformSpaceToggle.Checked;
});
InputActions.Add(options => options.FocusSelection, ShowSelectedActors);
SetUpdate(ref _update, OnUpdate);
}
/// <summary>
/// Updates the viewport's gizmos, especially to toggle between 3D and UI editing modes.
/// </summary>
internal void UpdateGizmoMode()
{
// Skip if gizmo mode was unmodified
if (_hasUILinked == _hasUILinkedCached)
return;
_hasUILinkedCached = _hasUILinked;
if (_hasUILinked)
{
// UI widget
Gizmos.Active = null;
ViewportCamera = new UIEditorCamera { UIEditor = _uiRoot };
// Hide 3D visuals
ShowEditorPrimitives = false;
ShowDefaultSceneActors = false;
ShowDebugDraw = false;
// Show whole UI on startup
var canvas = (CanvasRootControl)_uiParentLink.Children.FirstOrDefault(x => x is CanvasRootControl);
if (canvas != null)
ViewportCamera.ShowActor(canvas.Canvas);
else if (Instance is UIControl)
ViewportCamera.ShowActor(Instance);
}
else
{
// Generic prefab
Gizmos.Active = TransformGizmo;
ViewportCamera = new FPSCamera();
}
// Update default components usage
bool defaultFeatures = !_hasUILinked;
_disableInputUpdate = _hasUILinked;
_spritesRenderer.Enabled = defaultFeatures;
SelectionOutline.Enabled = defaultFeatures;
_showDefaultSceneButton.Visible = defaultFeatures;
_cameraWidget.Visible = defaultFeatures;
_cameraButton.Visible = defaultFeatures;
_orthographicModeButton.Visible = defaultFeatures;
Task.Enabled = defaultFeatures;
UseAutomaticTaskManagement = defaultFeatures;
TintColor = defaultFeatures ? Color.White : Color.Transparent;
}
private void OnUpdate(float deltaTime)
{
UpdateGizmoMode();
for (int i = 0; i < Gizmos.Count; i++)
{
Gizmos[i].Update(deltaTime);
@@ -259,11 +201,19 @@ namespace FlaxEditor.Viewport
var selectedParents = TransformGizmo.SelectedParents;
if (selectedParents.Count > 0)
{
// Use temporary Debug Draw context to pull any debug shapes drawing in Scene Graph Nodes - those are used in OnDebugDraw down below
if (_tempDebugDrawContext == IntPtr.Zero)
_tempDebugDrawContext = DebugDraw.AllocateContext();
DebugDraw.SetContext(_tempDebugDrawContext);
DebugDraw.UpdateContext(_tempDebugDrawContext, 1.0f);
for (int i = 0; i < selectedParents.Count; i++)
{
if (selectedParents[i].IsActiveInHierarchy)
selectedParents[i].OnDebugDraw(_debugDrawData);
}
DebugDraw.SetContext(IntPtr.Zero);
}
}
@@ -307,7 +257,7 @@ namespace FlaxEditor.Viewport
public void ShowSelectedActors()
{
var orient = ViewOrientation;
((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orient);
ViewportCamera.ShowActors(TransformGizmo.SelectedParents, ref orient);
}
/// <inheritdoc />
@@ -338,7 +288,7 @@ namespace FlaxEditor.Viewport
public bool SnapToGround => false;
/// <inheritdoc />
public bool SnapToVertex => Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root);
public bool SnapToVertex => ContainsFocus && Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root);
/// <inheritdoc />
public Float2 MouseDelta => _mouseDelta * 1000;
@@ -367,6 +317,13 @@ namespace FlaxEditor.Viewport
_window.Spawn(actor);
}
/// <inheritdoc />
public void OpenContextMenu()
{
var mouse = PointFromWindow(Root.MousePosition);
_window.ShowContextMenu(this, ref mouse);
}
/// <inheritdoc />
protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false;
@@ -386,151 +343,6 @@ namespace FlaxEditor.Viewport
root.UpdateCallbacksToRemove.Add(_update);
}
private void OnGizmoModeToggle(ViewportWidgetButton button)
{
TransformGizmo.ActiveMode = (TransformGizmoBase.Mode)(int)button.Tag;
}
private void OnTranslateSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.TranslationSnapEnable = !TransformGizmo.TranslationSnapEnable;
}
private void OnRotateSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.RotationSnapEnabled = !TransformGizmo.RotationSnapEnabled;
}
private void OnScaleSnappingToggle(ViewportWidgetButton button)
{
TransformGizmo.ScaleSnapEnabled = !TransformGizmo.ScaleSnapEnabled;
}
private void OnTransformSpaceToggle(ViewportWidgetButton button)
{
TransformGizmo.ToggleTransformSpace();
}
private void OnGizmoModeChanged()
{
// Update all viewport widgets status
var mode = TransformGizmo.ActiveMode;
_gizmoModeTranslate.Checked = mode == TransformGizmoBase.Mode.Translate;
_gizmoModeRotate.Checked = mode == TransformGizmoBase.Mode.Rotate;
_gizmoModeScale.Checked = mode == TransformGizmoBase.Mode.Scale;
}
private static readonly float[] EditorViewportScaleSnapValues =
{
0.05f,
0.1f,
0.25f,
0.5f,
1.0f,
2.0f,
4.0f,
6.0f,
8.0f,
};
private void OnWidgetScaleSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.ScaleSnapValue = v;
_scaleSnapping.Text = v.ToString();
}
private void OnWidgetScaleSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.ScaleSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private static readonly float[] EditorViewportRotateSnapValues =
{
1.0f,
5.0f,
10.0f,
15.0f,
30.0f,
45.0f,
60.0f,
90.0f,
};
private void OnWidgetRotateSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.RotationSnapValue = v;
_rotateSnapping.Text = v.ToString();
}
private void OnWidgetRotateSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.RotationSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private static readonly float[] EditorViewportTranslateSnapValues =
{
0.1f,
0.5f,
1.0f,
5.0f,
10.0f,
100.0f,
1000.0f,
};
private void OnWidgetTranslateSnapClick(ContextMenuButton button)
{
var v = (float)button.Tag;
TransformGizmo.TranslationSnapValue = v;
_translateSnappng.Text = v.ToString();
}
private void OnWidgetTranslateSnapShowHide(Control control)
{
if (control.Visible == false)
return;
var ccm = (ContextMenu)control;
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b)
{
var v = (float)b.Tag;
b.Icon = Mathf.Abs(TransformGizmo.TranslationSnapValue - v) < 0.001f
? Style.Current.CheckBoxTick
: SpriteHandle.Invalid;
}
}
}
private void OnSelectionChanged()
{
Gizmos.ForEach(x => x.OnSelectionChanged(_window.Selection));
@@ -585,23 +397,6 @@ namespace FlaxEditor.Viewport
}
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
// Selected UI controls outline
for (var i = 0; i < _window.Selection.Count; i++)
{
if (_window.Selection[i]?.EditableObject is UIControl controlActor && controlActor && controlActor.Control != null)
{
var control = controlActor.Control;
var bounds = Rectangle.FromPoints(control.PointToParent(this, Float2.Zero), control.PointToParent(this, control.Size));
Render2D.DrawRectangle(bounds, Editor.Instance.Options.Options.Visual.SelectionOutlineColor0, Editor.Instance.Options.Options.Visual.UISelectionOutlineSize);
}
}
}
/// <inheritdoc />
protected override void OnLeftMouseButtonUp()
{
@@ -744,14 +539,7 @@ namespace FlaxEditor.Viewport
/// <param name="orientation">The target view orientation.</param>
public void FocusSelection(ref Quaternion orientation)
{
if (TransformGizmo.SelectedParents.Count == 0)
return;
var gizmoBounds = Gizmos.Active.FocusBounds;
if (gizmoBounds != BoundingSphere.Empty)
((FPSCamera)ViewportCamera).ShowSphere(ref gizmoBounds, ref orientation);
else
((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orientation);
ViewportCamera.FocusSelection(Gizmos, ref orientation);
}
/// <inheritdoc />
@@ -776,6 +564,13 @@ namespace FlaxEditor.Viewport
/// <inheritdoc />
public override void OnDestroy()
{
if (IsDisposing)
return;
if (_tempDebugDrawContext != IntPtr.Zero)
{
DebugDraw.FreeContext(_tempDebugDrawContext);
_tempDebugDrawContext = IntPtr.Zero;
}
FlaxEngine.Object.Destroy(ref SelectionOutline);
FlaxEngine.Object.Destroy(ref _spritesRenderer);
@@ -799,6 +594,15 @@ namespace FlaxEditor.Viewport
{
base.OnDebugDraw(context, ref renderContext);
// Collect selected objects debug shapes again when DebugDraw is active with a custom context
_debugDrawData.Clear();
var selectedParents = TransformGizmo.SelectedParents;
for (int i = 0; i < selectedParents.Count; i++)
{
if (selectedParents[i].IsActiveInHierarchy)
selectedParents[i].OnDebugDraw(_debugDrawData);
}
unsafe
{
fixed (IntPtr* actors = _debugDrawData.ActorsPtrs)

View File

@@ -21,7 +21,7 @@ namespace FlaxEditor.Viewport.Previews
/// <seealso cref="FlaxEditor.Viewport.EditorViewport" />
public abstract class AssetPreview : EditorViewport, IEditorPrimitivesOwner
{
private ContextMenuButton _showDefaultSceneButton;
internal ContextMenuButton _showDefaultSceneButton;
private IntPtr _debugDrawContext;
private bool _debugDrawEnable;
private bool _editorPrimitivesEnable;
@@ -239,6 +239,8 @@ namespace FlaxEditor.Viewport.Previews
/// <inheritdoc />
public override void OnDestroy()
{
if (IsDisposing)
return;
Object.Destroy(ref PreviewLight);
Object.Destroy(ref EnvProbe);
Object.Destroy(ref Sky);

View File

@@ -1,8 +1,8 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
@@ -13,19 +13,11 @@ namespace FlaxEditor.Viewport.Previews
/// <seealso cref="AssetPreview" />
public class PrefabPreview : AssetPreview
{
/// <summary>
/// The currently spawned prefab instance owner. Used to link some actors such as UIControl to preview scene and view.
/// </summary>
internal static PrefabPreview LoadingPreview;
/// <summary>
/// The list of active prefab previews. Used to link some actors such as UIControl to preview scene and view.
/// </summary>
internal static List<PrefabPreview> ActivePreviews;
private Prefab _prefab;
private Actor _instance;
internal UIControl customControlLinked;
private UIControl _uiControlLinked;
internal bool _hasUILinked;
internal ContainerControl _uiParentLink;
/// <summary>
/// Gets or sets the prefab asset to preview.
@@ -54,13 +46,10 @@ namespace FlaxEditor.Viewport.Previews
_prefab.WaitForLoaded();
// Spawn prefab
LoadingPreview = this;
var instance = PrefabManager.SpawnPrefab(_prefab, null);
LoadingPreview = null;
if (instance == null)
{
_prefab = null;
ActivePreviews.Remove(this);
throw new Exception("Failed to spawn a prefab for the preview.");
}
@@ -84,11 +73,11 @@ namespace FlaxEditor.Viewport.Previews
if (_instance)
{
// Unlink UI control
if (customControlLinked)
if (_uiControlLinked)
{
if (customControlLinked.Control?.Parent == this)
customControlLinked.Control.Parent = null;
customControlLinked = null;
if (_uiControlLinked.Control?.Parent == _uiParentLink)
_uiControlLinked.Control.Parent = null;
_uiControlLinked = null;
}
// Remove for the preview
@@ -96,27 +85,51 @@ namespace FlaxEditor.Viewport.Previews
}
_instance = value;
_hasUILinked = false;
if (_instance)
{
// Add to the preview
Task.AddCustomActor(_instance);
// Link UI canvases to the preview
LinkCanvas(_instance);
UpdateLinkage();
}
}
}
private void UpdateLinkage()
{
// Clear flag
_hasUILinked = false;
// Link UI canvases to the preview (eg. after canvas added to the prefab)
LinkCanvas(_instance);
// Link UI control to the preview
if (_uiControlLinked == null &&
_instance is UIControl uiControl &&
uiControl.Control != null &&
uiControl.Control.Parent == null)
{
uiControl.Control.Parent = _uiParentLink;
_uiControlLinked = uiControl;
_hasUILinked = true;
}
else if (_uiControlLinked != null)
_hasUILinked = true;
}
private void LinkCanvas(Actor actor)
{
if (actor is UICanvas uiCanvas)
uiCanvas.EditorOverride(Task, this);
{
uiCanvas.EditorOverride(Task, _uiParentLink);
if (uiCanvas.GUI.Parent == _uiParentLink)
_hasUILinked = true;
}
var children = actor.ChildrenCount;
for (int i = 0; i < children; i++)
{
LinkCanvas(actor.GetChild(i));
}
}
/// <summary>
@@ -126,9 +139,8 @@ namespace FlaxEditor.Viewport.Previews
public PrefabPreview(bool useWidgets)
: base(useWidgets)
{
if (ActivePreviews == null)
ActivePreviews = new List<PrefabPreview>();
ActivePreviews.Add(this);
// Link to itself by default
_uiParentLink = this;
}
/// <inheritdoc />
@@ -138,15 +150,13 @@ namespace FlaxEditor.Viewport.Previews
if (_instance != null)
{
// Link UI canvases to the preview (eg. after canvas added to the prefab)
LinkCanvas(_instance);
UpdateLinkage();
}
}
/// <inheritdoc />
public override void OnDestroy()
{
ActivePreviews.Remove(this);
Prefab = null;
base.OnDestroy();

View File

@@ -146,7 +146,7 @@ namespace FlaxEditor.Viewport
var gridPlane = new Plane(Vector3.Zero, Vector3.Up);
var flags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives;
hit = _owner.SceneGraphRoot.RayCast(ref ray, ref view, out var closest, out var normal, flags);
var girdGizmo = (GridGizmo)_owner.Gizmos.FirstOrDefault(x => x is GridGizmo);
var girdGizmo = _owner.Gizmos.Get<GridGizmo>();
if (hit != null)
{
// Use hit location
@@ -180,7 +180,7 @@ namespace FlaxEditor.Viewport
var location = hitLocation + new Vector3(0, bottomToCenter, 0);
// Apply grid snapping if enabled
var transformGizmo = (TransformGizmo)_owner.Gizmos.FirstOrDefault(x => x is TransformGizmo);
var transformGizmo = _owner.Gizmos.Get<TransformGizmo>();
if (transformGizmo != null && (_owner.UseSnapping || transformGizmo.TranslationSnapEnable))
{
float snapValue = transformGizmo.TranslationSnapValue;

View File

@@ -186,6 +186,10 @@ namespace FlaxEditor.Windows.Assets
base.Initialize(layout);
// Ignore import settings GUI if the type is not animation. This removes the import UI if the animation asset was not created using an import.
if (proxy.ImportSettings.Settings.Type != FlaxEngine.Tools.ModelTool.ModelType.Animation)
return;
// Import Settings
{
var group = layout.Group("Import Settings");

View File

@@ -76,37 +76,37 @@ namespace FlaxEditor.Windows.Assets
// Transparency
[EditorOrder(200), DefaultValue(MaterialTransparentLightingMode.Surface), EditorDisplay("Transparency"), Tooltip("Transparent material lighting mode.")]
[EditorOrder(200), DefaultValue(MaterialTransparentLightingMode.Surface), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Transparent material lighting mode.")]
public MaterialTransparentLightingMode TransparentLightingMode;
[EditorOrder(205), DefaultValue(true), EditorDisplay("Transparency"), Tooltip("Enables reflections when rendering material.")]
[EditorOrder(205), DefaultValue(true), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Enables reflections when rendering material.")]
public bool EnableReflections;
[VisibleIf(nameof(EnableReflections))]
[EditorOrder(210), DefaultValue(false), EditorDisplay("Transparency"), Tooltip("Enables Screen Space Reflections when rendering material.")]
[EditorOrder(210), DefaultValue(false), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Enables Screen Space Reflections when rendering material.")]
public bool EnableScreenSpaceReflections;
[EditorOrder(210), DefaultValue(true), EditorDisplay("Transparency"), Tooltip("Enables fog effects when rendering material.")]
[EditorOrder(210), DefaultValue(true), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Enables fog effects when rendering material.")]
public bool EnableFog;
[EditorOrder(220), DefaultValue(true), EditorDisplay("Transparency"), Tooltip("Enables distortion effect when rendering.")]
[EditorOrder(220), DefaultValue(true), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Enables distortion effect when rendering.")]
public bool EnableDistortion;
[EditorOrder(224), DefaultValue(false), EditorDisplay("Transparency"), Tooltip("Enables sampling Global Illumination in material (eg. light probes or volumetric lightmap).")]
[EditorOrder(224), DefaultValue(false), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Enables sampling Global Illumination in material (eg. light probes or volumetric lightmap).")]
public bool EnableGlobalIllumination;
[EditorOrder(225), DefaultValue(false), EditorDisplay("Transparency"), Tooltip("Enables refraction offset based on the difference between the per-pixel normal and the per-vertex normal. Useful for large water-like surfaces.")]
[EditorOrder(225), DefaultValue(false), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Enables refraction offset based on the difference between the per-pixel normal and the per-vertex normal. Useful for large water-like surfaces.")]
public bool PixelNormalOffsetRefraction;
[EditorOrder(230), DefaultValue(0.12f), EditorDisplay("Transparency"), Tooltip("Controls opacity values clipping point."), Limit(0.0f, 1.0f, 0.01f)]
[EditorOrder(230), DefaultValue(0.12f), VisibleIf(nameof(IsStandard)), EditorDisplay("Transparency"), Tooltip("Controls opacity values clipping point."), Limit(0.0f, 1.0f, 0.01f)]
public float OpacityThreshold;
// Tessellation
[EditorOrder(300), DefaultValue(TessellationMethod.None), EditorDisplay("Tessellation"), Tooltip("Mesh tessellation method.")]
[EditorOrder(300), DefaultValue(TessellationMethod.None), VisibleIf(nameof(IsStandard)), EditorDisplay("Tessellation"), Tooltip("Mesh tessellation method.")]
public TessellationMethod TessellationMode;
[EditorOrder(310), DefaultValue(15), EditorDisplay("Tessellation"), Tooltip("Maximum triangle tessellation factor."), Limit(1, 60, 0.01f)]
[EditorOrder(310), DefaultValue(15), VisibleIf(nameof(IsStandard)), EditorDisplay("Tessellation"), Tooltip("Maximum triangle tessellation factor."), Limit(1, 60, 0.01f)]
public int MaxTessellationFactor;
// Misc
@@ -120,10 +120,10 @@ namespace FlaxEditor.Windows.Assets
[EditorOrder(420), DefaultValue(0.3f), EditorDisplay("Misc"), Tooltip("Controls mask values clipping point."), Limit(0.0f, 1.0f, 0.01f)]
public float MaskThreshold;
[EditorOrder(430), DefaultValue(MaterialDecalBlendingMode.Translucent), EditorDisplay("Misc"), Tooltip("The decal material blending mode.")]
[EditorOrder(430), DefaultValue(MaterialDecalBlendingMode.Translucent), VisibleIf(nameof(IsDecal)), EditorDisplay("Misc"), Tooltip("The decal material blending mode.")]
public MaterialDecalBlendingMode DecalBlendingMode;
[EditorOrder(440), DefaultValue(MaterialPostFxLocation.AfterPostProcessingPass), EditorDisplay("Misc"), Tooltip("The post fx material rendering location.")]
[EditorOrder(440), DefaultValue(MaterialPostFxLocation.AfterPostProcessingPass), VisibleIf(nameof(IsPostProcess)), EditorDisplay("Misc"), Tooltip("The post fx material rendering location.")]
public MaterialPostFxLocation PostFxLocation;
// Parameters
@@ -140,6 +140,12 @@ namespace FlaxEditor.Windows.Assets
set => throw new Exception("No setter.");
}
// Visibility conditionals
private bool IsPostProcess => Domain == MaterialDomain.PostProcess;
private bool IsDecal => Domain == MaterialDomain.Decal;
private bool IsStandard => Domain == MaterialDomain.Surface || Domain == MaterialDomain.Terrain || Domain == MaterialDomain.Particle || Domain == MaterialDomain.Deformable;
/// <summary>
/// Gathers parameters from the specified material.
/// </summary>

View File

@@ -360,10 +360,9 @@ namespace FlaxEditor.Windows.Assets
/// </summary>
/// <param name="parent">The parent control.</param>
/// <param name="location">The location (within a given control).</param>
private void ShowContextMenu(Control parent, ref Float2 location)
internal void ShowContextMenu(Control parent, ref Float2 location)
{
var contextMenu = CreateContextMenu();
contextMenu.Show(parent, location);
}

View File

@@ -3,11 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.Tree;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.GUI;
using FlaxEditor.Viewport.Cameras;
using FlaxEngine;
namespace FlaxEditor.Windows.Assets
@@ -64,8 +62,11 @@ namespace FlaxEditor.Windows.Assets
private void OnSelectionUndo(SceneGraphNode[] toSelect)
{
Selection.Clear();
Selection.AddRange(toSelect);
foreach (var e in toSelect)
{
if (e != null)
Selection.Add(e);
}
OnSelectionChanges();
}
@@ -118,11 +119,13 @@ namespace FlaxEditor.Windows.Assets
/// <param name="nodes">The nodes.</param>
public void Select(List<SceneGraphNode> nodes)
{
nodes?.RemoveAll(x => x == null);
if (nodes == null || nodes.Count == 0)
{
Deselect();
return;
}
if (Utils.ArraysEqual(Selection, nodes))
return;

View File

@@ -132,7 +132,7 @@ namespace FlaxEditor.Windows.Assets
IsScrollable = false,
Offsets = new Margin(0, 0, 0, 18 + 6),
};
_searchBox = new SearchBox()
_searchBox = new SearchBox
{
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
Parent = headerPanel,
@@ -140,7 +140,8 @@ namespace FlaxEditor.Windows.Assets
};
_searchBox.TextChanged += OnSearchBoxTextChanged;
_treePanel = new Panel()
// Prefab structure tree
_treePanel = new Panel
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = new Margin(0.0f, 0.0f, headerPanel.Bottom, 0.0f),
@@ -148,8 +149,6 @@ namespace FlaxEditor.Windows.Assets
IsScrollable = true,
Parent = sceneTreePanel,
};
// Prefab structure tree
Graph = new LocalSceneGraph(new CustomRootNode(this));
Graph.Root.TreeNode.Expand(true);
_tree = new PrefabTree
@@ -316,11 +315,7 @@ namespace FlaxEditor.Windows.Assets
return;
// Restore
_viewport.Prefab = _asset;
Graph.MainActor = _viewport.Instance;
Selection.Clear();
Select(Graph.Main);
Graph.Root.TreeNode.Expand(true);
OnPrefabOpened();
_undo.Clear();
ClearEditedFlag();
}
@@ -346,6 +341,16 @@ namespace FlaxEditor.Windows.Assets
}
}
private void OnPrefabOpened()
{
_viewport.Prefab = _asset;
_viewport.UpdateGizmoMode();
Graph.MainActor = _viewport.Instance;
Selection.Clear();
Select(Graph.Main);
Graph.Root.TreeNode.Expand(true);
}
/// <inheritdoc />
public override void Save()
{
@@ -355,6 +360,8 @@ namespace FlaxEditor.Windows.Assets
try
{
Editor.Scene.OnSaveStart(_viewport._uiParentLink);
// Simply update changes
Editor.Prefabs.ApplyAll(_viewport.Instance);
@@ -371,6 +378,10 @@ namespace FlaxEditor.Windows.Assets
throw;
}
finally
{
Editor.Scene.OnSaveEnd(_viewport._uiParentLink);
}
}
/// <inheritdoc />
@@ -411,13 +422,8 @@ namespace FlaxEditor.Windows.Assets
return;
}
_viewport.Prefab = _asset;
Graph.MainActor = _viewport.Instance;
OnPrefabOpened();
_focusCamera = true;
Selection.Clear();
Select(Graph.Main);
Graph.Root.TreeNode.Expand(true);
_undo.Clear();
ClearEditedFlag();
@@ -462,11 +468,7 @@ namespace FlaxEditor.Windows.Assets
_viewport.Prefab = null;
if (_asset.IsLoaded)
{
_viewport.Prefab = _asset;
Graph.MainActor = _viewport.Instance;
Selection.Clear();
Select(Graph.Main);
Graph.Root.TreeNode.ExpandAll(true);
OnPrefabOpened();
}
}
finally
@@ -478,7 +480,6 @@ namespace FlaxEditor.Windows.Assets
if (_focusCamera && _viewport.Task.FrameCount > 1)
{
_focusCamera = false;
Editor.GetActorEditorSphere(_viewport.Instance, out BoundingSphere bounds);
_viewport.ViewPosition = bounds.Center - _viewport.ViewDirection * (bounds.Radius * 1.2f);
}

View File

@@ -281,6 +281,9 @@ namespace FlaxEditor.Windows
_view.OnDelete += Delete;
_view.OnDuplicate += Duplicate;
_view.OnPaste += Paste;
_view.InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
}
private ContextMenu OnViewDropdownPopupCreate(ComboBox comboBox)

View File

@@ -23,6 +23,7 @@ namespace FlaxEditor.Windows
private Tabs _tabs;
private EditorOptions _options;
private ToolStripButton _saveButton;
private readonly Undo _undo;
private readonly List<Tab> _customTabs = new List<Tab>();
/// <summary>
@@ -33,6 +34,12 @@ namespace FlaxEditor.Windows
: base(editor, true, ScrollBars.None)
{
Title = "Editor Options";
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
_undo.RedoDone += OnUndoRedo;
_undo.ActionDone += OnUndoRedo;
var toolstrip = new ToolStrip
{
@@ -58,9 +65,19 @@ namespace FlaxEditor.Windows
CreateTab("Visual", () => _options.Visual);
CreateTab("Source Code", () => _options.SourceCode);
CreateTab("Theme", () => _options.Theme);
// Setup input actions
InputActions.Add(options => options.Undo, _undo.PerformUndo);
InputActions.Add(options => options.Redo, _undo.PerformRedo);
InputActions.Add(options => options.Save, SaveData);
_tabs.SelectedTabIndex = 0;
}
private void OnUndoRedo(IUndoAction action)
{
MarkAsEdited();
}
private Tab CreateTab(string name, Func<object> getValue)
{
@@ -73,7 +90,7 @@ namespace FlaxEditor.Windows
Parent = tab
};
var settings = new CustomEditorPresenter(null);
var settings = new CustomEditorPresenter(_undo);
settings.Panel.Parent = panel;
settings.Panel.Tag = getValue;
settings.Modified += MarkAsEdited;

View File

@@ -38,6 +38,7 @@ namespace FlaxEditor.Windows
protected EditorWindow(Editor editor, bool hideOnClose, ScrollBars scrollBars)
: base(editor.UI.MasterPanel, hideOnClose, scrollBars)
{
AutoFocus = true;
Editor = editor;
InputActions.Add(options => options.ContentFinder, () =>

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Xml;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEditor.Options;
@@ -194,133 +195,14 @@ namespace FlaxEditor.Windows
public bool Active;
}
private class GameRoot : ContainerControl
/// <summary>
/// Root control for game UI preview in Editor. Supports basic UI editing via <see cref="UIEditorRoot"/>.
/// </summary>
private class GameRoot : UIEditorRoot
{
public bool EnableEvents => !Time.GamePaused;
public override bool RayCast(ref Float2 location, out Control hit)
{
return RayCastChildren(ref location, out hit);
}
public override bool ContainsPoint(ref Float2 location, bool precise = false)
{
if (precise)
return false;
return base.ContainsPoint(ref location, precise);
}
public override bool OnCharInput(char c)
{
if (!EnableEvents)
return false;
return base.OnCharInput(c);
}
public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
{
if (!EnableEvents)
return DragDropEffect.None;
return base.OnDragDrop(ref location, data);
}
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
if (!EnableEvents)
return DragDropEffect.None;
return base.OnDragEnter(ref location, data);
}
public override void OnDragLeave()
{
if (!EnableEvents)
return;
base.OnDragLeave();
}
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
if (!EnableEvents)
return DragDropEffect.None;
return base.OnDragMove(ref location, data);
}
public override bool OnKeyDown(KeyboardKeys key)
{
if (!EnableEvents)
return false;
return base.OnKeyDown(key);
}
public override void OnKeyUp(KeyboardKeys key)
{
if (!EnableEvents)
return;
base.OnKeyUp(key);
}
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
{
if (!EnableEvents)
return false;
return base.OnMouseDoubleClick(location, button);
}
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (!EnableEvents)
return false;
return base.OnMouseDown(location, button);
}
public override void OnMouseEnter(Float2 location)
{
if (!EnableEvents)
return;
base.OnMouseEnter(location);
}
public override void OnMouseLeave()
{
if (!EnableEvents)
return;
base.OnMouseLeave();
}
public override void OnMouseMove(Float2 location)
{
if (!EnableEvents)
return;
base.OnMouseMove(location);
}
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (!EnableEvents)
return false;
return base.OnMouseUp(location, button);
}
public override bool OnMouseWheel(Float2 location, float delta)
{
if (!EnableEvents)
return false;
return base.OnMouseWheel(location, delta);
}
public override bool EnableInputs => !Time.GamePaused && Editor.IsPlayMode;
public override bool EnableSelecting => !Editor.IsPlayMode || Time.GamePaused;
public override TransformGizmo TransformGizmo => Editor.Instance.MainTransformGizmo;
}
/// <summary>
@@ -348,13 +230,9 @@ namespace FlaxEditor.Windows
// Override the game GUI root
_guiRoot = new GameRoot
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
//Visible = false,
AutoFocus = false,
Parent = _viewport
};
RootControl.GameRoot = _guiRoot;
RootControl.GameRoot = _guiRoot.UIRoot;
SizeChanged += control => { ResizeViewport(); };
@@ -382,6 +260,56 @@ namespace FlaxEditor.Windows
Editor.Instance.Windows.ProfilerWin.Clear();
Editor.Instance.UI.AddStatusMessage($"Profiling results cleared.");
});
InputActions.Add(options => options.Save, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.SaveAll();
});
InputActions.Add(options => options.Undo, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.PerformUndo();
Focus();
});
InputActions.Add(options => options.Redo, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.PerformRedo();
Focus();
});
InputActions.Add(options => options.Cut, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.SceneEditing.Cut();
});
InputActions.Add(options => options.Copy, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.SceneEditing.Copy();
});
InputActions.Add(options => options.Paste, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.SceneEditing.Paste();
});
InputActions.Add(options => options.Duplicate, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.SceneEditing.Duplicate();
});
InputActions.Add(options => options.Delete, () =>
{
if (Editor.IsPlayMode)
return;
Editor.Instance.SceneEditing.Delete();
});
}
private void ChangeViewportRatio(ViewportScaleOptions v)
@@ -916,35 +844,6 @@ namespace FlaxEditor.Windows
Render2D.DrawText(style.FontLarge, "No camera", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
}
// Selected UI controls outline
bool drawAnySelectedControl = false;
// TODO: optimize this (eg. cache list of selected UIControl's when selection gets changed)
var selection = Editor.SceneEditing.Selection;
for (var i = 0; i < selection.Count; i++)
{
if (selection[i].EditableObject is UIControl controlActor && controlActor && controlActor.Control != null)
{
if (!drawAnySelectedControl)
{
drawAnySelectedControl = true;
Render2D.PushTransform(ref _viewport._cachedTransform);
}
var options = Editor.Options.Options.Visual;
var control = controlActor.Control;
var bounds = control.EditorBounds;
var p1 = control.PointToParent(_viewport, bounds.UpperLeft);
var p2 = control.PointToParent(_viewport, bounds.UpperRight);
var p3 = control.PointToParent(_viewport, bounds.BottomLeft);
var p4 = control.PointToParent(_viewport, bounds.BottomRight);
var min = Float2.Min(Float2.Min(p1, p2), Float2.Min(p3, p4));
var max = Float2.Max(Float2.Max(p1, p2), Float2.Max(p3, p4));
bounds = new Rectangle(min, Float2.Max(max - min, Float2.Zero));
Render2D.DrawRectangle(bounds, options.SelectionOutlineColor0, options.UISelectionOutlineSize);
}
}
if (drawAnySelectedControl)
Render2D.PopTransform();
// Play mode hints and overlay
if (Editor.StateMachine.IsPlayMode)
{

View File

@@ -65,6 +65,11 @@ namespace FlaxEditor.Windows
/// </summary>
public OutputLogWindow Window;
/// <summary>
/// The input actions collection to processed during user input.
/// </summary>
public InputActionsContainer InputActions = new InputActionsContainer();
/// <summary>
/// The default text style.
/// </summary>
@@ -80,6 +85,14 @@ namespace FlaxEditor.Windows
/// </summary>
public TextBlockStyle ErrorStyle;
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (InputActions.Process(Editor.Instance, this, key))
return true;
return base.OnKeyDown(key);
}
/// <inheritdoc />
protected override void OnParseTextBlocks()
{
@@ -201,6 +214,9 @@ namespace FlaxEditor.Windows
// Setup editor options
Editor.Options.OptionsChanged += OnEditorOptionsChanged;
OnEditorOptionsChanged(Editor.Options.Options);
_output.InputActions.Add(options => options.Search, () => _searchBox.Focus());
InputActions.Add(options => options.Search, () => _searchBox.Focus());
GameCooker.Event += OnGameCookerEvent;
ScriptsBuilder.CompilationFailed += OnScriptsCompilationFailed;

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
@@ -258,10 +257,9 @@ namespace FlaxEditor.Windows
/// </summary>
/// <param name="parent">The parent control.</param>
/// <param name="location">The location (within a given control).</param>
private void ShowContextMenu(Control parent, Float2 location)
internal void ShowContextMenu(Control parent, Float2 location)
{
var contextMenu = CreateContextMenu();
contextMenu.Show(parent, location);
}

View File

@@ -93,6 +93,7 @@ namespace FlaxEditor.Windows
InputActions.Add(options => options.RotateMode, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
InputActions.Add(options => options.ScaleMode, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
InputActions.Add(options => options.FocusSelection, () => Editor.Windows.EditWin.Viewport.FocusSelection());
InputActions.Add(options => options.LockFocusSelection, () => Editor.Windows.EditWin.Viewport.LockFocusSelection());
InputActions.Add(options => options.Rename, Rename);
}
@@ -221,7 +222,6 @@ namespace FlaxEditor.Windows
{
if (!Editor.StateMachine.CurrentState.CanEditScene)
return;
ShowContextMenu(node, location);
}

View File

@@ -474,8 +474,8 @@ BehaviorUpdateResult BehaviorTreeMoveToNode::Update(const BehaviorUpdateContext&
state->NavAgentRadius = navMesh->Properties.Agent.Radius;
// Place start and end on navmesh
navMesh->ProjectPoint(state->Path.First(), state->Path.First());
navMesh->ProjectPoint(state->Path.Last(), state->Path.Last());
navMesh->FindClosestPoint(state->Path.First(), state->Path.First());
navMesh->FindClosestPoint(state->Path.Last(), state->Path.Last());
// Calculate offset between path and the agent (aka feet offset)
state->AgentOffset = state->Path.First() - agentLocation;

View File

@@ -281,10 +281,26 @@ Asset::LoadResult SceneAnimation::load()
track.TrackStateIndex = TrackStatesCount++;
trackRuntime->PropertyName = stream.Move<char>(trackData->PropertyNameLength + 1);
trackRuntime->PropertyTypeName = stream.Move<char>(trackData->PropertyTypeNameLength + 1);
const int32 keyframesDataSize = trackData->KeyframesCount * (sizeof(float) + trackData->ValueSize);
int32 keyframesDataSize = trackData->KeyframesCount * (sizeof(float) + trackData->ValueSize);
if (trackData->ValueSize == 0)
{
// When using json data (from non-POD types) read the sum of all keyframes data
const int32 keyframesDataStart = stream.GetPosition();
for (int32 j = 0; j < trackData->KeyframesCount; j++)
{
stream.Move<float>(); // Time
int32 jsonLen;
stream.ReadInt32(&jsonLen);
stream.Move(jsonLen);
}
const int32 keyframesDataEnd = stream.GetPosition();
stream.SetPosition(keyframesDataStart);
keyframesDataSize = keyframesDataEnd - keyframesDataStart;
}
trackRuntime->ValueSize = trackData->ValueSize;
trackRuntime->KeyframesCount = trackData->KeyframesCount;
trackRuntime->Keyframes = stream.Move(keyframesDataSize);
trackRuntime->KeyframesSize = keyframesDataSize;
needsParent = true;
break;
}
@@ -298,6 +314,7 @@ Asset::LoadResult SceneAnimation::load()
trackRuntime->PropertyName = stream.Move<char>(trackData->PropertyNameLength + 1);
trackRuntime->PropertyTypeName = stream.Move<char>(trackData->PropertyTypeNameLength + 1);
const int32 keyframesDataSize = trackData->KeyframesCount * (sizeof(float) + trackData->ValueSize * 3);
ASSERT(trackData->ValueSize > 0);
trackRuntime->ValueSize = trackData->ValueSize;
trackRuntime->KeyframesCount = trackData->KeyframesCount;
trackRuntime->Keyframes = stream.Move(keyframesDataSize);
@@ -375,6 +392,7 @@ Asset::LoadResult SceneAnimation::load()
trackRuntime->PropertyName = stream.Move<char>(trackData->PropertyNameLength + 1);
trackRuntime->PropertyTypeName = stream.Move<char>(trackData->PropertyTypeNameLength + 1);
trackRuntime->ValueSize = trackData->ValueSize;
ASSERT(trackData->ValueSize > 0);
trackRuntime->KeyframesCount = trackData->KeyframesCount;
const auto keyframesTimes = (float*)((byte*)trackRuntime + sizeof(StringPropertyTrack::Runtime));
const auto keyframesLengths = (int32*)((byte*)keyframesTimes + sizeof(float) * trackData->KeyframesCount);

View File

@@ -289,6 +289,7 @@ public:
/// The keyframes array (items count is KeyframesCount). Each keyframe is represented by pair of time (of type float) and the value data (of size ValueSize).
/// </summary>
void* Keyframes;
int32 KeyframesSize;
};
};

View File

@@ -7,6 +7,7 @@
#include "Engine/Level/SceneObjectsFactory.h"
#include "Engine/Level/Actors/Camera.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Audio/AudioClip.h"
#include "Engine/Audio/AudioSource.h"
#include "Engine/Graphics/RenderTask.h"
@@ -19,6 +20,7 @@
#include "Engine/Scripting/ManagedCLR/MField.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MMethod.h"
#include "Engine/Scripting/Internal/ManagedSerialization.h"
// This could be Update, LateUpdate or FixedUpdate
#define UPDATE_POINT Update
@@ -370,47 +372,96 @@ bool SceneAnimationPlayer::TickPropertyTrack(int32 trackIndex, int32 stateIndexO
case SceneAnimation::Track::Types::KeyframesProperty:
case SceneAnimation::Track::Types::ObjectReferenceProperty:
{
const auto trackDataKeyframes = track.GetRuntimeData<SceneAnimation::KeyframesPropertyTrack::Runtime>();
const int32 count = trackDataKeyframes->KeyframesCount;
const auto trackRuntime = track.GetRuntimeData<SceneAnimation::KeyframesPropertyTrack::Runtime>();
const int32 count = trackRuntime->KeyframesCount;
if (count == 0)
return false;
// Find the keyframe at time
int32 keyframeSize = sizeof(float) + trackDataKeyframes->ValueSize;
#define GET_KEY_TIME(idx) *(float*)((byte*)trackDataKeyframes->Keyframes + keyframeSize * (idx))
const float keyTime = Math::Clamp(time, 0.0f, GET_KEY_TIME(count - 1));
int32 start = 0;
int32 searchLength = count;
while (searchLength > 0)
// If size is 0 then track uses Json storage for keyframes data (variable memory length of keyframes), otherwise it's optimized simple data with O(1) access
if (trackRuntime->ValueSize != 0)
{
const int32 half = searchLength >> 1;
int32 mid = start + half;
if (keyTime < GET_KEY_TIME(mid))
// Find the keyframe at time (binary search)
int32 keyframeSize = sizeof(float) + trackRuntime->ValueSize;
#define GET_KEY_TIME(idx) *(float*)((byte*)trackRuntime->Keyframes + keyframeSize * (idx))
const float keyTime = Math::Clamp(time, 0.0f, GET_KEY_TIME(count - 1));
int32 start = 0;
int32 searchLength = count;
while (searchLength > 0)
{
searchLength = half;
const int32 half = searchLength >> 1;
int32 mid = start + half;
if (keyTime < GET_KEY_TIME(mid))
{
searchLength = half;
}
else
{
start = mid + 1;
searchLength -= half + 1;
}
}
int32 leftKey = Math::Max(0, start - 1);
#undef GET_KEY_TIME
// Return the value
void* value = (void*)((byte*)trackRuntime->Keyframes + keyframeSize * (leftKey) + sizeof(float));
if (track.Type == SceneAnimation::Track::Types::ObjectReferenceProperty)
{
// Object ref track uses Guid for object Id storage
Guid id = *(Guid*)value;
_objectsMapping.TryGet(id, id);
auto obj = Scripting::FindObject<ScriptingObject>(id);
value = obj ? obj->GetOrCreateManagedInstance() : nullptr;
*(void**)target = value;
}
else
{
start = mid + 1;
searchLength -= half + 1;
// POD memory
Platform::MemoryCopy(target, value, trackRuntime->ValueSize);
}
}
int32 leftKey = Math::Max(0, start - 1);
#undef GET_KEY_TIME
// Return the value
void* value = (void*)((byte*)trackDataKeyframes->Keyframes + keyframeSize * (leftKey) + sizeof(float));
if (track.Type == SceneAnimation::Track::Types::ObjectReferenceProperty)
{
Guid id = *(Guid*)value;
_objectsMapping.TryGet(id, id);
auto obj = Scripting::FindObject<ScriptingObject>(id);
value = obj ? obj->GetOrCreateManagedInstance() : nullptr;
*(void**)target = value;
}
else
{
Platform::MemoryCopy(target, value, trackDataKeyframes->ValueSize);
// Clear pointer
*(void**)target = nullptr;
// Find the keyframe at time (linear search)
MemoryReadStream stream((byte*)trackRuntime->Keyframes, trackRuntime->KeyframesSize);
int32 prevKeyPos = sizeof(float);
int32 jsonLen;
for (int32 key = 0; key < count; key++)
{
float keyTime;
stream.ReadFloat(&keyTime);
if (keyTime > time)
break;
prevKeyPos = stream.GetPosition();
stream.ReadInt32(&jsonLen);
stream.Move(jsonLen);
}
// Read json text
stream.SetPosition(prevKeyPos);
stream.ReadInt32(&jsonLen);
const StringAnsiView json((const char*)stream.GetPositionHandle(), jsonLen);
// Create empty value of the keyframe type
const auto trackData = track.GetData<SceneAnimation::KeyframesPropertyTrack::Data>();
const StringAnsiView propertyTypeName(trackRuntime->PropertyTypeName, trackData->PropertyTypeNameLength);
MClass* klass = Scripting::FindClass(propertyTypeName);
if (!klass)
return false;
MObject* obj = MCore::Object::New(klass);
if (!obj)
return false;
if (!klass->IsValueType())
MCore::Object::Init(obj);
// Deserialize value from json
ManagedSerialization::Deserialize(json, obj);
// Set value
*(void**)target = obj;
}
break;
}
@@ -479,13 +530,13 @@ bool SceneAnimationPlayer::TickPropertyTrack(int32 trackIndex, int32 stateIndexO
}
case SceneAnimation::Track::Types::StringProperty:
{
const auto trackDataKeyframes = track.GetRuntimeData<SceneAnimation::StringPropertyTrack::Runtime>();
const int32 count = trackDataKeyframes->KeyframesCount;
const auto trackRuntime = track.GetRuntimeData<SceneAnimation::StringPropertyTrack::Runtime>();
const int32 count = trackRuntime->KeyframesCount;
if (count == 0)
return false;
const auto keyframesTimes = (float*)((byte*)trackDataKeyframes + sizeof(SceneAnimation::StringPropertyTrack::Runtime));
const auto keyframesLengths = (int32*)((byte*)keyframesTimes + sizeof(float) * trackDataKeyframes->KeyframesCount);
const auto keyframesValues = (Char**)((byte*)keyframesLengths + sizeof(int32) * trackDataKeyframes->KeyframesCount);
const auto keyframesTimes = (float*)((byte*)trackRuntime + sizeof(SceneAnimation::StringPropertyTrack::Runtime));
const auto keyframesLengths = (int32*)((byte*)keyframesTimes + sizeof(float) * trackRuntime->KeyframesCount);
const auto keyframesValues = (Char**)((byte*)keyframesLengths + sizeof(int32) * trackRuntime->KeyframesCount);
// Find the keyframe at time
#define GET_KEY_TIME(idx) keyframesTimes[idx]
@@ -522,7 +573,7 @@ bool SceneAnimationPlayer::TickPropertyTrack(int32 trackIndex, int32 stateIndexO
auto& childTrack = anim->Tracks[childTrackIndex];
if (childTrack.Disabled || childTrack.ParentIndex != trackIndex)
continue;
const auto childTrackRuntimeData = childTrack.GetRuntimeData<SceneAnimation::PropertyTrack::Runtime>();
const auto childTrackRuntime = childTrack.GetRuntimeData<SceneAnimation::PropertyTrack::Runtime>();
auto& childTrackState = _tracks[stateIndexOffset + childTrack.TrackStateIndex];
// Cache field
@@ -532,7 +583,7 @@ bool SceneAnimationPlayer::TickPropertyTrack(int32 trackIndex, int32 stateIndexO
if (!type)
continue;
MClass* mclass = MCore::Type::GetClass(type);
childTrackState.Field = mclass->GetField(childTrackRuntimeData->PropertyName);
childTrackState.Field = mclass->GetField(childTrackRuntime->PropertyName);
if (!childTrackState.Field)
continue;
}
@@ -956,7 +1007,8 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3
if (TickPropertyTrack(j, stateIndexOffset, anim, time, track, state, value))
{
// Set the value
if (MCore::Type::IsPointer(valueType))
auto valueTypes = MCore::Type::GetType(valueType);
if (valueTypes == MTypes::Object || MCore::Type::IsPointer(valueType))
value = (void*)*(intptr*)value;
if (state.Property)
{

View File

@@ -0,0 +1,171 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Math/Transform.h"
/// <summary>
/// The helper class for that handles active audio backend operations.
/// </summary>
class AudioBackendTools
{
public:
struct Settings
{
float Volume = 1.0f;
float DopplerFactor = 1.0f;
};
struct Listener
{
Vector3 Velocity;
Vector3 Position;
Quaternion Orientation;
};
struct Source
{
bool Is3D;
float Volume;
float Pitch;
float Pan;
float MinDistance;
float Attenuation;
float DopplerFactor;
Vector3 Velocity;
Vector3 Position;
Quaternion Orientation;
};
enum Channels
{
FrontLeft = 0,
FrontRight = 1,
FontCenter = 2,
BackLeft = 3,
BackRight = 4,
SideLeft = 5,
SideRight = 6,
MaxChannels
};
struct SoundMix
{
float Pitch;
float Volume;
float Channels[MaxChannels];
void VolumeIntoChannels()
{
for (float& c : Channels)
c *= Volume;
Volume = 1.0f;
}
};
static SoundMix CalculateSoundMix(const Settings& settings, const Listener& listener, const Source& source, int32 channelCount = 2)
{
ASSERT_LOW_LAYER(channelCount <= MaxChannels);
SoundMix mix;
mix.Pitch = source.Pitch;
mix.Volume = source.Volume * settings.Volume;
Platform::MemoryClear(mix.Channels, sizeof(mix.Channels));
if (source.Is3D)
{
const Transform listenerTransform(listener.Position, listener.Orientation);
float distance = (float)Vector3::Distance(listener.Position, source.Position);
float gain = 1;
// Calculate attenuation (OpenAL formula for mode: AL_INVERSE_DISTANCE_CLAMPED)
// [https://www.openal.org/documentation/openal-1.1-specification.pdf]
distance = Math::Clamp(distance, source.MinDistance, MAX_float);
const float dst = source.MinDistance + source.Attenuation * (distance - source.MinDistance);
if (dst > 0)
gain = source.MinDistance / dst;
mix.Volume *= Math::Saturate(gain);
// Calculate panning
// Ramy Sadek and Chris Kyriakakis, 2004, "A Novel Multichannel Panning Method for Standard and Arbitrary Loudspeaker Configurations"
// [https://www.researchgate.net/publication/235080603_A_Novel_Multichannel_Panning_Method_for_Standard_and_Arbitrary_Loudspeaker_Configurations]
static const Float3 ChannelDirections[MaxChannels] =
{
Float3(-1.0, 0.0, -1.0).GetNormalized(),
Float3(1.0, 0.0, -1.0).GetNormalized(),
Float3(0.0, 0.0, -1.0).GetNormalized(),
Float3(-1.0, 0.0, 1.0).GetNormalized(),
Float3(1.0, 0.0, 1.0).GetNormalized(),
Float3(-1.0, 0.0, 0.0).GetNormalized(),
Float3(1.0, 0.0, 0.0).GetNormalized(),
};
const Float3 sourceInListenerSpace = (Float3)listenerTransform.WorldToLocal(source.Position);
const Float3 sourceToListener = Float3::Normalize(sourceInListenerSpace);
float sqGainsSum = 0.0f;
for (int32 i = 0; i < channelCount; i++)
{
float othersSum = 0.0f;
for (int32 j = 0; j < channelCount; j++)
othersSum += (1.0f + Float3::Dot(ChannelDirections[i], ChannelDirections[j])) * 0.5f;
const float sqGain = Math::Square(0.5f * Math::Pow(1.0f + Float3::Dot(ChannelDirections[i], sourceToListener), 2.0f) / othersSum);
sqGainsSum += sqGain;
mix.Channels[i] = sqGain;
}
for (int32 i = 0; i < channelCount; i++)
mix.Channels[i] = Math::Sqrt(mix.Channels[i] / sqGainsSum);
// Calculate doppler
const Float3 velocityInListenerSpace = (Float3)listenerTransform.WorldToLocalVector(source.Velocity - listener.Velocity);
const float velocity = velocityInListenerSpace.Length();
const float dopplerFactor = settings.DopplerFactor * source.DopplerFactor;
if (dopplerFactor > 0.0f && velocity > 0.0f)
{
constexpr float speedOfSound = 343.3f * 100.0f * 100.0f; // in air, in Flax units
const float approachingFactor = Float3::Dot(sourceToListener, velocityInListenerSpace.GetNormalized());
const float dopplerPitch = speedOfSound / (speedOfSound + velocity * approachingFactor);
mix.Pitch *= Math::Clamp(dopplerPitch, 0.1f, 10.0f);
}
}
else
{
const float panLeft = Math::Min(1.0f - source.Pan, 1.0f);
const float panRight = Math::Min(1.0f + source.Pan, 1.0f);
switch (channelCount)
{
case 1:
mix.Channels[0] = 1.0f;
break;
case 2:
default: // TODO: handle other channel configuration (eg. 7.1 or 5.1)
mix.Channels[FrontLeft] = panLeft;
mix.Channels[FrontRight] = panRight;
break;
}
}
return mix;
}
static void MapChannels(int32 sourceChannels, int32 outputChannels, float channels[MaxChannels], float* outputMatrix)
{
Platform::MemoryClear(outputMatrix, sizeof(float) * sourceChannels * outputChannels);
switch (outputChannels)
{
case 1:
outputMatrix[0] = channels[FrontLeft];
break;
case 2:
default: // TODO: implement multi-channel support (eg. 5.1, 7.1)
if (sourceChannels == 1)
{
outputMatrix[0] = channels[FrontLeft];
outputMatrix[1] = channels[FrontRight];
}
else if (sourceChannels == 2)
{
outputMatrix[0] = channels[FrontLeft];
outputMatrix[1] = 0.0f;
outputMatrix[2] = 0.0f;
outputMatrix[3] = channels[FrontRight];
}
break;
}
}
};

View File

@@ -3,7 +3,6 @@
#pragma once
#include "Engine/Core/Config/Settings.h"
#include "Engine/Serialization/Serialization.h"
/// <summary>
/// Audio settings container.
@@ -11,6 +10,7 @@
API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API AudioSettings : public SettingsBase
{
DECLARE_SCRIPTING_TYPE_MINIMAL(AudioSettings);
API_AUTO_SERIALIZATION();
public:
/// <summary>
@@ -46,12 +46,4 @@ public:
// [SettingsBase]
void Apply() override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override
{
DESERIALIZE(DisableAudio);
DESERIALIZE(DopplerFactor);
DESERIALIZE(MuteOnFocusLoss);
DESERIALIZE(EnableHRTF);
}
};

View File

@@ -3,7 +3,7 @@
#if AUDIO_API_XAUDIO2
#include "AudioBackendXAudio2.h"
#include "Engine/Audio/AudioSettings.h"
#include "Engine/Audio/AudioBackendTools.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Collections/ChunkedArray.h"
#include "Engine/Core/Log.h"
@@ -22,10 +22,11 @@
// Documentation: https://docs.microsoft.com/en-us/windows/desktop/xaudio2/xaudio2-apis-portal
#include <xaudio2.h>
//#include <xaudio2fx.h>
#include <x3daudio.h>
//#include <x3daudio.h>
// TODO: implement multi-channel support (eg. 5.1, 7.1)
#define MAX_INPUT_CHANNELS 2
#define MAX_OUTPUT_CHANNELS 8
#define MAX_OUTPUT_CHANNELS 2
#define MAX_CHANNELS_MATRIX_SIZE (MAX_INPUT_CHANNELS*MAX_OUTPUT_CHANNELS)
#if ENABLE_ASSERTION
#define XAUDIO2_CHECK_ERROR(method) \
@@ -36,18 +37,12 @@
#else
#define XAUDIO2_CHECK_ERROR(method)
#endif
#define FLAX_COORD_SCALE 0.01f // units are meters
#define FLAX_DST_TO_XAUDIO(x) x * FLAX_COORD_SCALE
#define FLAX_POS_TO_XAUDIO(vec) X3DAUDIO_VECTOR(vec.X * FLAX_COORD_SCALE, vec.Y * FLAX_COORD_SCALE, vec.Z * FLAX_COORD_SCALE)
#define FLAX_VEL_TO_XAUDIO(vec) X3DAUDIO_VECTOR(vec.X * (FLAX_COORD_SCALE*FLAX_COORD_SCALE), vec.Y * (FLAX_COORD_SCALE*FLAX_COORD_SCALE), vec.Z * (FLAX_COORD_SCALE*FLAX_COORD_SCALE))
#define FLAX_VEC_TO_XAUDIO(vec) (*((X3DAUDIO_VECTOR*)&vec))
namespace XAudio2
{
struct Listener
struct Listener : AudioBackendTools::Listener
{
AudioListener* AudioListener;
X3DAUDIO_LISTENER Data;
Listener()
{
@@ -57,7 +52,6 @@ namespace XAudio2
void Init()
{
AudioListener = nullptr;
Data.pCone = nullptr;
}
bool IsFree() const
@@ -67,21 +61,13 @@ namespace XAudio2
void UpdateTransform()
{
const Vector3& position = AudioListener->GetPosition();
const Quaternion& orientation = AudioListener->GetOrientation();
const Vector3 front = orientation * Vector3::Forward;
const Vector3 top = orientation * Vector3::Up;
Data.OrientFront = FLAX_VEC_TO_XAUDIO(front);
Data.OrientTop = FLAX_VEC_TO_XAUDIO(top);
Data.Position = FLAX_POS_TO_XAUDIO(position);
Position = AudioListener->GetPosition();
Orientation = AudioListener->GetOrientation();
}
void UpdateVelocity()
{
const Vector3& velocity = AudioListener->GetVelocity();
Data.Velocity = FLAX_VEL_TO_XAUDIO(velocity);
Velocity = AudioListener->GetVelocity();
}
};
@@ -123,23 +109,18 @@ namespace XAudio2
void PeekSamples();
};
struct Source
struct Source : AudioBackendTools::Source
{
IXAudio2SourceVoice* Voice;
X3DAUDIO_EMITTER Data;
WAVEFORMATEX Format;
XAUDIO2_SEND_DESCRIPTOR Destination;
float Pitch;
float Pan;
float StartTimeForQueueBuffer;
float LastBufferStartTime;
float DopplerFactor;
uint64 LastBufferStartSamplesPlayed;
int32 BuffersProcessed;
int32 Channels;
bool IsDirty;
bool Is3D;
bool IsPlaying;
bool IsForceMono3D;
VoiceCallback Callback;
Source()
@@ -150,8 +131,6 @@ namespace XAudio2
void Init()
{
Voice = nullptr;
Platform::MemoryClear(&Data, sizeof(Data));
Data.CurveDistanceScaler = 1.0f;
Destination.Flags = 0;
Destination.pOutputVoice = nullptr;
Pitch = 1.0f;
@@ -172,21 +151,13 @@ namespace XAudio2
void UpdateTransform(const AudioSource* source)
{
const Vector3& position = source->GetPosition();
const Quaternion& orientation = source->GetOrientation();
const Vector3 front = orientation * Vector3::Forward;
const Vector3 top = orientation * Vector3::Up;
Data.OrientFront = FLAX_VEC_TO_XAUDIO(front);
Data.OrientTop = FLAX_VEC_TO_XAUDIO(top);
Data.Position = FLAX_POS_TO_XAUDIO(position);
Position = source->GetPosition();
Orientation = source->GetOrientation();
}
void UpdateVelocity(const AudioSource* source)
{
const Vector3& velocity = source->GetVelocity();
Data.Velocity = FLAX_VEL_TO_XAUDIO(velocity);
Velocity = source->GetVelocity();
}
};
@@ -214,11 +185,9 @@ namespace XAudio2
IXAudio2* Instance = nullptr;
IXAudio2MasteringVoice* MasteringVoice = nullptr;
X3DAUDIO_HANDLE X3DInstance;
DWORD ChannelMask;
UINT32 SampleRate;
UINT32 Channels;
int32 Channels;
bool ForceDirty = true;
AudioBackendTools::Settings Settings;
Listener Listeners[AUDIO_MAX_LISTENERS];
CriticalSection Locker;
ChunkedArray<Source, 32> Sources;
@@ -387,7 +356,7 @@ void AudioBackendXAudio2::Source_OnAdd(AudioSource* source)
const auto& header = clip->AudioHeader;
auto& format = aSource->Format;
format.wFormatTag = WAVE_FORMAT_PCM;
format.nChannels = clip->Is3D() ? 1 : header.Info.NumChannels; // 3d audio is always mono (AudioClip auto-converts before buffer write)
format.nChannels = clip->Is3D() ? 1 : header.Info.NumChannels; // 3d audio is always mono (AudioClip auto-converts before buffer write if FeatureFlags::SpatialMultiChannel is unset)
format.nSamplesPerSec = header.Info.SampleRate;
format.wBitsPerSample = header.Info.BitDepth;
format.nBlockAlign = (WORD)(format.nChannels * (format.wBitsPerSample / 8));
@@ -408,26 +377,25 @@ void AudioBackendXAudio2::Source_OnAdd(AudioSource* source)
if (FAILED(hr))
return;
sourceID++; // 0 is invalid ID so shift them
source->SourceIDs.Add(sourceID);
// Prepare source state
aSource->Callback.Source = source;
aSource->IsDirty = true;
aSource->Data.ChannelCount = format.nChannels;
aSource->Data.InnerRadius = FLAX_DST_TO_XAUDIO(source->GetMinDistance());
aSource->Is3D = source->Is3D();
aSource->IsForceMono3D = header.Is3D && header.Info.NumChannels > 1;
aSource->Pitch = source->GetPitch();
aSource->Pan = source->GetPan();
aSource->DopplerFactor = source->GetDopplerFactor();
aSource->Volume = source->GetVolume();
aSource->MinDistance = source->GetMinDistance();
aSource->Attenuation = source->GetAttenuation();
aSource->Channels = format.nChannels;
aSource->UpdateTransform(source);
aSource->UpdateVelocity(source);
hr = aSource->Voice->SetVolume(source->GetVolume());
XAUDIO2_CHECK_ERROR(SetVolume);
// 0 is invalid ID so shift them
sourceID++;
source->SourceIDs.Add(sourceID);
source->Restore();
}
@@ -462,6 +430,7 @@ void AudioBackendXAudio2::Source_VolumeChanged(AudioSource* source)
auto aSource = XAudio2::GetSource(source);
if (aSource && aSource->Voice)
{
aSource->Volume = source->GetVolume();
const HRESULT hr = aSource->Voice->SetVolume(source->GetVolume());
XAUDIO2_CHECK_ERROR(SetVolume);
}
@@ -546,17 +515,10 @@ void AudioBackendXAudio2::Source_SpatialSetupChanged(AudioSource* source)
auto aSource = XAudio2::GetSource(source);
if (aSource)
{
// TODO: implement attenuation settings for 3d audio
auto clip = source->Clip.Get();
if (clip && clip->IsLoaded())
{
const auto& header = clip->AudioHeader;
aSource->Data.ChannelCount = source->Is3D() ? 1 : header.Info.NumChannels; // 3d audio is always mono (AudioClip auto-converts before buffer write)
aSource->IsForceMono3D = header.Is3D && header.Info.NumChannels > 1;
}
aSource->Is3D = source->Is3D();
aSource->MinDistance = source->GetMinDistance();
aSource->Attenuation = source->GetAttenuation();
aSource->DopplerFactor = source->GetDopplerFactor();
aSource->Data.InnerRadius = FLAX_DST_TO_XAUDIO(source->GetMinDistance());
aSource->IsDirty = true;
}
}
@@ -655,7 +617,7 @@ float AudioBackendXAudio2::Source_GetCurrentBufferTime(const AudioSource* source
aSource->Voice->GetState(&state);
const uint32 numChannels = clipInfo.NumChannels;
const uint32 totalSamples = clipInfo.NumSamples / numChannels;
const uint32 sampleRate = clipInfo.SampleRate;// / clipInfo.NumChannels;
const uint32 sampleRate = clipInfo.SampleRate; // / clipInfo.NumChannels;
state.SamplesPlayed -= aSource->LastBufferStartSamplesPlayed % totalSamples; // Offset by the last buffer start to get time relative to its begin
time = aSource->LastBufferStartTime + (state.SamplesPlayed % totalSamples) / static_cast<float>(Math::Max(1U, sampleRate));
}
@@ -770,6 +732,8 @@ void AudioBackendXAudio2::Buffer_Delete(uint32 bufferId)
void AudioBackendXAudio2::Buffer_Write(uint32 bufferId, byte* samples, const AudioDataInfo& info)
{
CHECK(info.NumChannels <= MAX_INPUT_CHANNELS);
XAudio2::Locker.Lock();
XAudio2::Buffer* aBuffer = XAudio2::Buffers[bufferId - 1];
XAudio2::Locker.Unlock();
@@ -796,6 +760,7 @@ void AudioBackendXAudio2::Base_OnActiveDeviceChanged()
void AudioBackendXAudio2::Base_SetDopplerFactor(float value)
{
XAudio2::Settings.DopplerFactor = value;
XAudio2::MarkAllDirty();
}
@@ -803,6 +768,7 @@ void AudioBackendXAudio2::Base_SetVolume(float value)
{
if (XAudio2::MasteringVoice)
{
XAudio2::Settings.Volume = 1.0f; // Volume is applied via MasteringVoice
const HRESULT hr = XAudio2::MasteringVoice->SetVolume(value);
XAUDIO2_CHECK_ERROR(SetVolume);
}
@@ -830,7 +796,8 @@ bool AudioBackendXAudio2::Base_Init()
}
XAUDIO2_VOICE_DETAILS details;
XAudio2::MasteringVoice->GetVoiceDetails(&details);
XAudio2::SampleRate = details.InputSampleRate;
#if 0
// TODO: implement multi-channel support (eg. 5.1, 7.1)
XAudio2::Channels = details.InputChannels;
hr = XAudio2::MasteringVoice->GetChannelMask(&XAudio2::ChannelMask);
if (FAILED(hr))
@@ -838,19 +805,10 @@ bool AudioBackendXAudio2::Base_Init()
LOG(Error, "Failed to get XAudio2 mastering voice channel mask. Error: 0x{0:x}", hr);
return true;
}
// Initialize spatial audio subsystem
DWORD dwChannelMask;
XAudio2::MasteringVoice->GetChannelMask(&dwChannelMask);
hr = X3DAudioInitialize(dwChannelMask, X3DAUDIO_SPEED_OF_SOUND, XAudio2::X3DInstance);
if (FAILED(hr))
{
LOG(Error, "Failed to initalize XAudio2 3D support. Error: 0x{0:x}", hr);
return true;
}
// Info
LOG(Info, "XAudio2: {0} channels at {1} kHz (channel mask {2})", XAudio2::Channels, XAudio2::SampleRate / 1000.0f, XAudio2::ChannelMask);
#else
XAudio2::Channels = 2;
#endif
LOG(Info, "XAudio2: {0} channels at {1} kHz", XAudio2::Channels, details.InputSampleRate / 1000.0f);
// Dummy device
devices.Resize(1);
@@ -864,53 +822,19 @@ void AudioBackendXAudio2::Base_Update()
{
// Update dirty voices
const auto listener = XAudio2::GetListener();
const float dopplerFactor = AudioSettings::Get()->DopplerFactor;
float matrixCoefficients[MAX_CHANNELS_MATRIX_SIZE];
X3DAUDIO_DSP_SETTINGS dsp = { 0 };
dsp.DstChannelCount = XAudio2::Channels;
dsp.pMatrixCoefficients = matrixCoefficients;
float outputMatrix[MAX_CHANNELS_MATRIX_SIZE];
for (int32 i = 0; i < XAudio2::Sources.Count(); i++)
{
auto& source = XAudio2::Sources[i];
if (source.IsFree() || !(source.IsDirty || XAudio2::ForceDirty))
continue;
dsp.SrcChannelCount = source.Data.ChannelCount;
if (source.Is3D && listener)
{
// 3D sound
X3DAudioCalculate(XAudio2::X3DInstance, &listener->Data, &source.Data, X3DAUDIO_CALCULATE_MATRIX | X3DAUDIO_CALCULATE_DOPPLER, &dsp);
}
else
{
// 2D sound
dsp.DopplerFactor = 1.0f;
Platform::MemoryClear(dsp.pMatrixCoefficients, sizeof(matrixCoefficients));
dsp.pMatrixCoefficients[0] = 1.0f;
if (source.Format.nChannels == 1)
{
dsp.pMatrixCoefficients[1] = 1.0f;
}
else
{
dsp.pMatrixCoefficients[3] = 1.0f;
}
const float panLeft = Math::Min(1.0f - source.Pan, 1.0f);
const float panRight = Math::Min(1.0f + source.Pan, 1.0f);
if (source.Format.nChannels >= 2)
{
dsp.pMatrixCoefficients[0] *= panLeft;
dsp.pMatrixCoefficients[3] *= panRight;
}
}
if (source.IsForceMono3D)
{
// Hack to fix playback speed for 3D clip that has auto-converted stereo to mono at runtime
dsp.DopplerFactor *= 0.5f;
}
const float frequencyRatio = dopplerFactor * source.Pitch * dsp.DopplerFactor * source.DopplerFactor;
source.Voice->SetFrequencyRatio(frequencyRatio);
source.Voice->SetOutputMatrix(XAudio2::MasteringVoice, dsp.SrcChannelCount, dsp.DstChannelCount, dsp.pMatrixCoefficients);
auto mix = AudioBackendTools::CalculateSoundMix(XAudio2::Settings, *listener, source, XAudio2::Channels);
mix.VolumeIntoChannels();
AudioBackendTools::MapChannels(source.Channels, XAudio2::Channels, mix.Channels, outputMatrix);
source.Voice->SetFrequencyRatio(mix.Pitch);
source.Voice->SetOutputMatrix(XAudio2::MasteringVoice, source.Channels, XAudio2::Channels, outputMatrix);
source.IsDirty = false;
}

View File

@@ -584,7 +584,43 @@ Asset::LoadResult BinaryAsset::loadAsset()
ASSERT(Storage && _header.ID.IsValid() && _header.TypeName.HasChars());
auto lock = Storage->Lock();
return load();
auto chunksToPreload = getChunksToPreload();
if (chunksToPreload != 0)
{
// Ensure that any chunks that were requested before are loaded in memory (in case streaming flushed them out after timeout)
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
const auto chunk = _header.Chunks[i];
if (GET_CHUNK_FLAG(i) & chunksToPreload && chunk && chunk->IsMissing())
Storage->LoadAssetChunk(chunk);
}
}
const LoadResult result = load();
#if !BUILD_RELEASE
if (result == LoadResult::MissingDataChunk)
{
// Provide more insights on potentially missing asset data chunk
Char chunksBitMask[ASSET_FILE_DATA_CHUNKS + 1];
Char chunksExistBitMask[ASSET_FILE_DATA_CHUNKS + 1];
Char chunksLoadBitMask[ASSET_FILE_DATA_CHUNKS + 1];
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
if (const FlaxChunk* chunk = _header.Chunks[i])
{
chunksBitMask[i] = '1';
chunksExistBitMask[i] = chunk->ExistsInFile() ? '1' : '0';
chunksLoadBitMask[i] = chunk->IsLoaded() ? '1' : '0';
}
else
{
chunksBitMask[i] = chunksExistBitMask[i] = chunksLoadBitMask[i] = '0';
}
}
chunksBitMask[ASSET_FILE_DATA_CHUNKS] = chunksExistBitMask[ASSET_FILE_DATA_CHUNKS] = chunksLoadBitMask[ASSET_FILE_DATA_CHUNKS] = 0;
LOG(Warning, "Asset reports missing data chunk. Chunks bitmask: {}, existing chunks: {} loaded chunks: {}. '{}'", chunksBitMask, chunksExistBitMask, chunksLoadBitMask, ToString());
}
#endif
return result;
}
void BinaryAsset::releaseStorage()

View File

@@ -4,6 +4,8 @@
#include "Engine/Core/Log.h"
#include "Engine/Core/DeleteMe.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Types/Stopwatch.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Serialization/FileWriteStream.h"
#include "Engine/Serialization/FileReadStream.h"
@@ -19,7 +21,7 @@ void AssetsCache::Init()
{
Entry e;
int32 count;
const DateTime loadStartTime = DateTime::Now();
Stopwatch stopwatch;
#if USE_EDITOR
_path = Globals::ProjectCacheFolder / TEXT("AssetsCache.dat");
#else
@@ -138,8 +140,8 @@ void AssetsCache::Init()
}
}
const int32 loadTimeInMs = static_cast<int32>((DateTime::Now() - loadStartTime).GetTotalMilliseconds());
LOG(Info, "Asset Cache loaded {0} entries in {1} ms ({2} rejected)", _registry.Count(), loadTimeInMs, rejectedCount);
stopwatch.Stop();
LOG(Info, "Asset Cache loaded {0} entries in {1}ms ({2} rejected)", _registry.Count(), stopwatch.GetMilliseconds(), rejectedCount);
}
bool AssetsCache::Save()

View File

@@ -163,7 +163,7 @@ bool ContentLoadingManagerService::Init()
// Calculate amount of loading threads to use
const CPUInfo cpuInfo = Platform::GetCPUInfo();
const int32 count = Math::Clamp(static_cast<int32>(LOADING_THREAD_PER_LOGICAL_CORE * (float)cpuInfo.LogicalProcessorCount), 1, 12);
const int32 count = Math::Clamp(Math::CeilToInt(LOADING_THREAD_PER_LOGICAL_CORE * (float)cpuInfo.LogicalProcessorCount), 1, 12);
LOG(Info, "Creating {0} content loading threads...", count);
// Create loading threads

View File

@@ -6,6 +6,7 @@
#include "Engine/Core/Types/Pair.h"
#include "Engine/Core/Types/String.h"
#if USE_EDITOR
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Collections/Array.h"
#endif
#include "FlaxChunk.h"
@@ -81,36 +82,17 @@ public:
/// <summary>
/// Gets the amount of created asset chunks.
/// </summary>
/// <returns>Created asset chunks</returns>
int32 GetChunksCount() const
{
int32 result = 0;
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
if (Chunks[i] != nullptr)
result++;
}
return result;
}
int32 GetChunksCount() const;
/// <summary>
/// Deletes all chunks. Warning! Chunks are managed internally, use with caution!
/// </summary>
void DeleteChunks()
{
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
SAFE_DELETE(Chunks[i]);
}
}
void DeleteChunks();
/// <summary>
/// Unlinks all chunks.
/// </summary>
void UnlinkChunks()
{
Platform::MemoryClear(Chunks, sizeof(Chunks));
}
void UnlinkChunks();
/// <summary>
/// Gets string with a human-readable info about that header

View File

@@ -78,9 +78,9 @@ public:
FlaxChunkFlags Flags = FlaxChunkFlags::None;
/// <summary>
/// The last usage time (atomic, ticks of DateTime in UTC).
/// The last usage time.
/// </summary>
int64 LastAccessTime = 0;
double LastAccessTime = 0.0;
/// <summary>
/// The chunk data.

View File

@@ -20,6 +20,30 @@
#endif
#include <ThirdParty/LZ4/lz4.h>
int32 AssetHeader::GetChunksCount() const
{
int32 result = 0;
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
if (Chunks[i] != nullptr)
result++;
}
return result;
}
void AssetHeader::DeleteChunks()
{
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
SAFE_DELETE(Chunks[i]);
}
}
void AssetHeader::UnlinkChunks()
{
Platform::MemoryClear(Chunks, sizeof(Chunks));
}
String AssetHeader::ToString() const
{
return String::Format(TEXT("ID: {0}, TypeName: {1}, Chunks Count: {2}"), ID, TypeName, GetChunksCount());
@@ -27,7 +51,7 @@ String AssetHeader::ToString() const
void FlaxChunk::RegisterUsage()
{
Platform::AtomicStore(&LastAccessTime, DateTime::NowUTC().Ticks);
LastAccessTime = Platform::GetTimeSeconds();
}
const int32 FlaxStorage::MagicCode = 1180124739;
@@ -235,7 +259,9 @@ uint32 FlaxStorage::GetRefCount() const
bool FlaxStorage::ShouldDispose() const
{
return Platform::AtomicRead((int64*)&_refCount) == 0 && Platform::AtomicRead((int64*)&_chunksLock) == 0 && DateTime::NowUTC() - _lastRefLostTime >= TimeSpan::FromMilliseconds(500);
return Platform::AtomicRead((int64*)&_refCount) == 0 &&
Platform::AtomicRead((int64*)&_chunksLock) == 0 &&
Platform::GetTimeSeconds() - _lastRefLostTime >= 0.5; // TTL in seconds
}
uint32 FlaxStorage::GetMemoryUsage() const
@@ -1374,12 +1400,13 @@ void FlaxStorage::Tick()
if (Platform::AtomicRead(&_chunksLock) != 0)
return;
const auto now = DateTime::NowUTC();
const double now = Platform::GetTimeSeconds();
bool wasAnyUsed = false;
const float unusedDataChunksLifetime = ContentStorageManager::UnusedDataChunksLifetime.GetTotalSeconds();
for (int32 i = 0; i < _chunks.Count(); i++)
{
auto chunk = _chunks[i];
const bool wasUsed = (now - DateTime(Platform::AtomicRead(&chunk->LastAccessTime))) < ContentStorageManager::UnusedDataChunksLifetime;
auto chunk = _chunks.Get()[i];
const bool wasUsed = (now - chunk->LastAccessTime) < unusedDataChunksLifetime;
if (!wasUsed && chunk->IsLoaded())
{
chunk->Unload();

View File

@@ -5,7 +5,6 @@
#include "Engine/Core/Object.h"
#include "Engine/Core/Delegate.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Platform/CriticalSection.h"
#include "Engine/Serialization/FileReadStream.h"
@@ -90,7 +89,7 @@ protected:
// State
int64 _refCount;
int64 _chunksLock;
DateTime _lastRefLostTime;
double _lastRefLostTime;
CriticalSection _loadLocker;
// Storage
@@ -115,7 +114,7 @@ private:
Platform::InterlockedDecrement(&_refCount);
if (Platform::AtomicRead(&_refCount) == 0)
{
_lastRefLostTime = DateTime::NowUTC();
_lastRefLostTime = Platform::GetTimeSeconds();
}
}

View File

@@ -3,7 +3,6 @@
#pragma once
#include "Engine/Core/Config/Settings.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Content/Asset.h"
#include "Engine/Content/AssetReference.h"
@@ -15,8 +14,15 @@
API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API BuildSettings : public SettingsBase
{
DECLARE_SCRIPTING_TYPE_MINIMAL(BuildSettings);
API_AUTO_SERIALIZATION();
public:
/// <summary>
/// Name of the output app created by the build system. Used to rename main executable (eg. MyGame.exe) or final package name (eg. MyGame.apk). Custom tokens: ${PROJECT_NAME}, ${COMPANY_NAME}.
/// </summary>
API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"General\")")
String OutputName = TEXT("${PROJECT_NAME}");
/// <summary>
/// The maximum amount of assets to include into a single assets package. Asset packages will split into several packages if need to.
/// </summary>
@@ -100,21 +106,4 @@ public:
/// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use.
/// </summary>
static BuildSettings* Get();
// [SettingsBase]
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override
{
DESERIALIZE(MaxAssetsPerPackage);
DESERIALIZE(MaxPackageSizeMB);
DESERIALIZE(ContentKey);
DESERIALIZE(ForDistribution);
DESERIALIZE(SkipPackaging);
DESERIALIZE(AdditionalAssets);
DESERIALIZE(AdditionalAssetFolders);
DESERIALIZE(ShadersNoOptimize);
DESERIALIZE(ShadersGenerateDebugData);
DESERIALIZE(SkipDefaultFonts);
DESERIALIZE(SkipDotnetPackaging);
DESERIALIZE(SkipUnusedDotnetLibsPackaging);
}
};

View File

@@ -22,6 +22,7 @@
#include "Engine/Engine/Globals.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Streaming/StreamingSettings.h"
#include "Engine/Serialization/Serialization.h"
#if FLAX_TESTS || USE_EDITOR
#include "Engine/Platform/FileSystem.h"
#endif

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