Merge remote-tracking branch 'origin/master' into 1.8
# Conflicts: # Source/Editor/Utilities/EditorUtilities.cpp # Source/Editor/Utilities/EditorUtilities.h
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,7 +11,6 @@ Source/*.Gen.*
|
||||
Source/*.csproj
|
||||
/Package_*/
|
||||
!Source/Engine/Debug
|
||||
/Source/Platforms/Editor/Linux/Mono/etc/mono/registry
|
||||
PackageEditor_Cert.command
|
||||
PackageEditor_Cert.bat
|
||||
PackagePlatforms_Cert.bat
|
||||
|
||||
BIN
Content/Editor/Fonts/NotoSansSC-Regular.flax
(Stored with Git LFS)
Normal file
BIN
Content/Editor/Fonts/NotoSansSC-Regular.flax
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Content/Shaders/ColorGrading.flax
(Stored with Git LFS)
BIN
Content/Shaders/ColorGrading.flax
(Stored with Git LFS)
Binary file not shown.
@@ -73,8 +73,12 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=TYPEDEF/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION_005FMEMBER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LO/@EntryIndexedValue">LO</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RPC/@EntryIndexedValue">RPC</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SDK/@EntryIndexedValue">SDK</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=VS/@EntryIndexedValue">VS</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue"><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FCONSTANT/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FFUNCTION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace FlaxEditor.Content.Create
|
||||
switch (_options.Template)
|
||||
{
|
||||
case Templates.Empty:
|
||||
return Editor.CreateAsset(Editor.NewAssetType.ParticleEmitter, ResultUrl);
|
||||
return Editor.CreateAsset("ParticleEmitter", ResultUrl);
|
||||
case Templates.ConstantBurst:
|
||||
templateName = "Constant Burst";
|
||||
break;
|
||||
|
||||
@@ -56,6 +56,9 @@ namespace FlaxEditor.Content.GUI
|
||||
|
||||
private float _viewScale = 1.0f;
|
||||
private ContentViewType _viewType = ContentViewType.Tiles;
|
||||
private bool _isRubberBandSpanning = false;
|
||||
private Float2 _mousePresslocation;
|
||||
private Rectangle _rubberBandRectangle;
|
||||
|
||||
#region External Events
|
||||
|
||||
@@ -600,6 +603,12 @@ namespace FlaxEditor.Content.GUI
|
||||
{
|
||||
Render2D.DrawText(style.FontSmall, IsSearching ? "No results" : "Empty", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
|
||||
}
|
||||
|
||||
if (_isRubberBandSpanning)
|
||||
{
|
||||
Render2D.FillRectangle(_rubberBandRectangle, Color.Orange * 0.4f);
|
||||
Render2D.DrawRectangle(_rubberBandRectangle, Color.Orange);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -607,9 +616,54 @@ namespace FlaxEditor.Content.GUI
|
||||
{
|
||||
if (base.OnMouseDown(location, button))
|
||||
return true;
|
||||
|
||||
if (button == MouseButton.Left)
|
||||
{
|
||||
_mousePresslocation = location;
|
||||
_rubberBandRectangle = new Rectangle(_mousePresslocation, 0, 0);
|
||||
_isRubberBandSpanning = true;
|
||||
StartMouseCapture();
|
||||
}
|
||||
return AutoFocus && Focus(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseMove(Float2 location)
|
||||
{
|
||||
if (_isRubberBandSpanning)
|
||||
{
|
||||
_rubberBandRectangle.Width = location.X - _mousePresslocation.X;
|
||||
_rubberBandRectangle.Height = location.Y - _mousePresslocation.Y;
|
||||
}
|
||||
|
||||
base.OnMouseMove(location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (_isRubberBandSpanning)
|
||||
{
|
||||
_isRubberBandSpanning = false;
|
||||
EndMouseCapture();
|
||||
if (_rubberBandRectangle.Width < 0 || _rubberBandRectangle.Height < 0)
|
||||
{
|
||||
// make sure we have a well-formed rectangle i.e. size is positive and X/Y is upper left corner
|
||||
var size = _rubberBandRectangle.Size;
|
||||
_rubberBandRectangle.X = Mathf.Min(_rubberBandRectangle.X, _rubberBandRectangle.X + _rubberBandRectangle.Width);
|
||||
_rubberBandRectangle.Y = Mathf.Min(_rubberBandRectangle.Y, _rubberBandRectangle.Y + _rubberBandRectangle.Height);
|
||||
size.X = Mathf.Abs(size.X);
|
||||
size.Y = Mathf.Abs(size.Y);
|
||||
_rubberBandRectangle.Size = size;
|
||||
}
|
||||
var itemsInRectangle = _items.Where(t => _rubberBandRectangle.Intersects(t.Bounds)).ToList();
|
||||
Select(itemsInRectangle, Input.GetKey(KeyboardKeys.Shift) || Input.GetKey(KeyboardKeys.Control));
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseUp(location, button);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseWheel(Float2 location, float delta)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace FlaxEngine.Tools
|
||||
private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab;
|
||||
private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab;
|
||||
private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab;
|
||||
private bool ShowRootMotion => ShowAnimation && RootMotion != RootMotionMode.None;
|
||||
private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals;
|
||||
private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents;
|
||||
private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom;
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.AnimationGraphFunction, outputPath))
|
||||
if (Editor.CreateAsset("AnimationGraphFunction", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.AnimationGraph, outputPath))
|
||||
if (Editor.CreateAsset("AnimationGraph", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.Animation, outputPath))
|
||||
if (Editor.CreateAsset("Animation", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.BehaviorTree, outputPath))
|
||||
if (Editor.CreateAsset("BehaviorTree", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.CollisionData, outputPath))
|
||||
if (Editor.CreateAsset("CollisionData", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.MaterialFunction, outputPath))
|
||||
if (Editor.CreateAsset("MaterialFunction", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.MaterialInstance, outputPath))
|
||||
if (Editor.CreateAsset("MaterialInstance", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.Material, outputPath))
|
||||
if (Editor.CreateAsset("Material", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.ParticleEmitterFunction, outputPath))
|
||||
if (Editor.CreateAsset("ParticleEmitterFunction", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.ParticleSystem, outputPath))
|
||||
if (Editor.CreateAsset("ParticleSystem", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.SceneAnimation, outputPath))
|
||||
if (Editor.CreateAsset("SceneAnimation", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override bool IsFileNameValid(string filename)
|
||||
{
|
||||
// Scripts cannot start with digit.
|
||||
if (Char.IsDigit(filename[0]))
|
||||
if (char.IsDigit(filename[0]))
|
||||
return false;
|
||||
if (filename.Equals("Script"))
|
||||
return false;
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
|
||||
/// <inheritdoc />
|
||||
public override void Create(string outputPath, object arg)
|
||||
{
|
||||
if (Editor.CreateAsset(Editor.NewAssetType.SkeletonMask, outputPath))
|
||||
if (Editor.CreateAsset("SkeletonMask", outputPath))
|
||||
throw new Exception("Failed to create new asset.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,463 @@
|
||||
#include "WindowsPlatformTools.h"
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
#include "Engine/Platform/Windows/WindowsPlatformSettings.h"
|
||||
#include "Engine/Core/Math/Color32.h"
|
||||
#include "Engine/Core/Config/GameSettings.h"
|
||||
#include "Editor/Utilities/EditorUtilities.h"
|
||||
#include "Engine/Graphics/Textures/TextureData.h"
|
||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Content/JsonAsset.h"
|
||||
#include <fstream>
|
||||
|
||||
#define MSDOS_SIGNATURE 0x5A4D
|
||||
#define PE_SIGNATURE 0x00004550
|
||||
#define PE_32BIT_SIGNATURE 0x10B
|
||||
#define PE_64BIT_SIGNATURE 0x20B
|
||||
#define PE_SECTION_UNINITIALIZED_DATA 0x00000080
|
||||
#define PE_IMAGE_DIRECTORY_ENTRY_RESOURCE 2
|
||||
#define PE_IMAGE_RT_ICON 3
|
||||
|
||||
/// <summary>
|
||||
/// MS-DOS header found at the beginning in a PE format file.
|
||||
/// </summary>
|
||||
struct MSDOSHeader
|
||||
{
|
||||
uint16 signature;
|
||||
uint16 lastSize;
|
||||
uint16 numBlocks;
|
||||
uint16 numReloc;
|
||||
uint16 hdrSize;
|
||||
uint16 minAlloc;
|
||||
uint16 maxAlloc;
|
||||
uint16 ss;
|
||||
uint16 sp;
|
||||
uint16 checksum;
|
||||
uint16 ip;
|
||||
uint16 cs;
|
||||
uint16 relocPos;
|
||||
uint16 numOverlay;
|
||||
uint16 reserved1[4];
|
||||
uint16 oemId;
|
||||
uint16 oemInfo;
|
||||
uint16 reserved2[10];
|
||||
uint32 lfanew;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// COFF header found in a PE format file.
|
||||
/// </summary>
|
||||
struct COFFHeader
|
||||
{
|
||||
uint16 machine;
|
||||
uint16 numSections;
|
||||
uint32 timeDateStamp;
|
||||
uint32 ptrSymbolTable;
|
||||
uint32 numSymbols;
|
||||
uint16 sizeOptHeader;
|
||||
uint16 characteristics;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Contains address and size of data areas in a PE image.
|
||||
/// </summary>
|
||||
struct PEDataDirectory
|
||||
{
|
||||
uint32 virtualAddress;
|
||||
uint32 size;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Optional header in a 32-bit PE format file.
|
||||
/// </summary>
|
||||
struct PEOptionalHeader32
|
||||
{
|
||||
uint16 signature;
|
||||
uint8 majorLinkerVersion;
|
||||
uint8 minorLinkerVersion;
|
||||
uint32 sizeCode;
|
||||
uint32 sizeInitializedData;
|
||||
uint32 sizeUninitializedData;
|
||||
uint32 addressEntryPoint;
|
||||
uint32 baseCode;
|
||||
uint32 baseData;
|
||||
uint32 baseImage;
|
||||
uint32 alignmentSection;
|
||||
uint32 alignmentFile;
|
||||
uint16 majorOSVersion;
|
||||
uint16 minorOSVersion;
|
||||
uint16 majorImageVersion;
|
||||
uint16 minorImageVersion;
|
||||
uint16 majorSubsystemVersion;
|
||||
uint16 minorSubsystemVersion;
|
||||
uint32 reserved;
|
||||
uint32 sizeImage;
|
||||
uint32 sizeHeaders;
|
||||
uint32 checksum;
|
||||
uint16 subsystem;
|
||||
uint16 characteristics;
|
||||
uint32 sizeStackReserve;
|
||||
uint32 sizeStackCommit;
|
||||
uint32 sizeHeapReserve;
|
||||
uint32 sizeHeapCommit;
|
||||
uint32 loaderFlags;
|
||||
uint32 NumRvaAndSizes;
|
||||
PEDataDirectory dataDirectory[16];
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Optional header in a 64-bit PE format file.
|
||||
/// </summary>
|
||||
struct PEOptionalHeader64
|
||||
{
|
||||
uint16 signature;
|
||||
uint8 majorLinkerVersion;
|
||||
uint8 minorLinkerVersion;
|
||||
uint32 sizeCode;
|
||||
uint32 sizeInitializedData;
|
||||
uint32 sizeUninitializedData;
|
||||
uint32 addressEntryPoint;
|
||||
uint32 baseCode;
|
||||
uint64 baseImage;
|
||||
uint32 alignmentSection;
|
||||
uint32 alignmentFile;
|
||||
uint16 majorOSVersion;
|
||||
uint16 minorOSVersion;
|
||||
uint16 majorImageVersion;
|
||||
uint16 minorImageVersion;
|
||||
uint16 majorSubsystemVersion;
|
||||
uint16 minorSubsystemVersion;
|
||||
uint32 reserved;
|
||||
uint32 sizeImage;
|
||||
uint32 sizeHeaders;
|
||||
uint32 checksum;
|
||||
uint16 subsystem;
|
||||
uint16 characteristics;
|
||||
uint64 sizeStackReserve;
|
||||
uint64 sizeStackCommit;
|
||||
uint64 sizeHeapReserve;
|
||||
uint64 sizeHeapCommit;
|
||||
uint32 loaderFlags;
|
||||
uint32 NumRvaAndSizes;
|
||||
PEDataDirectory dataDirectory[16];
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A section header in a PE format file.
|
||||
/// </summary>
|
||||
struct PESectionHeader
|
||||
{
|
||||
char name[8];
|
||||
uint32 virtualSize;
|
||||
uint32 relativeVirtualAddress;
|
||||
uint32 physicalSize;
|
||||
uint32 physicalAddress;
|
||||
uint8 deprecated[12];
|
||||
uint32 flags;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A resource table header within a .rsrc section in a PE format file.
|
||||
/// </summary>
|
||||
struct PEImageResourceDirectory
|
||||
{
|
||||
uint32 flags;
|
||||
uint32 timeDateStamp;
|
||||
uint16 majorVersion;
|
||||
uint16 minorVersion;
|
||||
uint16 numNamedEntries;
|
||||
uint16 numIdEntries;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A single entry in a resource table within a .rsrc section in a PE format file.
|
||||
/// </summary>
|
||||
struct PEImageResourceEntry
|
||||
{
|
||||
uint32 type;
|
||||
uint32 offsetDirectory : 31;
|
||||
uint32 isDirectory : 1;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// An entry in a resource table referencing resource data. Found within a .rsrc section in a PE format file.
|
||||
/// </summary>
|
||||
struct PEImageResourceEntryData
|
||||
{
|
||||
uint32 offsetData;
|
||||
uint32 size;
|
||||
uint32 codePage;
|
||||
uint32 resourceHandle;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Header used in icon file format.
|
||||
/// </summary>
|
||||
struct IconHeader
|
||||
{
|
||||
uint32 size;
|
||||
int32 width;
|
||||
int32 height;
|
||||
uint16 planes;
|
||||
uint16 bitCount;
|
||||
uint32 compression;
|
||||
uint32 sizeImage;
|
||||
int32 xPelsPerMeter;
|
||||
int32 yPelsPerMeter;
|
||||
uint32 clrUsed;
|
||||
uint32 clrImportant;
|
||||
};
|
||||
|
||||
void UpdateIconData(uint8* iconData, const TextureData* icon)
|
||||
{
|
||||
IconHeader* iconHeader = (IconHeader*)iconData;
|
||||
if (iconHeader->size != sizeof(IconHeader) || iconHeader->compression != 0 || iconHeader->planes != 1 || iconHeader->bitCount != 32)
|
||||
{
|
||||
// Unsupported format
|
||||
return;
|
||||
}
|
||||
uint8* iconPixels = iconData + sizeof(IconHeader);
|
||||
const uint32 width = iconHeader->width;
|
||||
const uint32 height = iconHeader->height / 2;
|
||||
|
||||
// Try to pick a proper mip (require the same size)
|
||||
int32 srcPixelsMip = 0;
|
||||
const int32 mipLevels = icon->GetMipLevels();
|
||||
for (int32 mipIndex = 0; mipIndex < mipLevels; mipIndex++)
|
||||
{
|
||||
const uint32 iconWidth = Math::Max(1, icon->Width >> mipIndex);
|
||||
const uint32 iconHeight = Math::Max(1, icon->Height >> mipIndex);
|
||||
if (width == iconWidth && height == iconHeight)
|
||||
{
|
||||
srcPixelsMip = mipIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const TextureMipData* srcPixels = icon->GetData(0, srcPixelsMip);
|
||||
const Color32* srcPixelsData = (Color32*)srcPixels->Data.Get();
|
||||
const Int2 srcPixelsSize(Math::Max(1, icon->Width >> srcPixelsMip), Math::Max(1, icon->Height >> srcPixelsMip));
|
||||
const auto sampler = TextureTool::GetSampler(icon->Format);
|
||||
ASSERT_LOW_LAYER(sampler);
|
||||
|
||||
// Write colors
|
||||
uint32* colorData = (uint32*)iconPixels;
|
||||
uint32 idx = 0;
|
||||
for (int32 y = (int32)height - 1; y >= 0; y--)
|
||||
{
|
||||
float v = (float)y / height;
|
||||
for (uint32 x = 0; x < width; x++)
|
||||
{
|
||||
float u = (float)x / width;
|
||||
const Color c = TextureTool::SampleLinear(sampler, Float2(u, v), srcPixelsData, srcPixelsSize, srcPixels->RowPitch);
|
||||
colorData[idx++] = Color32(c).GetAsBGRA();
|
||||
}
|
||||
}
|
||||
|
||||
// Write AND mask
|
||||
uint32 colorDataSize = width * height * sizeof(uint32);
|
||||
uint8* maskData = iconPixels + colorDataSize;
|
||||
uint32 numPackedPixels = width / 8; // One per bit in byte
|
||||
for (int32 y = (int32)height - 1; y >= 0; y--)
|
||||
{
|
||||
uint8 mask = 0;
|
||||
float v = (float)y / height;
|
||||
for (uint32 packedX = 0; packedX < numPackedPixels; packedX++)
|
||||
{
|
||||
for (uint32 pixelIdx = 0; pixelIdx < 8; pixelIdx++)
|
||||
{
|
||||
uint32 x = packedX * 8 + pixelIdx;
|
||||
float u = (float)x / width;
|
||||
const Color c = TextureTool::SampleLinear(sampler, Float2(u, v), srcPixelsData, srcPixelsSize, srcPixels->RowPitch);
|
||||
if (c.A < 0.25f)
|
||||
mask |= 1 << (7 - pixelIdx);
|
||||
}
|
||||
*maskData = mask;
|
||||
maskData++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetIconData(PEImageResourceDirectory* base, PEImageResourceDirectory* current, uint8* imageData, uint32 sectionAddress, const TextureData* iconRGBA8)
|
||||
{
|
||||
uint32 numEntries = current->numIdEntries; // Not supporting name entries
|
||||
PEImageResourceEntry* entries = (PEImageResourceEntry*)(current + 1);
|
||||
for (uint32 i = 0; i < numEntries; i++)
|
||||
{
|
||||
// Only at root does the type identify resource type
|
||||
if (base == current && entries[i].type != PE_IMAGE_RT_ICON)
|
||||
continue;
|
||||
|
||||
if (entries[i].isDirectory)
|
||||
{
|
||||
PEImageResourceDirectory* child = (PEImageResourceDirectory*)(((uint8*)base) + entries[i].offsetDirectory);
|
||||
SetIconData(base, child, imageData, sectionAddress, iconRGBA8);
|
||||
}
|
||||
else
|
||||
{
|
||||
PEImageResourceEntryData* data = (PEImageResourceEntryData*)(((uint8*)base) + entries[i].offsetDirectory);
|
||||
uint8* iconData = imageData + (data->offsetData - sectionAddress);
|
||||
UpdateIconData(iconData, iconRGBA8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateExeIcon(const String& path, const TextureData& icon)
|
||||
{
|
||||
if (!FileSystem::FileExists(path))
|
||||
{
|
||||
LOG(Warning, "Missing file");
|
||||
return true;
|
||||
}
|
||||
if (icon.Width < 1 || icon.Height < 1 || icon.GetMipLevels() <= 0)
|
||||
{
|
||||
LOG(Warning, "Inalid icon data");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure that image format can be sampled
|
||||
const TextureData* iconRGBA8 = &icon;
|
||||
TextureData tmpData1;
|
||||
//if (icon.Format != PixelFormat::R8G8B8A8_UNorm)
|
||||
if (TextureTool::GetSampler(icon.Format) == nullptr)
|
||||
{
|
||||
if (TextureTool::Convert(tmpData1, *iconRGBA8, PixelFormat::R8G8B8A8_UNorm))
|
||||
{
|
||||
LOG(Warning, "Failed convert icon data.");
|
||||
return true;
|
||||
}
|
||||
iconRGBA8 = &tmpData1;
|
||||
}
|
||||
|
||||
// Use fixed-size input icon image
|
||||
TextureData tmpData2;
|
||||
if (iconRGBA8->Width != 256 || iconRGBA8->Height != 256)
|
||||
{
|
||||
if (TextureTool::Resize(tmpData2, *iconRGBA8, 256, 256))
|
||||
{
|
||||
LOG(Warning, "Failed resize icon data.");
|
||||
return true;
|
||||
}
|
||||
iconRGBA8 = &tmpData2;
|
||||
}
|
||||
|
||||
// A PE file is structured as such:
|
||||
// - MSDOS Header
|
||||
// - PE Signature
|
||||
// - COFF Header
|
||||
// - PE Optional Header
|
||||
// - One or multiple sections
|
||||
// - .code
|
||||
// - .data
|
||||
// - ...
|
||||
// - .rsrc
|
||||
// - icon/cursor/etc data
|
||||
|
||||
std::fstream stream;
|
||||
#if PLATFORM_WINDOWS
|
||||
stream.open(path.Get(), std::ios::in | std::ios::out | std::ios::binary);
|
||||
#else
|
||||
StringAsANSI<> pathAnsi(path.Get());
|
||||
stream.open(pathAnsi.Get(), std::ios::in | std::ios::out | std::ios::binary);
|
||||
#endif
|
||||
if (!stream.is_open())
|
||||
{
|
||||
LOG(Warning, "Cannot open file");
|
||||
return true;
|
||||
}
|
||||
|
||||
// First check magic number to ensure file is even an executable
|
||||
uint16 magicNum;
|
||||
stream.read((char*)&magicNum, sizeof(magicNum));
|
||||
if (magicNum != MSDOS_SIGNATURE)
|
||||
{
|
||||
LOG(Warning, "Provided file is not a valid executable.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read the MSDOS header and skip over it
|
||||
stream.seekg(0);
|
||||
MSDOSHeader msdosHeader;
|
||||
stream.read((char*)&msdosHeader, sizeof(MSDOSHeader));
|
||||
|
||||
// Read PE signature
|
||||
stream.seekg(msdosHeader.lfanew);
|
||||
uint32 peSignature;
|
||||
stream.read((char*)&peSignature, sizeof(peSignature));
|
||||
if (peSignature != PE_SIGNATURE)
|
||||
{
|
||||
LOG(Warning, "Provided file is not in PE format.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read COFF header
|
||||
COFFHeader coffHeader;
|
||||
stream.read((char*)&coffHeader, sizeof(COFFHeader));
|
||||
if (coffHeader.sizeOptHeader == 0)
|
||||
{
|
||||
LOG(Warning, "Provided file is not a valid executable.");
|
||||
return true;
|
||||
}
|
||||
uint32 sectionHeadersCount = coffHeader.numSections;
|
||||
|
||||
// Read optional header
|
||||
auto optionalHeaderPos = stream.tellg();
|
||||
uint16 optionalHeaderSignature;
|
||||
stream.read((char*)&optionalHeaderSignature, sizeof(optionalHeaderSignature));
|
||||
PEDataDirectory* dataDirectory = nullptr;
|
||||
stream.seekg(optionalHeaderPos);
|
||||
if (optionalHeaderSignature == PE_32BIT_SIGNATURE)
|
||||
{
|
||||
PEOptionalHeader32 optionalHeader;
|
||||
stream.read((char*)&optionalHeader, sizeof(optionalHeader));
|
||||
dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
|
||||
}
|
||||
else if (optionalHeaderSignature == PE_64BIT_SIGNATURE)
|
||||
{
|
||||
PEOptionalHeader64 optionalHeader;
|
||||
stream.read((char*)&optionalHeader, sizeof(optionalHeader));
|
||||
dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(Warning, "Unrecognized PE format.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read section headers
|
||||
auto sectionHeaderPos = optionalHeaderPos + (std::ifstream::pos_type)coffHeader.sizeOptHeader;
|
||||
stream.seekg(sectionHeaderPos);
|
||||
Array<PESectionHeader> sectionHeaders;
|
||||
sectionHeaders.Resize(sectionHeadersCount);
|
||||
stream.read((char*)sectionHeaders.Get(), sizeof(PESectionHeader) * sectionHeadersCount);
|
||||
|
||||
// Look for .rsrc section header
|
||||
for (uint32 i = 0; i < sectionHeadersCount; i++)
|
||||
{
|
||||
PESectionHeader& sectionHeader = sectionHeaders[i];
|
||||
if (sectionHeader.flags & PE_SECTION_UNINITIALIZED_DATA)
|
||||
continue;
|
||||
if (strcmp(sectionHeader.name, ".rsrc") == 0)
|
||||
{
|
||||
uint32 imageSize = sectionHeader.physicalSize;
|
||||
Array<uint8> imageData;
|
||||
imageData.Resize(imageSize);
|
||||
|
||||
stream.seekg(sectionHeader.physicalAddress);
|
||||
stream.read((char*)imageData.Get(), imageSize);
|
||||
|
||||
uint32 resourceDirOffset = dataDirectory->virtualAddress - sectionHeader.relativeVirtualAddress;
|
||||
PEImageResourceDirectory* resourceDirectory = (PEImageResourceDirectory*)&imageData.Get()[resourceDirOffset];
|
||||
|
||||
SetIconData(resourceDirectory, resourceDirectory, imageData.Get(), sectionHeader.relativeVirtualAddress, iconRGBA8);
|
||||
stream.seekp(sectionHeader.physicalAddress);
|
||||
stream.write((char*)imageData.Get(), imageSize);
|
||||
}
|
||||
}
|
||||
|
||||
stream.close();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
IMPLEMENT_ENGINE_SETTINGS_GETTER(WindowsPlatformSettings, WindowsPlatform);
|
||||
|
||||
@@ -50,7 +502,7 @@ bool WindowsPlatformTools::OnDeployBinaries(CookingData& data)
|
||||
TextureData iconData;
|
||||
if (!EditorUtilities::GetApplicationImage(platformSettings->OverrideIcon, iconData))
|
||||
{
|
||||
if (EditorUtilities::UpdateExeIcon(files[0], iconData))
|
||||
if (UpdateExeIcon(files[0], iconData))
|
||||
{
|
||||
data.Error(TEXT("Failed to change output executable file icon."));
|
||||
return true;
|
||||
|
||||
@@ -543,10 +543,11 @@ namespace FlaxEditor.CustomEditors
|
||||
try
|
||||
{
|
||||
string text;
|
||||
var value = Values[0];
|
||||
if (ParentEditor is Dedicated.ScriptsEditor)
|
||||
{
|
||||
// Script
|
||||
text = JsonSerializer.Serialize(Values[0]);
|
||||
text = JsonSerializer.Serialize(value);
|
||||
|
||||
// Remove properties that should be ignored when copy/pasting data
|
||||
if (text == null)
|
||||
@@ -576,12 +577,12 @@ namespace FlaxEditor.CustomEditors
|
||||
else if (ScriptType.FlaxObject.IsAssignableFrom(Values.Type))
|
||||
{
|
||||
// Object reference
|
||||
text = JsonSerializer.GetStringID(Values[0] as FlaxEngine.Object);
|
||||
text = JsonSerializer.GetStringID(value as FlaxEngine.Object);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default
|
||||
text = JsonSerializer.Serialize(Values[0]);
|
||||
text = JsonSerializer.Serialize(value, TypeUtils.GetType(Values.Type));
|
||||
}
|
||||
Clipboard.Text = text;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FlaxEditor.Actions;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.GUI;
|
||||
@@ -16,6 +18,27 @@ using Object = FlaxEngine.Object;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
internal class NewScriptItem : ItemsListContextMenu.Item
|
||||
{
|
||||
private string _scriptName;
|
||||
|
||||
public string ScriptName
|
||||
{
|
||||
get => _scriptName;
|
||||
set
|
||||
{
|
||||
_scriptName = value;
|
||||
Name = $"Create script '{value}'";
|
||||
}
|
||||
}
|
||||
|
||||
public NewScriptItem(string scriptName)
|
||||
{
|
||||
ScriptName = scriptName;
|
||||
TooltipText = "Create a new script";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drag and drop scripts area control.
|
||||
/// </summary>
|
||||
@@ -74,7 +97,43 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i]));
|
||||
}
|
||||
cm.ItemClicked += item => AddScript((ScriptType)item.Tag);
|
||||
cm.TextChanged += text =>
|
||||
{
|
||||
if (!IsValidScriptName(text))
|
||||
return;
|
||||
if (!cm.ItemsPanel.Children.Any(x => x.Visible && x is not NewScriptItem))
|
||||
{
|
||||
// If there are no visible items, that means the search failed so we can find the create script button or create one if it's the first time
|
||||
var newScriptItem = (NewScriptItem)cm.ItemsPanel.Children.FirstOrDefault(x => x is NewScriptItem);
|
||||
if (newScriptItem != null)
|
||||
{
|
||||
newScriptItem.Visible = true;
|
||||
newScriptItem.ScriptName = text;
|
||||
}
|
||||
else
|
||||
{
|
||||
cm.AddItem(new NewScriptItem(text));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure to hide the create script button if there
|
||||
var newScriptItem = cm.ItemsPanel.Children.FirstOrDefault(x => x is NewScriptItem);
|
||||
if (newScriptItem != null)
|
||||
newScriptItem.Visible = false;
|
||||
}
|
||||
};
|
||||
cm.ItemClicked += item =>
|
||||
{
|
||||
if (item.Tag is ScriptType script)
|
||||
{
|
||||
AddScript(script);
|
||||
}
|
||||
else if (item is NewScriptItem newScriptItem)
|
||||
{
|
||||
CreateScript(newScriptItem);
|
||||
}
|
||||
};
|
||||
cm.SortItems();
|
||||
cm.Show(this, button.BottomLeft - new Float2((cm.Width - button.Width) / 2, 0));
|
||||
}
|
||||
@@ -113,6 +172,19 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsValidScriptName(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return false;
|
||||
if (text.Contains(' '))
|
||||
return false;
|
||||
if (char.IsDigit(text[0]))
|
||||
return false;
|
||||
if (text.Any(c => !char.IsLetterOrDigit(c) && c != '_'))
|
||||
return false;
|
||||
return Editor.Instance.ContentDatabase.GetProxy("cs").IsFileNameValid(text);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
|
||||
{
|
||||
@@ -163,6 +235,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
if (_dragScripts.HasValidDrag)
|
||||
{
|
||||
result = _dragScripts.Effect;
|
||||
|
||||
AddScripts(_dragScripts.Objects);
|
||||
}
|
||||
else if (_dragAssets.HasValidDrag)
|
||||
@@ -177,7 +250,43 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
return result;
|
||||
}
|
||||
|
||||
private void AddScript(ScriptType item)
|
||||
private void CreateScript(NewScriptItem item)
|
||||
{
|
||||
ScriptsEditor.NewScriptName = item.ScriptName;
|
||||
var paths = Directory.GetFiles(Globals.ProjectSourceFolder, "*.Build.cs");
|
||||
|
||||
string moduleName = null;
|
||||
foreach (var p in paths)
|
||||
{
|
||||
var file = File.ReadAllText(p);
|
||||
if (!file.Contains("GameProjectTarget"))
|
||||
continue; // Skip
|
||||
|
||||
if (file.Contains("Modules.Add(\"Game\")"))
|
||||
{
|
||||
// Assume Game represents the main game module
|
||||
moduleName = "Game";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the path slashes are correct for the OS
|
||||
var correctedPath = Path.GetFullPath(Globals.ProjectSourceFolder);
|
||||
if (string.IsNullOrEmpty(moduleName))
|
||||
{
|
||||
var error = FileSystem.ShowBrowseFolderDialog(Editor.Instance.Windows.MainWindow, correctedPath, "Select a module folder to put the new script in", out moduleName);
|
||||
if (error)
|
||||
return;
|
||||
}
|
||||
var path = Path.Combine(Globals.ProjectSourceFolder, moduleName, item.ScriptName + ".cs");
|
||||
Editor.Instance.ContentDatabase.GetProxy("cs").Create(path, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attach a script to the actor.
|
||||
/// </summary>
|
||||
/// <param name="item">The script.</param>
|
||||
public void AddScript(ScriptType item)
|
||||
{
|
||||
var list = new List<ScriptType>(1) { item };
|
||||
AddScripts(list);
|
||||
@@ -224,16 +333,67 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
|
||||
private void AddScripts(List<ScriptType> items)
|
||||
{
|
||||
var actions = new List<IUndoAction>(4);
|
||||
var actions = new List<IUndoAction>();
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
var scriptType = items[i];
|
||||
RequireScriptAttribute scriptAttribute = null;
|
||||
if (scriptType.HasAttribute(typeof(RequireScriptAttribute), false))
|
||||
{
|
||||
foreach (var e in scriptType.GetAttributes(false))
|
||||
{
|
||||
if (e is not RequireScriptAttribute requireScriptAttribute)
|
||||
continue;
|
||||
scriptAttribute = requireScriptAttribute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// See if script requires a specific actor type
|
||||
RequireActorAttribute actorAttribute = null;
|
||||
if (scriptType.HasAttribute(typeof(RequireActorAttribute), false))
|
||||
{
|
||||
foreach (var e in scriptType.GetAttributes(false))
|
||||
{
|
||||
if (e is not RequireActorAttribute requireActorAttribute)
|
||||
continue;
|
||||
actorAttribute = requireActorAttribute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var actors = ScriptsEditor.ParentEditor.Values;
|
||||
for (int j = 0; j < actors.Count; j++)
|
||||
{
|
||||
var actor = (Actor)actors[j];
|
||||
|
||||
// If required actor exists but is not this actor type then skip adding to actor
|
||||
if (actorAttribute != null)
|
||||
{
|
||||
if (actor.GetType() != actorAttribute.RequiredType && !actor.GetType().IsSubclassOf(actorAttribute.RequiredType))
|
||||
{
|
||||
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` not added to `{actor}` due to script requiring an Actor type of `{actorAttribute.RequiredType}`.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
actions.Add(AddRemoveScript.Add(actor, scriptType));
|
||||
// Check if actor has required scripts and add them if the actor does not.
|
||||
if (scriptAttribute != null)
|
||||
{
|
||||
foreach (var type in scriptAttribute.RequiredTypes)
|
||||
{
|
||||
if (!type.IsSubclassOf(typeof(Script)))
|
||||
{
|
||||
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(type.Name)}` not added to `{actor}` due to the class not being a subclass of Script.");
|
||||
continue;
|
||||
}
|
||||
if (actor.GetScript(type) != null)
|
||||
continue;
|
||||
actions.Add(AddRemoveScript.Add(actor, new ScriptType(type)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +600,11 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<object> UndoObjects => _scripts;
|
||||
|
||||
/// <summary>
|
||||
/// Cached the newly created script name - used to add script after compilation.
|
||||
/// </summary>
|
||||
internal static string NewScriptName;
|
||||
|
||||
private void AddMissingScript(int index, LayoutElementsContainer layout)
|
||||
{
|
||||
var group = layout.Group("Missing script");
|
||||
@@ -548,6 +713,21 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
var dragArea = layout.CustomContainer<DragAreaControl>();
|
||||
dragArea.CustomControl.ScriptsEditor = this;
|
||||
|
||||
// If the initialization is triggered by an editor recompilation, check if it was due to script generation from DragAreaControl
|
||||
if (NewScriptName != null)
|
||||
{
|
||||
var script = Editor.Instance.CodeEditing.Scripts.Get().FirstOrDefault(x => x.Name == NewScriptName);
|
||||
NewScriptName = null;
|
||||
if (script != null)
|
||||
{
|
||||
dragArea.CustomControl.AddScript(script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Editor.LogWarning("Failed to find newly created script.");
|
||||
}
|
||||
}
|
||||
|
||||
// No support for showing scripts from multiple actors that have different set of scripts
|
||||
var scripts = (Script[])Values[0];
|
||||
_scripts.Clear();
|
||||
@@ -586,19 +766,71 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
var scriptType = TypeUtils.GetObjectType(script);
|
||||
var editor = CustomEditorsUtil.CreateEditor(scriptType, false);
|
||||
|
||||
// Check if actor has all the required scripts
|
||||
bool hasAllRequirements = true;
|
||||
if (scriptType.HasAttribute(typeof(RequireScriptAttribute), false))
|
||||
{
|
||||
RequireScriptAttribute scriptAttribute = null;
|
||||
foreach (var e in scriptType.GetAttributes(false))
|
||||
{
|
||||
if (e is not RequireScriptAttribute requireScriptAttribute)
|
||||
continue;
|
||||
scriptAttribute = requireScriptAttribute;
|
||||
}
|
||||
|
||||
if (scriptAttribute != null)
|
||||
{
|
||||
foreach (var type in scriptAttribute.RequiredTypes)
|
||||
{
|
||||
if (!type.IsSubclassOf(typeof(Script)))
|
||||
continue;
|
||||
var requiredScript = script.Actor.GetScript(type);
|
||||
if (requiredScript == null)
|
||||
{
|
||||
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` on `{script.Actor}` is missing a required Script of type `{type}`.");
|
||||
hasAllRequirements = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scriptType.HasAttribute(typeof(RequireActorAttribute), false))
|
||||
{
|
||||
RequireActorAttribute attribute = null;
|
||||
foreach (var e in scriptType.GetAttributes(false))
|
||||
{
|
||||
if (e is not RequireActorAttribute requireActorAttribute)
|
||||
continue;
|
||||
attribute = requireActorAttribute;
|
||||
break;
|
||||
}
|
||||
|
||||
if (attribute != null)
|
||||
{
|
||||
var actor = script.Actor;
|
||||
if (actor.GetType() != attribute.RequiredType && !actor.GetType().IsSubclassOf(attribute.RequiredType))
|
||||
{
|
||||
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` on `{script.Actor}` is missing a required Actor of type `{attribute.RequiredType}`.");
|
||||
hasAllRequirements = false;
|
||||
// Maybe call to remove script here?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create group
|
||||
var title = Utilities.Utils.GetPropertyNameUI(scriptType.Name);
|
||||
var group = layout.Group(title, editor);
|
||||
if (!hasAllRequirements)
|
||||
group.Panel.HeaderTextColor = FlaxEngine.GUI.Style.Current.Statusbar.Failed;
|
||||
if ((Presenter.Features & FeatureFlags.CacheExpandedGroups) != 0)
|
||||
{
|
||||
if (Editor.Instance.ProjectCache.IsCollapsedGroup(title))
|
||||
group.Panel.Close(false);
|
||||
if (Editor.Instance.ProjectCache.IsGroupToggled(title))
|
||||
group.Panel.Close();
|
||||
else
|
||||
group.Panel.Open(false);
|
||||
group.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetCollapsedGroup(panel.HeaderText, panel.IsClosed);
|
||||
group.Panel.Open();
|
||||
group.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetGroupToggle(panel.HeaderText, panel.IsClosed);
|
||||
}
|
||||
else
|
||||
group.Panel.Open(false);
|
||||
group.Panel.Open();
|
||||
|
||||
// Customize
|
||||
group.Panel.TooltipText = Editor.Instance.CodeDocs.GetTooltip(scriptType);
|
||||
|
||||
@@ -51,10 +51,13 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
return;
|
||||
Picker = layout.Custom<AssetPicker>().CustomControl;
|
||||
|
||||
_valueType = Values.Type.Type != typeof(object) || Values[0] == null ? Values.Type : TypeUtils.GetObjectType(Values[0]);
|
||||
var value = Values[0];
|
||||
_valueType = Values.Type.Type != typeof(object) || value == null ? Values.Type : TypeUtils.GetObjectType(value);
|
||||
var assetType = _valueType;
|
||||
if (assetType == typeof(string))
|
||||
assetType = new ScriptType(typeof(Asset));
|
||||
else if (_valueType.Type != null && _valueType.Type.Name == typeof(JsonAssetReference<>).Name)
|
||||
assetType = new ScriptType(_valueType.Type.GenericTypeArguments[0]);
|
||||
|
||||
float height = 48;
|
||||
var attributes = Values.GetAttributes();
|
||||
@@ -102,6 +105,12 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
SetValue(new SceneReference(Picker.Validator.SelectedID));
|
||||
else if (_valueType.Type == typeof(string))
|
||||
SetValue(Picker.Validator.SelectedPath);
|
||||
else if (_valueType.Type.Name == typeof(JsonAssetReference<>).Name)
|
||||
{
|
||||
var value = Values[0];
|
||||
value.GetType().GetField("Asset").SetValue(value, Picker.Validator.SelectedAsset as JsonAsset);
|
||||
SetValue(value);
|
||||
}
|
||||
else
|
||||
SetValue(Picker.Validator.SelectedAsset);
|
||||
}
|
||||
@@ -114,16 +123,19 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
if (!HasDifferentValues)
|
||||
{
|
||||
_isRefreshing = true;
|
||||
if (Values[0] is AssetItem assetItem)
|
||||
var value = Values[0];
|
||||
if (value is AssetItem assetItem)
|
||||
Picker.Validator.SelectedItem = assetItem;
|
||||
else if (Values[0] is Guid guid)
|
||||
else if (value is Guid guid)
|
||||
Picker.Validator.SelectedID = guid;
|
||||
else if (Values[0] is SceneReference sceneAsset)
|
||||
else if (value is SceneReference sceneAsset)
|
||||
Picker.Validator.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID);
|
||||
else if (Values[0] is string path)
|
||||
else if (value is string path)
|
||||
Picker.Validator.SelectedPath = path;
|
||||
else if (value != null && value.GetType().Name == typeof(JsonAssetReference<>).Name)
|
||||
Picker.Validator.SelectedAsset = value.GetType().GetField("Asset").GetValue(value) as JsonAsset;
|
||||
else
|
||||
Picker.Validator.SelectedAsset = Values[0] as Asset;
|
||||
Picker.Validator.SelectedAsset = value as Asset;
|
||||
_isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,8 +176,8 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
|
||||
private IntValueBox _sizeBox;
|
||||
private Color _background;
|
||||
private int _elementsCount;
|
||||
private bool _readOnly;
|
||||
private int _elementsCount, _minCount, _maxCount;
|
||||
private bool _canResize;
|
||||
private bool _canReorderItems;
|
||||
private CollectionAttribute.DisplayType _displayType;
|
||||
|
||||
@@ -209,8 +209,10 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
return;
|
||||
|
||||
var size = Count;
|
||||
_readOnly = false;
|
||||
_canResize = true;
|
||||
_canReorderItems = true;
|
||||
_minCount = 0;
|
||||
_maxCount = 0;
|
||||
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
|
||||
_displayType = CollectionAttribute.DisplayType.Header;
|
||||
NotNullItems = false;
|
||||
@@ -222,7 +224,9 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute);
|
||||
if (collection != null)
|
||||
{
|
||||
_readOnly = collection.ReadOnly;
|
||||
_canResize = !collection.ReadOnly;
|
||||
_minCount = collection.MinCount;
|
||||
_maxCount = collection.MaxCount;
|
||||
_canReorderItems = collection.CanReorderItems;
|
||||
NotNullItems = collection.NotNullItems;
|
||||
if (collection.BackgroundColor.HasValue)
|
||||
@@ -231,6 +235,9 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
spacing = collection.Spacing;
|
||||
_displayType = collection.Display;
|
||||
}
|
||||
if (_maxCount == 0)
|
||||
_maxCount = ushort.MaxValue;
|
||||
_canResize &= _minCount < _maxCount;
|
||||
|
||||
var dragArea = layout.CustomContainer<DragAreaControl>();
|
||||
dragArea.CustomControl.Editor = this;
|
||||
@@ -268,8 +275,8 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top;
|
||||
_sizeBox = new IntValueBox(size)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = ushort.MaxValue,
|
||||
MinValue = _minCount,
|
||||
MaxValue = _maxCount,
|
||||
AnchorPreset = AnchorPresets.TopRight,
|
||||
Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height),
|
||||
Parent = dropPanel,
|
||||
@@ -283,7 +290,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
Parent = dropPanel
|
||||
};
|
||||
|
||||
if (_readOnly || (NotNullItems && size == 0))
|
||||
if (!_canResize || (NotNullItems && size == 0))
|
||||
{
|
||||
_sizeBox.IsReadOnly = true;
|
||||
_sizeBox.Enabled = false;
|
||||
@@ -339,7 +346,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
_elementsCount = size;
|
||||
|
||||
// Add/Remove buttons
|
||||
if (!_readOnly)
|
||||
if (_canResize)
|
||||
{
|
||||
var panel = dragArea.HorizontalPanel();
|
||||
panel.Panel.Size = new Float2(0, 20);
|
||||
@@ -347,25 +354,23 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
|
||||
var removeButton = panel.Button("-", "Remove last item");
|
||||
removeButton.Button.Size = new Float2(16, 16);
|
||||
removeButton.Button.Enabled = size > 0;
|
||||
removeButton.Button.Enabled = size > _minCount;
|
||||
removeButton.Button.AnchorPreset = AnchorPresets.TopRight;
|
||||
removeButton.Button.Clicked += () =>
|
||||
{
|
||||
if (IsSetBlocked)
|
||||
return;
|
||||
|
||||
Resize(Count - 1);
|
||||
};
|
||||
|
||||
var addButton = panel.Button("+", "Add new item");
|
||||
addButton.Button.Size = new Float2(16, 16);
|
||||
addButton.Button.Enabled = !NotNullItems || size > 0;
|
||||
addButton.Button.Enabled = (!NotNullItems || size > 0) && size < _maxCount;
|
||||
addButton.Button.AnchorPreset = AnchorPresets.TopRight;
|
||||
addButton.Button.Clicked += () =>
|
||||
{
|
||||
if (IsSetBlocked)
|
||||
return;
|
||||
|
||||
Resize(Count + 1);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ namespace FlaxEditor.CustomEditors
|
||||
/// </summary>
|
||||
internal bool isRootGroup = true;
|
||||
|
||||
/// <summary>
|
||||
/// Parent container who created this one.
|
||||
/// </summary>
|
||||
internal LayoutElementsContainer _parent;
|
||||
|
||||
/// <summary>
|
||||
/// The children.
|
||||
/// </summary>
|
||||
@@ -40,6 +45,24 @@ namespace FlaxEditor.CustomEditors
|
||||
/// </summary>
|
||||
public abstract ContainerControl ContainerControl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Custom Editors layout presenter.
|
||||
/// </summary>
|
||||
internal CustomEditorPresenter Presenter
|
||||
{
|
||||
get
|
||||
{
|
||||
CustomEditorPresenter result;
|
||||
var container = this;
|
||||
do
|
||||
{
|
||||
result = container as CustomEditorPresenter;
|
||||
container = container._parent;
|
||||
} while (container != null);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds new group element.
|
||||
/// </summary>
|
||||
@@ -81,17 +104,31 @@ namespace FlaxEditor.CustomEditors
|
||||
public GroupElement Group(string title, bool useTransparentHeader = false)
|
||||
{
|
||||
var element = new GroupElement();
|
||||
if (!isRootGroup)
|
||||
var presenter = Presenter;
|
||||
var isSubGroup = !isRootGroup;
|
||||
if (isSubGroup)
|
||||
element.Panel.Close();
|
||||
if (presenter != null && (presenter.Features & FeatureFlags.CacheExpandedGroups) != 0)
|
||||
{
|
||||
element.Panel.Close(false);
|
||||
// Build group identifier (made of path from group titles)
|
||||
var expandPath = title;
|
||||
var container = this;
|
||||
while (container != null && !(container is CustomEditorPresenter))
|
||||
{
|
||||
if (container.ContainerControl is DropPanel dropPanel)
|
||||
expandPath = dropPanel.HeaderText + "/" + expandPath;
|
||||
container = container._parent;
|
||||
}
|
||||
else if (this is CustomEditorPresenter presenter && (presenter.Features & FeatureFlags.CacheExpandedGroups) != 0)
|
||||
{
|
||||
if (Editor.Instance.ProjectCache.IsCollapsedGroup(title))
|
||||
element.Panel.Close(false);
|
||||
element.Panel.IsClosedChanged += OnPanelIsClosedChanged;
|
||||
|
||||
// Caching/restoring expanded groups (non-root groups cache expanded state so invert boolean expression)
|
||||
if (Editor.Instance.ProjectCache.IsGroupToggled(expandPath) ^ isSubGroup)
|
||||
element.Panel.Close();
|
||||
else
|
||||
element.Panel.Open();
|
||||
element.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetGroupToggle(expandPath, panel.IsClosed ^ isSubGroup);
|
||||
}
|
||||
element.isRootGroup = false;
|
||||
element._parent = this;
|
||||
element.Panel.HeaderText = title;
|
||||
if (useTransparentHeader)
|
||||
{
|
||||
@@ -103,11 +140,6 @@ namespace FlaxEditor.CustomEditors
|
||||
return element;
|
||||
}
|
||||
|
||||
private void OnPanelIsClosedChanged(DropPanel panel)
|
||||
{
|
||||
Editor.Instance.ProjectCache.SetCollapsedGroup(panel.HeaderText, panel.IsClosed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds new horizontal panel element.
|
||||
/// </summary>
|
||||
@@ -627,7 +659,6 @@ namespace FlaxEditor.CustomEditors
|
||||
if (style == DisplayStyle.Group)
|
||||
{
|
||||
var group = Group(name, editor, true);
|
||||
group.Panel.Close(false);
|
||||
group.Panel.TooltipText = tooltip;
|
||||
return group.Object(values, editor);
|
||||
}
|
||||
@@ -657,7 +688,6 @@ namespace FlaxEditor.CustomEditors
|
||||
if (style == DisplayStyle.Group)
|
||||
{
|
||||
var group = Group(label.Text, editor, true);
|
||||
group.Panel.Close(false);
|
||||
group.Panel.TooltipText = tooltip;
|
||||
return group.Object(values, editor);
|
||||
}
|
||||
|
||||
@@ -869,7 +869,9 @@ namespace FlaxEditor
|
||||
|
||||
/// <summary>
|
||||
/// New asset types allowed to create.
|
||||
/// [Deprecated in v1.8]
|
||||
/// </summary>
|
||||
[Obsolete("Use CreateAsset with named tag.")]
|
||||
public enum NewAssetType
|
||||
{
|
||||
/// <summary>
|
||||
@@ -1046,12 +1048,59 @@ namespace FlaxEditor
|
||||
|
||||
/// <summary>
|
||||
/// Creates new asset at the target location.
|
||||
/// [Deprecated in v1.8]
|
||||
/// </summary>
|
||||
/// <param name="type">New asset type.</param>
|
||||
/// <param name="outputPath">Output asset path.</param>
|
||||
[Obsolete("Use CreateAsset with named tag.")]
|
||||
public static bool CreateAsset(NewAssetType type, string outputPath)
|
||||
{
|
||||
return Internal_CreateAsset(type, outputPath);
|
||||
// [Deprecated on 18.02.2024, expires on 18.02.2025]
|
||||
string tag;
|
||||
switch (type)
|
||||
{
|
||||
case NewAssetType.Material:
|
||||
tag = "Material";
|
||||
break;
|
||||
case NewAssetType.MaterialInstance:
|
||||
tag = "MaterialInstance";
|
||||
break;
|
||||
case NewAssetType.CollisionData:
|
||||
tag = "CollisionData";
|
||||
break;
|
||||
case NewAssetType.AnimationGraph:
|
||||
tag = "AnimationGraph";
|
||||
break;
|
||||
case NewAssetType.SkeletonMask:
|
||||
tag = "SkeletonMask";
|
||||
break;
|
||||
case NewAssetType.ParticleEmitter:
|
||||
tag = "ParticleEmitter";
|
||||
break;
|
||||
case NewAssetType.ParticleSystem:
|
||||
tag = "ParticleSystem";
|
||||
break;
|
||||
case NewAssetType.SceneAnimation:
|
||||
tag = "SceneAnimation";
|
||||
break;
|
||||
case NewAssetType.MaterialFunction:
|
||||
tag = "MaterialFunction";
|
||||
break;
|
||||
case NewAssetType.ParticleEmitterFunction:
|
||||
tag = "ParticleEmitterFunction";
|
||||
break;
|
||||
case NewAssetType.AnimationGraphFunction:
|
||||
tag = "AnimationGraphFunction";
|
||||
break;
|
||||
case NewAssetType.Animation:
|
||||
tag = "Animation";
|
||||
break;
|
||||
case NewAssetType.BehaviorTree:
|
||||
tag = "BehaviorTree";
|
||||
break;
|
||||
default: return true;
|
||||
}
|
||||
return CreateAsset(tag, outputPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1588,10 +1637,6 @@ namespace FlaxEditor
|
||||
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_CloseSplashScreen", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
|
||||
internal static partial void Internal_CloseSplashScreen();
|
||||
|
||||
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_CreateAsset", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
|
||||
[return: MarshalAs(UnmanagedType.U1)]
|
||||
internal static partial bool Internal_CreateAsset(NewAssetType type, string outputPath);
|
||||
|
||||
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_CreateVisualScript", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
|
||||
[return: MarshalAs(UnmanagedType.U1)]
|
||||
internal static partial bool Internal_CreateVisualScript(string outputPath, string baseTypename);
|
||||
|
||||
@@ -54,6 +54,11 @@ namespace FlaxEditor
|
||||
/// </summary>
|
||||
public static string PrimaryFont = "Editor/Fonts/Roboto-Regular";
|
||||
|
||||
/// <summary>
|
||||
/// The secondary (fallback) font to use for missing characters rendering (CJK - Chinese/Japanese/Korean characters).
|
||||
/// </summary>
|
||||
public static string FallbackFont = "Editor/Fonts/NotoSansSC-Regular";
|
||||
|
||||
/// <summary>
|
||||
/// The Inconsolata Regular font.
|
||||
/// </summary>
|
||||
|
||||
@@ -189,6 +189,11 @@ namespace FlaxEditor.GUI
|
||||
/// </summary>
|
||||
public event Action<Item> ItemClicked;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when search text in this popup menu gets changed.
|
||||
/// </summary>
|
||||
public event Action<string> TextChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The panel control where you should add your items.
|
||||
/// </summary>
|
||||
@@ -263,6 +268,7 @@ namespace FlaxEditor.GUI
|
||||
UnlockChildrenRecursive();
|
||||
PerformLayout(true);
|
||||
_searchBox.Focus();
|
||||
TextChanged?.Invoke(_searchBox.Text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -439,6 +445,7 @@ namespace FlaxEditor.GUI
|
||||
Hide();
|
||||
return true;
|
||||
case KeyboardKeys.ArrowDown:
|
||||
{
|
||||
if (RootWindow.FocusedControl == null)
|
||||
{
|
||||
// Focus search box if nothing is focused
|
||||
@@ -447,7 +454,6 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
|
||||
// Focus the first visible item or then next one
|
||||
{
|
||||
var items = GetVisibleItems();
|
||||
var focusedIndex = items.IndexOf(focusedItem);
|
||||
if (focusedIndex == -1)
|
||||
@@ -459,8 +465,8 @@ namespace FlaxEditor.GUI
|
||||
_scrollPanel.ScrollViewTo(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case KeyboardKeys.ArrowUp:
|
||||
if (focusedItem != null)
|
||||
{
|
||||
|
||||
@@ -43,8 +43,9 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
Depth = -1;
|
||||
|
||||
if (Height < Style.Current.FontMedium.Height)
|
||||
Height = Style.Current.FontMedium.Height + 4;
|
||||
var fontHeight = Style.Current.FontMedium.Height;
|
||||
if (Height < fontHeight)
|
||||
Height = fontHeight + 4;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace FlaxEditor.GUI.Tabs
|
||||
/// </summary>
|
||||
public Tab SelectedTab
|
||||
{
|
||||
get => _selectedIndex < 0 || Children.Count <= _selectedIndex ? null : Children[_selectedIndex + 1] as Tab;
|
||||
get => _selectedIndex < 0 || Children.Count <= (_selectedIndex+1) ? null : Children[_selectedIndex + 1] as Tab;
|
||||
set => SelectedTabIndex = value != null ? Children.IndexOf(value) - 1 : -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,78 +170,6 @@ DEFINE_INTERNAL_CALL(bool) EditorInternal_CloneAssetFile(MString* dstPathObj, MS
|
||||
return Content::CloneAssetFile(dstPath, srcPath, *dstId);
|
||||
}
|
||||
|
||||
enum class NewAssetType
|
||||
{
|
||||
Material = 0,
|
||||
MaterialInstance = 1,
|
||||
CollisionData = 2,
|
||||
AnimationGraph = 3,
|
||||
SkeletonMask = 4,
|
||||
ParticleEmitter = 5,
|
||||
ParticleSystem = 6,
|
||||
SceneAnimation = 7,
|
||||
MaterialFunction = 8,
|
||||
ParticleEmitterFunction = 9,
|
||||
AnimationGraphFunction = 10,
|
||||
Animation = 11,
|
||||
BehaviorTree = 12,
|
||||
};
|
||||
|
||||
DEFINE_INTERNAL_CALL(bool) EditorInternal_CreateAsset(NewAssetType type, MString* outputPathObj)
|
||||
{
|
||||
String tag;
|
||||
switch (type)
|
||||
{
|
||||
case NewAssetType::Material:
|
||||
tag = AssetsImportingManager::CreateMaterialTag;
|
||||
break;
|
||||
case NewAssetType::MaterialInstance:
|
||||
tag = AssetsImportingManager::CreateMaterialInstanceTag;
|
||||
break;
|
||||
case NewAssetType::CollisionData:
|
||||
tag = AssetsImportingManager::CreateCollisionDataTag;
|
||||
break;
|
||||
case NewAssetType::AnimationGraph:
|
||||
tag = AssetsImportingManager::CreateAnimationGraphTag;
|
||||
break;
|
||||
case NewAssetType::SkeletonMask:
|
||||
tag = AssetsImportingManager::CreateSkeletonMaskTag;
|
||||
break;
|
||||
case NewAssetType::ParticleEmitter:
|
||||
tag = AssetsImportingManager::CreateParticleEmitterTag;
|
||||
break;
|
||||
case NewAssetType::ParticleSystem:
|
||||
tag = AssetsImportingManager::CreateParticleSystemTag;
|
||||
break;
|
||||
case NewAssetType::SceneAnimation:
|
||||
tag = AssetsImportingManager::CreateSceneAnimationTag;
|
||||
break;
|
||||
case NewAssetType::MaterialFunction:
|
||||
tag = AssetsImportingManager::CreateMaterialFunctionTag;
|
||||
break;
|
||||
case NewAssetType::ParticleEmitterFunction:
|
||||
tag = AssetsImportingManager::CreateParticleEmitterFunctionTag;
|
||||
break;
|
||||
case NewAssetType::AnimationGraphFunction:
|
||||
tag = AssetsImportingManager::CreateAnimationGraphFunctionTag;
|
||||
break;
|
||||
case NewAssetType::Animation:
|
||||
tag = AssetsImportingManager::CreateAnimationTag;
|
||||
break;
|
||||
case NewAssetType::BehaviorTree:
|
||||
tag = AssetsImportingManager::CreateBehaviorTreeTag;
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
||||
String outputPath;
|
||||
MUtils::ToString(outputPathObj, outputPath);
|
||||
FileSystem::NormalizePath(outputPath);
|
||||
|
||||
return AssetsImportingManager::Create(tag, outputPath);
|
||||
}
|
||||
|
||||
DEFINE_INTERNAL_CALL(bool) EditorInternal_CreateVisualScript(MString* outputPathObj, MString* baseTypenameObj)
|
||||
{
|
||||
String outputPath;
|
||||
@@ -639,8 +567,6 @@ bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String
|
||||
{
|
||||
options.GenerateSDF = graphicsSettings->GenerateSDFOnModelImport;
|
||||
}
|
||||
|
||||
// Get options from model
|
||||
FileSystem::NormalizePath(assetPath);
|
||||
return ImportModel::TryGetImportOptions(assetPath, options);
|
||||
}
|
||||
@@ -652,7 +578,12 @@ bool ManagedEditor::Import(const String& inputPath, const String& outputPath, co
|
||||
|
||||
bool ManagedEditor::TryRestoreImportOptions(AudioTool::Options& options, String assetPath)
|
||||
{
|
||||
// Get options from model
|
||||
FileSystem::NormalizePath(assetPath);
|
||||
return ImportAudio::TryGetImportOptions(assetPath, options);
|
||||
}
|
||||
|
||||
bool ManagedEditor::CreateAsset(const String& tag, String outputPath)
|
||||
{
|
||||
FileSystem::NormalizePath(outputPath);
|
||||
return AssetsImportingManager::Create(tag, outputPath);
|
||||
}
|
||||
|
||||
@@ -210,6 +210,13 @@ public:
|
||||
API_FUNCTION() static bool TryRestoreImportOptions(API_PARAM(Ref) AudioTool::Options& options, String assetPath);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new asset at the target location.
|
||||
/// </summary>
|
||||
/// <param name="tag">New asset type.</param>
|
||||
/// <param name="outputPath">Output asset path.</param>
|
||||
API_FUNCTION() static bool CreateAsset(const String& tag, String outputPath);
|
||||
|
||||
public:
|
||||
API_STRUCT(Internal, NoDefault) struct VisualScriptStackFrame
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace FlaxEditor.Modules
|
||||
private DateTime _lastSaveTime;
|
||||
|
||||
private readonly HashSet<Guid> _expandedActors = new HashSet<Guid>();
|
||||
private readonly HashSet<string> _collapsedGroups = new HashSet<string>();
|
||||
private readonly HashSet<string> _toggledGroups = new HashSet<string>();
|
||||
private readonly Dictionary<string, string> _customData = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
@@ -62,26 +62,26 @@ namespace FlaxEditor.Modules
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether group identified by the given title is collapsed in the UI.
|
||||
/// Determines whether group identified by the given title is collapsed/opened in the UI.
|
||||
/// </summary>
|
||||
/// <param name="title">The group title.</param>
|
||||
/// <returns><c>true</c> if group is collapsed; otherwise, <c>false</c>.</returns>
|
||||
public bool IsCollapsedGroup(string title)
|
||||
/// <returns><c>true</c> if group is toggled; otherwise, <c>false</c>.</returns>
|
||||
public bool IsGroupToggled(string title)
|
||||
{
|
||||
return _collapsedGroups.Contains(title);
|
||||
return _toggledGroups.Contains(title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the group collapsed cached value.
|
||||
/// Sets the group collapsed/opened cached value.
|
||||
/// </summary>
|
||||
/// <param name="title">The group title.</param>
|
||||
/// <param name="isCollapsed">If set to <c>true</c> group will be cached as an collapsed, otherwise false.</param>
|
||||
public void SetCollapsedGroup(string title, bool isCollapsed)
|
||||
/// <param name="isToggled">If set to <c>true</c> group will be cached as a toggled, otherwise false.</param>
|
||||
public void SetGroupToggle(string title, bool isToggled)
|
||||
{
|
||||
if (isCollapsed)
|
||||
_collapsedGroups.Add(title);
|
||||
if (isToggled)
|
||||
_toggledGroups.Add(title);
|
||||
else
|
||||
_collapsedGroups.Remove(title);
|
||||
_toggledGroups.Remove(title);
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace FlaxEditor.Modules
|
||||
_expandedActors.Add(new Guid(bytes16));
|
||||
}
|
||||
|
||||
_collapsedGroups.Clear();
|
||||
_toggledGroups.Clear();
|
||||
_customData.Clear();
|
||||
|
||||
break;
|
||||
@@ -176,7 +176,7 @@ namespace FlaxEditor.Modules
|
||||
_expandedActors.Add(new Guid(bytes16));
|
||||
}
|
||||
|
||||
_collapsedGroups.Clear();
|
||||
_toggledGroups.Clear();
|
||||
|
||||
_customData.Clear();
|
||||
int customDataCount = reader.ReadInt32();
|
||||
@@ -201,11 +201,9 @@ namespace FlaxEditor.Modules
|
||||
}
|
||||
|
||||
int collapsedGroupsCount = reader.ReadInt32();
|
||||
_collapsedGroups.Clear();
|
||||
_toggledGroups.Clear();
|
||||
for (int i = 0; i < collapsedGroupsCount; i++)
|
||||
{
|
||||
_collapsedGroups.Add(reader.ReadString());
|
||||
}
|
||||
_toggledGroups.Add(reader.ReadString());
|
||||
|
||||
_customData.Clear();
|
||||
int customDataCount = reader.ReadInt32();
|
||||
@@ -259,11 +257,9 @@ namespace FlaxEditor.Modules
|
||||
writer.Write(e.ToByteArray());
|
||||
}
|
||||
|
||||
writer.Write(_collapsedGroups.Count);
|
||||
foreach (var e in _collapsedGroups)
|
||||
{
|
||||
writer.Write(_toggledGroups.Count);
|
||||
foreach (var e in _toggledGroups)
|
||||
writer.Write(e);
|
||||
}
|
||||
|
||||
writer.Write(_customData.Count);
|
||||
foreach (var e in _customData)
|
||||
@@ -284,7 +280,6 @@ namespace FlaxEditor.Modules
|
||||
try
|
||||
{
|
||||
SaveGuarded();
|
||||
|
||||
_isDirty = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -534,6 +534,41 @@ namespace FlaxEditor.Modules
|
||||
Delete();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create parent for selected actors.
|
||||
/// </summary>
|
||||
public void CreateParentForSelectedActors()
|
||||
{
|
||||
Actor actor = new EmptyActor();
|
||||
Editor.SceneEditing.Spawn(actor, null, false);
|
||||
List<SceneGraphNode> selection = Editor.SceneEditing.Selection;
|
||||
var actors = selection.Where(x => x is ActorNode).Select(x => ((ActorNode)x).Actor);
|
||||
using (new UndoMultiBlock(Undo, actors, "Reparent actors"))
|
||||
{
|
||||
for (int i = 0; i < selection.Count; i++)
|
||||
{
|
||||
if (selection[i] is ActorNode node)
|
||||
{
|
||||
if (node.ParentNode != node.ParentScene) // If parent node is not a scene
|
||||
{
|
||||
if (selection.Contains(node.ParentNode))
|
||||
{
|
||||
continue; // If parent and child nodes selected together, don't touch child nodes
|
||||
}
|
||||
|
||||
// Put created node as child of the Parent Node of node
|
||||
int parentOrder = node.Actor.OrderInParent;
|
||||
actor.Parent = node.Actor.Parent;
|
||||
actor.OrderInParent = parentOrder;
|
||||
}
|
||||
node.Actor.Parent = actor;
|
||||
}
|
||||
}
|
||||
}
|
||||
Editor.SceneEditing.Select(actor);
|
||||
Editor.Scene.GetActorNode(actor).TreeNode.StartRenaming(Editor.Windows.SceneWin, Editor.Windows.SceneWin.SceneTreePanel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates the selected objects. Supports undo/redo.
|
||||
/// </summary>
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace FlaxEditor.Modules
|
||||
private ContextMenuButton _menuEditCut;
|
||||
private ContextMenuButton _menuEditCopy;
|
||||
private ContextMenuButton _menuEditPaste;
|
||||
private ContextMenuButton _menuCreateParentForSelectedActors;
|
||||
private ContextMenuButton _menuEditDelete;
|
||||
private ContextMenuButton _menuEditDuplicate;
|
||||
private ContextMenuButton _menuEditSelectAll;
|
||||
@@ -535,6 +536,7 @@ namespace FlaxEditor.Modules
|
||||
_menuFileRecompileScripts = cm.AddButton("Recompile scripts", inputOptions.RecompileScripts, ScriptsBuilder.Compile);
|
||||
cm.AddSeparator();
|
||||
cm.AddButton("Open project...", OpenProject);
|
||||
cm.AddButton("Reload project", ReloadProject);
|
||||
cm.AddSeparator();
|
||||
cm.AddButton("Exit", "Alt+F4", () => Editor.Windows.MainWindow.Close(ClosingReason.User));
|
||||
|
||||
@@ -548,11 +550,11 @@ namespace FlaxEditor.Modules
|
||||
_menuEditCut = cm.AddButton("Cut", inputOptions.Cut, Editor.SceneEditing.Cut);
|
||||
_menuEditCopy = cm.AddButton("Copy", inputOptions.Copy, Editor.SceneEditing.Copy);
|
||||
_menuEditPaste = cm.AddButton("Paste", inputOptions.Paste, Editor.SceneEditing.Paste);
|
||||
cm.AddSeparator();
|
||||
_menuEditDelete = cm.AddButton("Delete", inputOptions.Delete, Editor.SceneEditing.Delete);
|
||||
_menuEditDuplicate = cm.AddButton("Duplicate", inputOptions.Duplicate, Editor.SceneEditing.Duplicate);
|
||||
cm.AddSeparator();
|
||||
_menuEditSelectAll = cm.AddButton("Select all", inputOptions.SelectAll, Editor.SceneEditing.SelectAllScenes);
|
||||
_menuCreateParentForSelectedActors = cm.AddButton("Create parent for selected actors", Editor.SceneEditing.CreateParentForSelectedActors);
|
||||
_menuEditFind = cm.AddButton("Find", inputOptions.Search, Editor.Windows.SceneWin.Search);
|
||||
cm.AddSeparator();
|
||||
cm.AddButton("Game Settings", () =>
|
||||
@@ -822,6 +824,13 @@ namespace FlaxEditor.Modules
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadProject()
|
||||
{
|
||||
// Open project, then close it
|
||||
Editor.OpenProject(Editor.GameProject.ProjectPath);
|
||||
Editor.Windows.MainWindow.Close(ClosingReason.User);
|
||||
}
|
||||
|
||||
private void OnMenuFileShowHide(Control control)
|
||||
{
|
||||
if (control.Visible == false)
|
||||
@@ -858,6 +867,7 @@ namespace FlaxEditor.Modules
|
||||
_menuEditCut.Enabled = hasSthSelected;
|
||||
_menuEditCopy.Enabled = hasSthSelected;
|
||||
_menuEditPaste.Enabled = canEditScene;
|
||||
_menuCreateParentForSelectedActors.Enabled = canEditScene && hasSthSelected;
|
||||
_menuEditDelete.Enabled = hasSthSelected;
|
||||
_menuEditDuplicate.Enabled = hasSthSelected;
|
||||
_menuEditSelectAll.Enabled = Level.IsAnySceneLoaded;
|
||||
|
||||
@@ -725,10 +725,8 @@ namespace FlaxEditor.Modules
|
||||
for (int i = 0; i < Windows.Count; i++)
|
||||
{
|
||||
if (string.Equals(Windows[i].SerializationTypename, typename, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Windows[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's an asset ID
|
||||
if (Guid.TryParse(typename, out Guid id))
|
||||
|
||||
@@ -172,9 +172,9 @@ namespace FlaxEditor.Options
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
_outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont), 10);
|
||||
_outputLogFont = new FontReference(ConsoleFont, 10);
|
||||
else if (!value.Font)
|
||||
_outputLogFont.Font = FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont);
|
||||
_outputLogFont.Font = ConsoleFont;
|
||||
else
|
||||
_outputLogFont = value;
|
||||
}
|
||||
@@ -237,11 +237,19 @@ namespace FlaxEditor.Options
|
||||
public int NumberOfGameClientsToLaunch = 1;
|
||||
|
||||
private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.PrimaryFont);
|
||||
private static FontAsset ConsoleFont => FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont);
|
||||
|
||||
private FontReference _titleFont = new FontReference(DefaultFont, 18);
|
||||
private FontReference _largeFont = new FontReference(DefaultFont, 14);
|
||||
private FontReference _mediumFont = new FontReference(DefaultFont, 9);
|
||||
private FontReference _smallFont = new FontReference(DefaultFont, 9);
|
||||
private FontReference _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont), 10);
|
||||
private FontReference _outputLogFont = new FontReference(ConsoleFont, 10);
|
||||
|
||||
/// <summary>
|
||||
/// The list of fallback fonts to use when main text font is missing certain characters. Empty to use fonts from GraphicsSettings.
|
||||
/// </summary>
|
||||
[EditorDisplay("Fonts"), EditorOrder(650)]
|
||||
public FontAsset[] FallbackFonts = new FontAsset[1] { FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.FallbackFont) };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title font for editor UI.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FlaxEditor.Content.Settings;
|
||||
using FlaxEditor.Modules;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
@@ -223,6 +225,12 @@ namespace FlaxEditor.Options
|
||||
Style.Current = CreateDefaultStyle();
|
||||
}
|
||||
}
|
||||
|
||||
// Set fallback fonts
|
||||
var fallbackFonts = Options.Interface.FallbackFonts;
|
||||
if (fallbackFonts == null || fallbackFonts.Length == 0 || fallbackFonts.All(x => x == null))
|
||||
fallbackFonts = GameSettings.Load<GraphicsSettings>().FallbackFonts;
|
||||
Font.FallbackFonts = fallbackFonts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
base.OnDebugDraw(data);
|
||||
|
||||
var transform = Actor.Transform;
|
||||
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 1.0f, Color.Red, 0.0f, false);
|
||||
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 1.0f, 0.5f, Color.Red, 0.0f, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ using Real = System.Single;
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor.Actions;
|
||||
using FlaxEditor.Modules;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEditor.Windows;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.Json;
|
||||
using FlaxEngine.Utilities;
|
||||
using Object = FlaxEngine.Object;
|
||||
|
||||
namespace FlaxEditor.SceneGraph.Actors
|
||||
@@ -288,6 +291,8 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
|
||||
private const Real PointNodeSize = 1.5f;
|
||||
private const Real TangentNodeSize = 1.0f;
|
||||
private const Real SnapIndicatorSize = 1.7f;
|
||||
private const Real SnapPointIndicatorSize = 2f;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SplineNode(Actor actor)
|
||||
@@ -297,9 +302,26 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
FlaxEngine.Scripting.Update += OnUpdate;
|
||||
}
|
||||
|
||||
private unsafe void OnUpdate()
|
||||
private void OnUpdate()
|
||||
{
|
||||
// If this node's point is selected
|
||||
var selection = Editor.Instance.SceneEditing.Selection;
|
||||
if (selection.Count == 1 && selection[0] is SplinePointNode selectedPoint && selectedPoint.ParentNode == this)
|
||||
{
|
||||
if (Input.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);
|
||||
if (requestAddSplinePoint && canAddSplinePoint)
|
||||
AddSplinePoint(selectedPoint);
|
||||
}
|
||||
|
||||
SyncSplineKeyframeWithNodes();
|
||||
}
|
||||
|
||||
private unsafe void SyncSplineKeyframeWithNodes()
|
||||
{
|
||||
// Sync spline points with gizmo handles
|
||||
var actor = (Spline)Actor;
|
||||
var dstCount = actor.SplinePointsCount;
|
||||
if (dstCount > 1 && actor.IsLoop)
|
||||
@@ -329,6 +351,119 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void AddSplinePoint(SplinePointNode selectedPoint)
|
||||
{
|
||||
// Check mouse hit on scene
|
||||
var spline = (Spline)Actor;
|
||||
var viewport = Editor.Instance.Windows.EditWin.Viewport;
|
||||
var mouseRay = viewport.MouseRay;
|
||||
var viewRay = viewport.ViewRay;
|
||||
var flags = RayCastData.FlagTypes.SkipColliders | RayCastData.FlagTypes.SkipEditorPrimitives;
|
||||
var hit = Editor.Instance.Scene.Root.RayCast(ref mouseRay, ref viewRay, out var closest, out var normal, flags);
|
||||
if (hit == null)
|
||||
return;
|
||||
|
||||
// Undo data
|
||||
var oldSpline = spline.SplineKeyframes;
|
||||
var editAction = new EditSplineAction(spline, oldSpline);
|
||||
Root.Undo.AddAction(editAction);
|
||||
|
||||
// Get spline point to duplicate
|
||||
var hitPoint = mouseRay.Position + mouseRay.Direction * closest;
|
||||
var lastPointIndex = selectedPoint.Index;
|
||||
var newPointIndex = lastPointIndex > 0 ? lastPointIndex + 1 : 0;
|
||||
var lastKeyframe = spline.GetSplineKeyframe(lastPointIndex);
|
||||
var isLastPoint = lastPointIndex == spline.SplinePointsCount - 1;
|
||||
var isFirstPoint = lastPointIndex == 0;
|
||||
|
||||
// Get data to create new point
|
||||
var lastPointTime = spline.GetSplineTime(lastPointIndex);
|
||||
var nextPointTime = isLastPoint ? lastPointTime : spline.GetSplineTime(newPointIndex);
|
||||
var newTime = isLastPoint ? lastPointTime + 1.0f : (lastPointTime + nextPointTime) * 0.5f;
|
||||
var distanceFromLastPoint = Vector3.Distance(hitPoint, spline.GetSplinePoint(lastPointIndex));
|
||||
var newPointDirection = spline.GetSplineTangent(lastPointIndex, false).Translation - hitPoint;
|
||||
|
||||
// Set correctly keyframe direction on spawn point
|
||||
if (isFirstPoint)
|
||||
newPointDirection = hitPoint - spline.GetSplineTangent(lastPointIndex, true).Translation;
|
||||
else if (isLastPoint)
|
||||
newPointDirection = spline.GetSplineTangent(lastPointIndex, false).Translation - hitPoint;
|
||||
var newPointLocalPosition = spline.Transform.WorldToLocal(hitPoint);
|
||||
var newPointLocalOrientation = Quaternion.LookRotation(newPointDirection);
|
||||
|
||||
// Add new point
|
||||
spline.InsertSplinePoint(newPointIndex, newTime, Transform.Identity, false);
|
||||
var newKeyframe = lastKeyframe.DeepClone();
|
||||
var newKeyframeTransform = newKeyframe.Value;
|
||||
newKeyframeTransform.Translation = newPointLocalPosition;
|
||||
newKeyframeTransform.Orientation = newPointLocalOrientation;
|
||||
newKeyframe.Value = newKeyframeTransform;
|
||||
|
||||
// Set new point keyframe
|
||||
var newKeyframeTangentIn = Transform.Identity;
|
||||
var newKeyframeTangentOut = Transform.Identity;
|
||||
newKeyframeTangentIn.Translation = (Vector3.Forward * newPointLocalOrientation) * distanceFromLastPoint;
|
||||
newKeyframeTangentOut.Translation = (Vector3.Backward * newPointLocalOrientation) * distanceFromLastPoint;
|
||||
newKeyframe.TangentIn = newKeyframeTangentIn;
|
||||
newKeyframe.TangentOut = newKeyframeTangentOut;
|
||||
spline.SetSplineKeyframe(newPointIndex, newKeyframe);
|
||||
|
||||
for (int i = 1; i < spline.SplinePointsCount; i++)
|
||||
{
|
||||
// check all elements to don't left keyframe has invalid time
|
||||
// because points can be added on start or on middle of spline
|
||||
// conflicting with time of another keyframes
|
||||
spline.SetSplinePointTime(i, i, false);
|
||||
}
|
||||
|
||||
// Select new point node
|
||||
SyncSplineKeyframeWithNodes();
|
||||
Editor.Instance.SceneEditing.Select(ChildNodes[newPointIndex]);
|
||||
|
||||
spline.UpdateSpline();
|
||||
}
|
||||
|
||||
private void EditSplineWithSnap(SplinePointNode selectedPoint)
|
||||
{
|
||||
var spline = (Spline)Actor;
|
||||
var selectedPointBounds = new BoundingSphere(selectedPoint.Transform.Translation, 1f);
|
||||
var allSplinesInView = GetSplinesInView();
|
||||
allSplinesInView.Remove(spline);
|
||||
if (allSplinesInView.Count == 0)
|
||||
return;
|
||||
|
||||
var snappedOnSplinePoint = false;
|
||||
for (int i = 0; i < allSplinesInView.Count; i++)
|
||||
{
|
||||
for (int x = 0; x < allSplinesInView[i].SplineKeyframes.Length; x++)
|
||||
{
|
||||
var keyframePosition = allSplinesInView[i].GetSplinePoint(x);
|
||||
var pointIndicatorSize = NodeSizeByDistance(keyframePosition, SnapPointIndicatorSize);
|
||||
var keyframeBounds = new BoundingSphere(keyframePosition, pointIndicatorSize);
|
||||
DebugDraw.DrawSphere(keyframeBounds, Color.Red, 0, false);
|
||||
|
||||
if (keyframeBounds.Intersects(selectedPointBounds))
|
||||
{
|
||||
spline.SetSplinePoint(selectedPoint.Index, keyframeBounds.Center);
|
||||
snappedOnSplinePoint = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!snappedOnSplinePoint)
|
||||
{
|
||||
var nearSplineSnapPoint = GetNearSplineSnapPosition(selectedPoint.Transform.Translation, allSplinesInView);
|
||||
var snapIndicatorSize = NodeSizeByDistance(nearSplineSnapPoint, SnapIndicatorSize);
|
||||
var snapBounds = new BoundingSphere(nearSplineSnapPoint, snapIndicatorSize);
|
||||
if (snapBounds.Intersects(selectedPointBounds))
|
||||
{
|
||||
spline.SetSplinePoint(selectedPoint.Index, snapBounds.Center);
|
||||
}
|
||||
DebugDraw.DrawSphere(snapBounds, Color.Yellow, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void PostSpawn()
|
||||
{
|
||||
@@ -343,14 +478,12 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
var spline = (Spline)Actor;
|
||||
spline.AddSplineLocalPoint(Vector3.Zero, false);
|
||||
spline.AddSplineLocalPoint(new Vector3(0, 0, 100.0f));
|
||||
|
||||
spline.SetSplineKeyframe(0, new BezierCurve<Transform>.Keyframe()
|
||||
{
|
||||
Value = new Transform(Vector3.Zero, Quaternion.Identity, Vector3.One),
|
||||
TangentIn = new Transform(Vector3.Backward * 100, Quaternion.Identity, Vector3.One),
|
||||
TangentOut = new Transform(Vector3.Forward * 100, Quaternion.Identity, Vector3.One),
|
||||
});
|
||||
|
||||
spline.SetSplineKeyframe(1, new BezierCurve<Transform>.Keyframe()
|
||||
{
|
||||
Value = new Transform(Vector3.Forward * 100, Quaternion.Identity, Vector3.One),
|
||||
@@ -413,6 +546,39 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Spline> GetSplinesInView()
|
||||
{
|
||||
var splines = Level.GetActors<Spline>(true);
|
||||
var result = new List<Spline>();
|
||||
var viewBounds = Editor.Instance.Windows.EditWin.Viewport.ViewFrustum;
|
||||
foreach (var s in splines)
|
||||
{
|
||||
var contains = viewBounds.Contains(s.EditorBox);
|
||||
if (contains == ContainmentType.Contains || contains == ContainmentType.Intersects)
|
||||
result.Add(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Vector3 GetNearSplineSnapPosition(Vector3 position, List<Spline> splines)
|
||||
{
|
||||
var nearPoint = splines[0].GetSplinePointClosestToPoint(position);
|
||||
var nearDistance = Vector3.Distance(nearPoint, position);
|
||||
|
||||
for (int i = 1; i < splines.Count; i++)
|
||||
{
|
||||
var point = splines[i].GetSplinePointClosestToPoint(position);
|
||||
var distance = Vector3.Distance(point, position);
|
||||
if (distance < nearDistance)
|
||||
{
|
||||
nearPoint = point;
|
||||
nearDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return nearPoint;
|
||||
}
|
||||
|
||||
internal static Real NodeSizeByDistance(Vector3 nodePosition, Real nodeSize)
|
||||
{
|
||||
var cameraTransform = Editor.Instance.Windows.EditWin.Viewport.ViewportCamera.Viewport.ViewTransform;
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
base.OnDebugDraw(data);
|
||||
|
||||
var transform = Actor.Transform;
|
||||
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 0.3f, Color.Red, 0.0f, false);
|
||||
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 0.3f, 0.15f, Color.Red, 0.0f, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,19 +115,34 @@ namespace FlaxEditor.Surface
|
||||
|
||||
internal AnimGraphTraceEvent[] LastTraceEvents;
|
||||
|
||||
internal bool TryGetTraceEvent(SurfaceNode node, out AnimGraphTraceEvent traceEvent)
|
||||
internal unsafe bool TryGetTraceEvent(SurfaceNode node, out AnimGraphTraceEvent traceEvent)
|
||||
{
|
||||
if (LastTraceEvents != null)
|
||||
{
|
||||
foreach (var e in LastTraceEvents)
|
||||
{
|
||||
// Node IDs must match
|
||||
if (e.NodeId == node.ID)
|
||||
{
|
||||
uint* nodePath = e.NodePath0;
|
||||
|
||||
// Get size of the path
|
||||
int nodePathSize = 0;
|
||||
while (nodePathSize < 8 && nodePath[nodePathSize] != 0)
|
||||
nodePathSize++;
|
||||
|
||||
// Follow input node contexts path to verify if it matches with the path in the event
|
||||
var c = node.Context;
|
||||
for (int i = nodePathSize - 1; i >= 0 && c != null; i--)
|
||||
c = c.OwnerNodeID == nodePath[i] ? c.Parent : null;
|
||||
if (c != null)
|
||||
{
|
||||
traceEvent = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
traceEvent = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -191,9 +191,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
var value = title;
|
||||
int count = 1;
|
||||
while (!OnRenameValidate(null, value))
|
||||
{
|
||||
value = title + " " + count++;
|
||||
}
|
||||
Values[0] = value;
|
||||
Title = value;
|
||||
|
||||
@@ -484,7 +482,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
var startPos = PointToParent(ref center);
|
||||
targetState.GetConnectionEndPoint(ref startPos, out var endPos);
|
||||
var color = style.Foreground;
|
||||
StateMachineState.DrawConnection(ref startPos, ref endPos, ref color);
|
||||
SurfaceStyle.DrawStraightConnection(startPos, endPos, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,7 +512,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
/// <inheritdoc />
|
||||
public void DrawConnectingLine(ref Float2 startPos, ref Float2 endPos, ref Color color)
|
||||
{
|
||||
StateMachineState.DrawConnection(ref startPos, ref endPos, ref color);
|
||||
SurfaceStyle.DrawStraightConnection(startPos, endPos, color);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -655,6 +653,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
protected Rectangle _renameButtonRect;
|
||||
private bool _cursorChanged = false;
|
||||
private bool _textRectHovered = false;
|
||||
private bool _debugActive;
|
||||
|
||||
/// <summary>
|
||||
/// The transitions list from this state to the others.
|
||||
@@ -677,38 +676,6 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the connection between two state machine nodes.
|
||||
/// </summary>
|
||||
/// <param name="startPos">The start position.</param>
|
||||
/// <param name="endPos">The end position.</param>
|
||||
/// <param name="color">The line color.</param>
|
||||
public static void DrawConnection(ref Float2 startPos, ref Float2 endPos, ref Color color)
|
||||
{
|
||||
var sub = endPos - startPos;
|
||||
var length = sub.Length;
|
||||
if (length > Mathf.Epsilon)
|
||||
{
|
||||
var dir = sub / length;
|
||||
var arrowRect = new Rectangle(0, 0, 16.0f, 16.0f);
|
||||
float rotation = Float2.Dot(dir, Float2.UnitY);
|
||||
if (endPos.X < startPos.X)
|
||||
rotation = 2 - rotation;
|
||||
var sprite = Editor.Instance.Icons.VisjectArrowClosed32;
|
||||
var arrowTransform =
|
||||
Matrix3x3.Translation2D(-6.5f, -8) *
|
||||
Matrix3x3.RotationZ(rotation * Mathf.PiOverTwo) *
|
||||
Matrix3x3.Translation2D(endPos - dir * 8);
|
||||
|
||||
Render2D.PushTransform(ref arrowTransform);
|
||||
Render2D.DrawSprite(sprite, arrowRect, color);
|
||||
Render2D.PopTransform();
|
||||
|
||||
endPos -= dir * 4.0f;
|
||||
}
|
||||
Render2D.DrawLine(startPos, endPos, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection end point for the given input position. Puts the end point near the edge of the node bounds.
|
||||
/// </summary>
|
||||
@@ -1092,6 +1059,16 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
// TODO: maybe update only on actual transitions change?
|
||||
UpdateTransitions();
|
||||
|
||||
// Debug current state
|
||||
if (((AnimGraphSurface)Surface).TryGetTraceEvent(this, out var traceEvent))
|
||||
{
|
||||
_debugActive = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_debugActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1132,6 +1109,10 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
// Close button
|
||||
Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
|
||||
|
||||
// Debug outline
|
||||
if (_debugActive)
|
||||
Render2D.DrawRectangle(_textRect.MakeExpanded(1.0f), style.ProgressNormal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1295,7 +1276,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
isMouseOver = Float2.DistanceSquared(ref mousePosition, ref point) < 25.0f;
|
||||
}
|
||||
var color = isMouseOver ? Color.Wheat : t.LineColor;
|
||||
DrawConnection(ref t.StartPos, ref t.EndPos, ref color);
|
||||
SurfaceStyle.DrawStraightConnection(t.StartPos, t.EndPos, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,7 +1305,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
/// <inheritdoc />
|
||||
public void DrawConnectingLine(ref Float2 startPos, ref Float2 endPos, ref Color color)
|
||||
{
|
||||
DrawConnection(ref startPos, ref endPos, ref color);
|
||||
SurfaceStyle.DrawStraightConnection(startPos, endPos, color);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace FlaxEditor.Surface
|
||||
var editor = Editor.Instance;
|
||||
var style = SurfaceStyle.CreateStyleHandler(editor);
|
||||
style.DrawBox = DrawBox;
|
||||
style.DrawConnection = DrawConnection;
|
||||
style.DrawConnection = SurfaceStyle.DrawStraightConnection;
|
||||
return style;
|
||||
}
|
||||
|
||||
@@ -49,11 +49,6 @@ namespace FlaxEditor.Surface
|
||||
Render2D.FillRectangle(rect, color);
|
||||
}
|
||||
|
||||
private static void DrawConnection(Float2 start, Float2 end, Color color, float thickness)
|
||||
{
|
||||
Archetypes.Animation.StateMachineStateBase.DrawConnection(ref start, ref end, ref color);
|
||||
}
|
||||
|
||||
private void OnActiveContextMenuVisibleChanged(Control activeCM)
|
||||
{
|
||||
_nodesCache.Wait();
|
||||
|
||||
@@ -492,7 +492,6 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
|
||||
// If no item is selected (or it's not visible anymore), select the top one
|
||||
Profiler.BeginEvent("VisjectCM.Layout");
|
||||
if (SelectedItem == null || !SelectedItem.VisibleInHierarchy)
|
||||
SelectedItem = _groups.Find(g => g.Visible)?.Children.Find(c => c.Visible && c is VisjectCMItem) as VisjectCMItem;
|
||||
PerformLayout();
|
||||
if (SelectedItem != null)
|
||||
@@ -704,7 +703,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
Hide();
|
||||
return true;
|
||||
}
|
||||
else if (key == KeyboardKeys.Return)
|
||||
else if (key == KeyboardKeys.Return || key == KeyboardKeys.Tab)
|
||||
{
|
||||
if (SelectedItem != null)
|
||||
OnClickItem(SelectedItem);
|
||||
|
||||
@@ -73,8 +73,6 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
{
|
||||
SortScore = 0;
|
||||
|
||||
if (!(_highlights?.Count > 0))
|
||||
return;
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
@@ -83,6 +81,8 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
SortScore += 1;
|
||||
if (Data != null)
|
||||
SortScore += 1;
|
||||
if (_highlights is { Count: > 0 })
|
||||
SortScore += 1;
|
||||
if (_isStartsWithMatch)
|
||||
SortScore += 2;
|
||||
if (_isFullMatch)
|
||||
@@ -169,6 +169,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
/// <param name="groupHeaderMatches">True if item's group header got a filter match and item should stay visible.</param>
|
||||
public void UpdateFilter(string filterText, Box selectedBox, bool groupHeaderMatches = false)
|
||||
{
|
||||
// When dragging connection out of a box, validate if the box is compatible with this item's type
|
||||
if (selectedBox != null)
|
||||
{
|
||||
Visible = CanConnectTo(selectedBox);
|
||||
@@ -185,10 +186,12 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
// Clear filter
|
||||
_highlights?.Clear();
|
||||
Visible = true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
GetTextRectangle(out var textRect);
|
||||
|
||||
// Check archetype title
|
||||
if (QueryFilterHelper.Match(filterText, _archetype.Title, out var ranges))
|
||||
{
|
||||
// Update highlights
|
||||
@@ -212,46 +215,59 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
}
|
||||
}
|
||||
Visible = true;
|
||||
return;
|
||||
}
|
||||
else if (_archetype.AlternativeTitles?.Any(altTitle => string.Equals(filterText, altTitle, StringComparison.CurrentCultureIgnoreCase)) == true)
|
||||
{
|
||||
// Update highlights
|
||||
if (_highlights == null)
|
||||
_highlights = new List<Rectangle>(1);
|
||||
else
|
||||
_highlights.Clear();
|
||||
var style = Style.Current;
|
||||
var font = style.FontSmall;
|
||||
var start = font.GetCharPosition(_archetype.Title, 0);
|
||||
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
|
||||
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
|
||||
_isFullMatch = true;
|
||||
Visible = true;
|
||||
}
|
||||
else if (NodeArchetype.TryParseText != null && NodeArchetype.TryParseText(filterText, out var data))
|
||||
{
|
||||
// Update highlights
|
||||
if (_highlights == null)
|
||||
_highlights = new List<Rectangle>(1);
|
||||
else
|
||||
_highlights.Clear();
|
||||
var style = Style.Current;
|
||||
var font = style.FontSmall;
|
||||
var start = font.GetCharPosition(_archetype.Title, 0);
|
||||
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
|
||||
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
|
||||
Visible = true;
|
||||
|
||||
Data = data;
|
||||
}
|
||||
else if (!groupHeaderMatches)
|
||||
// Check archetype synonyms
|
||||
if (_archetype.AlternativeTitles!= null && _archetype.AlternativeTitles.Any(altTitle => QueryFilterHelper.Match(filterText, altTitle, out ranges)))
|
||||
{
|
||||
// Hide
|
||||
// Update highlights
|
||||
if (_highlights == null)
|
||||
_highlights = new List<Rectangle>(1);
|
||||
else
|
||||
_highlights.Clear();
|
||||
var style = Style.Current;
|
||||
var font = style.FontSmall;
|
||||
var start = font.GetCharPosition(_archetype.Title, 0);
|
||||
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
|
||||
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
|
||||
|
||||
for (int i = 0; i < ranges.Length; i++)
|
||||
{
|
||||
if (ranges[i].StartIndex <= 0)
|
||||
{
|
||||
_isStartsWithMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check archetype data (if it exists)
|
||||
if (NodeArchetype.TryParseText != null && NodeArchetype.TryParseText(filterText, out var data))
|
||||
{
|
||||
// Update highlights
|
||||
if (_highlights == null)
|
||||
_highlights = new List<Rectangle>(1);
|
||||
else
|
||||
_highlights.Clear();
|
||||
var style = Style.Current;
|
||||
var font = style.FontSmall;
|
||||
var start = font.GetCharPosition(_archetype.Title, 0);
|
||||
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
|
||||
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
|
||||
Visible = true;
|
||||
Data = data;
|
||||
return;
|
||||
}
|
||||
|
||||
_highlights?.Clear();
|
||||
|
||||
// Hide
|
||||
if (!groupHeaderMatches)
|
||||
Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
|
||||
@@ -295,5 +295,38 @@ namespace FlaxEditor.Surface
|
||||
Background = editor.UI.VisjectSurfaceBackground,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a simple straight connection between two locations.
|
||||
/// </summary>
|
||||
/// <param name="startPos">The start position.</param>
|
||||
/// <param name="endPos">The end position.</param>
|
||||
/// <param name="color">The line color.</param>
|
||||
/// <param name="thickness">The line thickness.</param>
|
||||
public static void DrawStraightConnection(Float2 startPos, Float2 endPos, Color color, float thickness = 1.0f)
|
||||
{
|
||||
var sub = endPos - startPos;
|
||||
var length = sub.Length;
|
||||
if (length > Mathf.Epsilon)
|
||||
{
|
||||
var dir = sub / length;
|
||||
var arrowRect = new Rectangle(0, 0, 16.0f, 16.0f);
|
||||
float rotation = Float2.Dot(dir, Float2.UnitY);
|
||||
if (endPos.X < startPos.X)
|
||||
rotation = 2 - rotation;
|
||||
var sprite = Editor.Instance.Icons.VisjectArrowClosed32;
|
||||
var arrowTransform =
|
||||
Matrix3x3.Translation2D(-6.5f, -8) *
|
||||
Matrix3x3.RotationZ(rotation * Mathf.PiOverTwo) *
|
||||
Matrix3x3.Translation2D(endPos - dir * 8);
|
||||
|
||||
Render2D.PushTransform(ref arrowTransform);
|
||||
Render2D.DrawSprite(sprite, arrowRect, color);
|
||||
Render2D.PopTransform();
|
||||
|
||||
endPos -= dir * 4.0f;
|
||||
}
|
||||
Render2D.DrawLine(startPos, endPos, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,30 @@ namespace FlaxEditor.Surface
|
||||
/// </summary>
|
||||
public event Action<VisjectSurfaceContext> ContextChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the surface context with the given owning nodes IDs path.
|
||||
/// </summary>
|
||||
/// <param name="nodePath">The node ids path.</param>
|
||||
/// <returns>Found context or null if cannot.</returns>
|
||||
public VisjectSurfaceContext FindContext(Span<uint> nodePath)
|
||||
{
|
||||
// Get size of the path
|
||||
int nodePathSize = 0;
|
||||
while (nodePathSize < nodePath.Length && nodePath[nodePathSize] != 0)
|
||||
nodePathSize++;
|
||||
|
||||
// Follow each context path to verify if it matches with the path in the input path
|
||||
foreach (var e in _contextCache)
|
||||
{
|
||||
var c = e.Value;
|
||||
for (int i = nodePathSize - 1; i >= 0 && c != null; i--)
|
||||
c = c.OwnerNodeID == nodePath[i] ? c.Parent : null;
|
||||
if (c != null)
|
||||
return e.Value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the Visject surface context for the given surface data source context.
|
||||
/// </summary>
|
||||
@@ -62,6 +86,8 @@ namespace FlaxEditor.Surface
|
||||
surfaceContext = CreateContext(_context, context);
|
||||
_context?.Children.Add(surfaceContext);
|
||||
_contextCache.Add(contextHandle, surfaceContext);
|
||||
if (context is SurfaceNode asNode)
|
||||
surfaceContext.OwnerNodeID = asNode.ID;
|
||||
|
||||
context.OnContextCreated(surfaceContext);
|
||||
|
||||
|
||||
@@ -20,8 +20,15 @@ namespace FlaxEditor.Surface
|
||||
/// <summary>
|
||||
/// Utility for easy nodes archetypes generation for Visject Surface based on scripting types.
|
||||
/// </summary>
|
||||
internal class NodesCache
|
||||
[HideInEditor]
|
||||
public class NodesCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for scripting types filtering into cache.
|
||||
/// </summary>
|
||||
/// <param name="scriptType">The input type to process.</param>
|
||||
/// <param name="cache">Node groups cache that can be used for reusing groups for different nodes.</param>
|
||||
/// <param name="version">The cache version number. Can be used to reject any cached data after <see cref="NodesCache"/> rebuilt.</param>
|
||||
public delegate void IterateType(ScriptType scriptType, Dictionary<KeyValuePair<string, ushort>, GroupArchetype> cache, int version);
|
||||
|
||||
internal static readonly List<NodesCache> Caches = new List<NodesCache>(8);
|
||||
@@ -33,11 +40,18 @@ namespace FlaxEditor.Surface
|
||||
private VisjectCM _taskContextMenu;
|
||||
private Dictionary<KeyValuePair<string, ushort>, GroupArchetype> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NodesCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="iterator">The iterator callback to build node types from Scripting.</param>
|
||||
public NodesCache(IterateType iterator)
|
||||
{
|
||||
_iterator = iterator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the async caching job to finish.
|
||||
/// </summary>
|
||||
public void Wait()
|
||||
{
|
||||
if (_task != null)
|
||||
@@ -48,6 +62,9 @@ namespace FlaxEditor.Surface
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears cache.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Wait();
|
||||
@@ -62,6 +79,10 @@ namespace FlaxEditor.Surface
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Visject Context Menu to contain current nodes.
|
||||
/// </summary>
|
||||
/// <param name="contextMenu">The output context menu to setup.</param>
|
||||
public void Get(VisjectCM contextMenu)
|
||||
{
|
||||
Profiler.BeginEvent("Setup Context Menu");
|
||||
|
||||
@@ -156,6 +156,11 @@ namespace FlaxEditor.Surface
|
||||
/// </summary>
|
||||
public event Action<SurfaceControl> ControlDeleted;
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the node that 'owns' this context (eg. State Machine which created this graph of state nodes).
|
||||
/// </summary>
|
||||
public uint OwnerNodeID;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VisjectSurfaceContext"/> class.
|
||||
/// </summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace FlaxEditor.Surface
|
||||
/// The base interface for editor windows that use <see cref="FlaxEditor.Surface.VisjectSurface"/> for content editing.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Surface.IVisjectSurfaceOwner" />
|
||||
interface IVisjectSurfaceWindow : IVisjectSurfaceOwner
|
||||
public interface IVisjectSurfaceWindow : IVisjectSurfaceOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the asset edited by the window.
|
||||
|
||||
@@ -43,6 +43,19 @@ namespace FlaxEngine.Tools
|
||||
/// Enables continuous painting, otherwise single paint on click.
|
||||
/// </summary>
|
||||
public bool ContinuousPaint;
|
||||
|
||||
/// <summary>
|
||||
/// Enables drawing cloth paint debugging with Depth Test enabled (skips occluded vertices).
|
||||
/// </summary>
|
||||
public bool DebugDrawDepthTest
|
||||
{
|
||||
get => Gizmo.Cloth?.DebugDrawDepthTest ?? true;
|
||||
set
|
||||
{
|
||||
if (Gizmo.Cloth != null)
|
||||
Gizmo.Cloth.DebugDrawDepthTest = value;
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0649
|
||||
|
||||
public override void Init(IGizmoOwner owner)
|
||||
@@ -62,6 +75,7 @@ namespace FlaxEngine.Tools
|
||||
public override void Dispose()
|
||||
{
|
||||
Owner.Gizmos.Remove(Gizmo);
|
||||
Gizmo = null;
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
@@ -83,6 +97,7 @@ namespace FlaxEngine.Tools
|
||||
private EditClothPaintAction _undoAction;
|
||||
|
||||
public bool IsPainting => _isPainting;
|
||||
public Cloth Cloth => _cloth;
|
||||
|
||||
public ClothPaintingGizmo(IGizmoOwner owner, ClothPaintingGizmoMode mode)
|
||||
: base(owner)
|
||||
|
||||
@@ -45,9 +45,6 @@ namespace FlaxEditor.Tools.Terrain
|
||||
[EditorOrder(130), EditorDisplay("Layout"), DefaultValue(null), Tooltip("The default material used for terrain rendering (chunks can override this). It must have Domain set to terrain.")]
|
||||
public MaterialBase Material;
|
||||
|
||||
[EditorOrder(200), EditorDisplay("Collision"), DefaultValue(null), AssetReference(typeof(PhysicalMaterial), true), Tooltip("Terrain default physical material used to define the collider physical properties.")]
|
||||
public JsonAsset PhysicalMaterial;
|
||||
|
||||
[EditorOrder(210), EditorDisplay("Collision", "Collision LOD"), DefaultValue(-1), Limit(-1, 100, 0.1f), Tooltip("Terrain geometry LOD index used for collision.")]
|
||||
public int CollisionLOD = -1;
|
||||
|
||||
@@ -152,7 +149,6 @@ namespace FlaxEditor.Tools.Terrain
|
||||
terrain.Setup(_options.LODCount, (int)_options.ChunkSize);
|
||||
terrain.Transform = new Transform(_options.Position, _options.Orientation, _options.Scale);
|
||||
terrain.Material = _options.Material;
|
||||
terrain.PhysicalMaterial = _options.PhysicalMaterial;
|
||||
terrain.CollisionLOD = _options.CollisionLOD;
|
||||
if (_options.Heightmap)
|
||||
terrain.Position -= new Vector3(0, _options.HeightmapScale * 0.5f, 0);
|
||||
|
||||
@@ -67,11 +67,13 @@ namespace FlaxEditor.Tools.Terrain.Paint
|
||||
|
||||
// Prepare
|
||||
var splatmapIndex = ActiveSplatmapIndex;
|
||||
var splatmapIndexOther = (splatmapIndex + 1) % 2;
|
||||
var chunkSize = terrain.ChunkSize;
|
||||
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
|
||||
var heightmapLength = heightmapSize * heightmapSize;
|
||||
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
|
||||
var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes).ToPointer();
|
||||
var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, splatmapIndex).ToPointer();
|
||||
var tempBufferOther = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, (splatmapIndex + 1) % 2).ToPointer();
|
||||
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
|
||||
ApplyParams p = new ApplyParams
|
||||
{
|
||||
@@ -81,8 +83,10 @@ namespace FlaxEditor.Tools.Terrain.Paint
|
||||
Options = options,
|
||||
Strength = strength,
|
||||
SplatmapIndex = splatmapIndex,
|
||||
SplatmapIndexOther = splatmapIndexOther,
|
||||
HeightmapSize = heightmapSize,
|
||||
TempBuffer = tempBuffer,
|
||||
TempBufferOther = tempBufferOther,
|
||||
};
|
||||
|
||||
// Get brush bounds in terrain local space
|
||||
@@ -132,10 +136,15 @@ namespace FlaxEditor.Tools.Terrain.Paint
|
||||
if (sourceData == null)
|
||||
throw new Exception("Cannot modify terrain. Loading splatmap failed. See log for more info.");
|
||||
|
||||
var sourceDataOther = TerrainTools.GetSplatMapData(terrain, ref patch.PatchCoord, splatmapIndexOther);
|
||||
if (sourceDataOther == null)
|
||||
throw new Exception("Cannot modify terrain. Loading splatmap failed. See log for more info.");
|
||||
|
||||
// Record patch data before editing it
|
||||
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
|
||||
{
|
||||
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord, splatmapIndex);
|
||||
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord, splatmapIndexOther);
|
||||
}
|
||||
|
||||
// Apply modification
|
||||
@@ -144,6 +153,7 @@ namespace FlaxEditor.Tools.Terrain.Paint
|
||||
p.PatchCoord = patch.PatchCoord;
|
||||
p.PatchPositionLocal = patchPositionLocal;
|
||||
p.SourceData = sourceData;
|
||||
p.SourceDataOther = sourceDataOther;
|
||||
Apply(ref p);
|
||||
}
|
||||
}
|
||||
@@ -198,16 +208,32 @@ namespace FlaxEditor.Tools.Terrain.Paint
|
||||
/// </summary>
|
||||
public int SplatmapIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The splatmap texture index. If <see cref="SplatmapIndex"/> is 0, this will be 1. If <see cref="SplatmapIndex"/> is 1, this will be 0.
|
||||
/// </summary>
|
||||
public int SplatmapIndexOther;
|
||||
|
||||
/// <summary>
|
||||
/// The temporary data buffer (for modified data).
|
||||
/// </summary>
|
||||
public Color32* TempBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// The 'other' temporary data buffer (for modified data). If <see cref="TempBuffer"/> refersto the splatmap with index 0, this one will refer to the one with index 1.
|
||||
/// </summary>
|
||||
public Color32* TempBufferOther;
|
||||
|
||||
/// <summary>
|
||||
/// The source data buffer.
|
||||
/// </summary>
|
||||
public Color32* SourceData;
|
||||
|
||||
/// <summary>
|
||||
/// The 'other' source data buffer. If <see cref="SourceData"/> refers
|
||||
/// to the splatmap with index 0, this one will refer to the one with index 1.
|
||||
/// </summary>
|
||||
public Color32* SourceDataOther;
|
||||
|
||||
/// <summary>
|
||||
/// The heightmap size (edge).
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Tools.Terrain.Paint
|
||||
@@ -73,53 +72,53 @@ namespace FlaxEditor.Tools.Terrain.Paint
|
||||
var strength = p.Strength;
|
||||
var layer = (int)Layer;
|
||||
var brushPosition = p.Gizmo.CursorPosition;
|
||||
var layerComponent = layer % 4;
|
||||
var c = layer % 4;
|
||||
|
||||
// Apply brush modification
|
||||
Profiler.BeginEvent("Apply Brush");
|
||||
bool otherModified = false;
|
||||
for (int z = 0; z < p.ModifiedSize.Y; z++)
|
||||
{
|
||||
var zz = z + p.ModifiedOffset.Y;
|
||||
for (int x = 0; x < p.ModifiedSize.X; x++)
|
||||
{
|
||||
var xx = x + p.ModifiedOffset.X;
|
||||
var src = p.SourceData[zz * p.HeightmapSize + xx];
|
||||
var src = (Color)p.SourceData[zz * p.HeightmapSize + xx];
|
||||
|
||||
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, 0, zz * FlaxEngine.Terrain.UnitsPerVertex);
|
||||
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
|
||||
var sample = Mathf.Saturate(p.Brush.Sample(ref brushPosition, ref samplePositionWorld));
|
||||
|
||||
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
|
||||
var paintAmount = sample * strength;
|
||||
if (paintAmount < 0.0f)
|
||||
continue; // Skip when pixel won't be affected
|
||||
|
||||
// Extract layer weight
|
||||
byte* srcPtr = &src.R;
|
||||
var srcWeight = *(srcPtr + layerComponent) / 255.0f;
|
||||
|
||||
// Accumulate weight
|
||||
float dstWeight = srcWeight + paintAmount;
|
||||
|
||||
// Check for solid layer case
|
||||
if (dstWeight >= 1.0f)
|
||||
{
|
||||
// Erase other layers
|
||||
// TODO: maybe erase only the higher layers?
|
||||
// TODO: need to erase also weights form the other splatmaps
|
||||
src = Color32.Transparent;
|
||||
|
||||
// Use limit value
|
||||
dstWeight = 1.0f;
|
||||
}
|
||||
|
||||
// Modify packed weight
|
||||
*(srcPtr + layerComponent) = (byte)(dstWeight * 255.0f);
|
||||
|
||||
// Write back
|
||||
// Paint on the active splatmap texture
|
||||
src[c] = Mathf.Saturate(src[c] + paintAmount);
|
||||
src[(c + 1) % 4] = Mathf.Saturate(src[(c + 1) % 4] - paintAmount);
|
||||
src[(c + 2) % 4] = Mathf.Saturate(src[(c + 2) % 4] - paintAmount);
|
||||
src[(c + 3) % 4] = Mathf.Saturate(src[(c + 3) % 4] - paintAmount);
|
||||
p.TempBuffer[z * p.ModifiedSize.X + x] = src;
|
||||
|
||||
var other = (Color)p.SourceDataOther[zz * p.HeightmapSize + xx];
|
||||
//if (other.ValuesSum > 0.0f) // Skip editing the other splatmap if it's empty
|
||||
{
|
||||
// Remove 'paint' from the other splatmap texture
|
||||
other[c] = Mathf.Saturate(other[c] - paintAmount);
|
||||
other[(c + 1) % 4] = Mathf.Saturate(other[(c + 1) % 4] - paintAmount);
|
||||
other[(c + 2) % 4] = Mathf.Saturate(other[(c + 2) % 4] - paintAmount);
|
||||
other[(c + 3) % 4] = Mathf.Saturate(other[(c + 3) % 4] - paintAmount);
|
||||
p.TempBufferOther[z * p.ModifiedSize.X + x] = other;
|
||||
otherModified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Profiler.EndEvent();
|
||||
|
||||
// Update terrain patch
|
||||
TerrainTools.ModifySplatMap(p.Terrain, ref p.PatchCoord, p.SplatmapIndex, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
if (otherModified)
|
||||
TerrainTools.ModifySplatMap(p.Terrain, ref p.PatchCoord, p.SplatmapIndexOther, p.TempBufferOther, ref p.ModifiedOffset, ref p.ModifiedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,35 @@ namespace FlaxEditor.Tools.Terrain
|
||||
"Layer 7",
|
||||
};
|
||||
|
||||
private IntPtr _cachedSplatmapData;
|
||||
private int _cachedSplatmapDataSize;
|
||||
private struct SplatmapData
|
||||
{
|
||||
public IntPtr DataPtr;
|
||||
public int Size;
|
||||
|
||||
public void EnsureCapacity(int size)
|
||||
{
|
||||
if (Size < size)
|
||||
{
|
||||
if (DataPtr != IntPtr.Zero)
|
||||
Marshal.FreeHGlobal(DataPtr);
|
||||
DataPtr = Marshal.AllocHGlobal(size);
|
||||
Utils.MemoryClear(DataPtr, (ulong)size);
|
||||
Size = size;
|
||||
}
|
||||
}
|
||||
|
||||
public void Free()
|
||||
{
|
||||
if (DataPtr == IntPtr.Zero)
|
||||
return;
|
||||
Marshal.FreeHGlobal(DataPtr);
|
||||
DataPtr = IntPtr.Zero;
|
||||
Size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private EditTerrainMapAction _activeAction;
|
||||
private SplatmapData[] _cachedSplatmapData = new SplatmapData[2];
|
||||
|
||||
/// <summary>
|
||||
/// The terrain painting gizmo.
|
||||
@@ -230,20 +256,13 @@ namespace FlaxEditor.Tools.Terrain
|
||||
/// Gets the splatmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
|
||||
/// </summary>
|
||||
/// <param name="size">The minimum buffer size (in bytes).</param>
|
||||
/// <param name="splatmapIndex">The splatmap index for which to return/create the temp buffer.</param>
|
||||
/// <returns>The allocated memory using <see cref="Marshal"/> interface.</returns>
|
||||
public IntPtr GetSplatmapTempBuffer(int size)
|
||||
public IntPtr GetSplatmapTempBuffer(int size, int splatmapIndex)
|
||||
{
|
||||
if (_cachedSplatmapDataSize < size)
|
||||
{
|
||||
if (_cachedSplatmapData != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(_cachedSplatmapData);
|
||||
}
|
||||
_cachedSplatmapData = Marshal.AllocHGlobal(size);
|
||||
_cachedSplatmapDataSize = size;
|
||||
}
|
||||
|
||||
return _cachedSplatmapData;
|
||||
ref var splatmapData = ref _cachedSplatmapData[splatmapIndex];
|
||||
splatmapData.EnsureCapacity(size);
|
||||
return splatmapData.DataPtr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -276,12 +295,8 @@ namespace FlaxEditor.Tools.Terrain
|
||||
base.OnDeactivated();
|
||||
|
||||
// Free temporary memory buffer
|
||||
if (_cachedSplatmapData != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(_cachedSplatmapData);
|
||||
_cachedSplatmapData = IntPtr.Zero;
|
||||
_cachedSplatmapDataSize = 0;
|
||||
}
|
||||
foreach (ref var splatmapData in _cachedSplatmapData.AsSpan())
|
||||
splatmapData.Free();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,7 +20,7 @@ bool TerrainTools::TryGetPatchCoordToAdd(Terrain* terrain, const Ray& ray, Int2&
|
||||
{
|
||||
CHECK_RETURN(terrain, true);
|
||||
result = Int2::Zero;
|
||||
const float patchSize = terrain->GetChunkSize() * TERRAIN_UNITS_PER_VERTEX * TerrainPatch::CHUNKS_COUNT_EDGE;
|
||||
const float patchSize = terrain->GetChunkSize() * TERRAIN_UNITS_PER_VERTEX * Terrain::ChunksCountEdge;
|
||||
|
||||
// Try to pick any of the patch edges
|
||||
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
|
||||
@@ -179,7 +179,7 @@ bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches
|
||||
terrain->AddPatches(numberOfPatches);
|
||||
|
||||
// Prepare data
|
||||
const auto heightmapSize = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
|
||||
const auto heightmapSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
|
||||
Array<float> heightmapData;
|
||||
heightmapData.Resize(heightmapSize * heightmapSize);
|
||||
|
||||
@@ -380,7 +380,7 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
|
||||
const auto firstPatch = terrain->GetPatch(0);
|
||||
|
||||
// Calculate texture size
|
||||
const int32 patchEdgeVertexCount = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
|
||||
const int32 patchEdgeVertexCount = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
|
||||
const int32 patchVertexCount = patchEdgeVertexCount * patchEdgeVertexCount;
|
||||
|
||||
// Find size of heightmap in patches
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "Engine/Graphics/Textures/TextureData.h"
|
||||
#include "Engine/Graphics/PixelFormatExtensions.h"
|
||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||
#include "Engine/Core/Math/Color32.h"
|
||||
#include "Engine/Core/Config/GameSettings.h"
|
||||
#include "Engine/Core/Config/BuildSettings.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
@@ -19,498 +18,6 @@
|
||||
#if PLATFORM_MAC
|
||||
#include "Engine/Platform/Apple/ApplePlatformSettings.h"
|
||||
#endif
|
||||
#include <fstream>
|
||||
|
||||
#define MSDOS_SIGNATURE 0x5A4D
|
||||
#define PE_SIGNATURE 0x00004550
|
||||
#define PE_32BIT_SIGNATURE 0x10B
|
||||
#define PE_64BIT_SIGNATURE 0x20B
|
||||
#define PE_NUM_DIRECTORY_ENTRIES 16
|
||||
#define PE_SECTION_UNINITIALIZED_DATA 0x00000080
|
||||
#define PE_IMAGE_DIRECTORY_ENTRY_RESOURCE 2
|
||||
#define PE_IMAGE_RT_ICON 3
|
||||
|
||||
/// <summary>
|
||||
/// MS-DOS header found at the beginning in a PE format file.
|
||||
/// </summary>
|
||||
struct MSDOSHeader
|
||||
{
|
||||
uint16 signature;
|
||||
uint16 lastSize;
|
||||
uint16 numBlocks;
|
||||
uint16 numReloc;
|
||||
uint16 hdrSize;
|
||||
uint16 minAlloc;
|
||||
uint16 maxAlloc;
|
||||
uint16 ss;
|
||||
uint16 sp;
|
||||
uint16 checksum;
|
||||
uint16 ip;
|
||||
uint16 cs;
|
||||
uint16 relocPos;
|
||||
uint16 numOverlay;
|
||||
uint16 reserved1[4];
|
||||
uint16 oemId;
|
||||
uint16 oemInfo;
|
||||
uint16 reserved2[10];
|
||||
uint32 lfanew;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// COFF header found in a PE format file.
|
||||
/// </summary>
|
||||
struct COFFHeader
|
||||
{
|
||||
uint16 machine;
|
||||
uint16 numSections;
|
||||
uint32 timeDateStamp;
|
||||
uint32 ptrSymbolTable;
|
||||
uint32 numSymbols;
|
||||
uint16 sizeOptHeader;
|
||||
uint16 characteristics;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Contains address and size of data areas in a PE image.
|
||||
/// </summary>
|
||||
struct PEDataDirectory
|
||||
{
|
||||
uint32 virtualAddress;
|
||||
uint32 size;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Optional header in a 32-bit PE format file.
|
||||
/// </summary>
|
||||
struct PEOptionalHeader32
|
||||
{
|
||||
uint16 signature;
|
||||
uint8 majorLinkerVersion;
|
||||
uint8 minorLinkerVersion;
|
||||
uint32 sizeCode;
|
||||
uint32 sizeInitializedData;
|
||||
uint32 sizeUninitializedData;
|
||||
uint32 addressEntryPoint;
|
||||
uint32 baseCode;
|
||||
uint32 baseData;
|
||||
uint32 baseImage;
|
||||
uint32 alignmentSection;
|
||||
uint32 alignmentFile;
|
||||
uint16 majorOSVersion;
|
||||
uint16 minorOSVersion;
|
||||
uint16 majorImageVersion;
|
||||
uint16 minorImageVersion;
|
||||
uint16 majorSubsystemVersion;
|
||||
uint16 minorSubsystemVersion;
|
||||
uint32 reserved;
|
||||
uint32 sizeImage;
|
||||
uint32 sizeHeaders;
|
||||
uint32 checksum;
|
||||
uint16 subsystem;
|
||||
uint16 characteristics;
|
||||
uint32 sizeStackReserve;
|
||||
uint32 sizeStackCommit;
|
||||
uint32 sizeHeapReserve;
|
||||
uint32 sizeHeapCommit;
|
||||
uint32 loaderFlags;
|
||||
uint32 NumRvaAndSizes;
|
||||
PEDataDirectory dataDirectory[16];
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Optional header in a 64-bit PE format file.
|
||||
/// </summary>
|
||||
struct PEOptionalHeader64
|
||||
{
|
||||
uint16 signature;
|
||||
uint8 majorLinkerVersion;
|
||||
uint8 minorLinkerVersion;
|
||||
uint32 sizeCode;
|
||||
uint32 sizeInitializedData;
|
||||
uint32 sizeUninitializedData;
|
||||
uint32 addressEntryPoint;
|
||||
uint32 baseCode;
|
||||
uint64 baseImage;
|
||||
uint32 alignmentSection;
|
||||
uint32 alignmentFile;
|
||||
uint16 majorOSVersion;
|
||||
uint16 minorOSVersion;
|
||||
uint16 majorImageVersion;
|
||||
uint16 minorImageVersion;
|
||||
uint16 majorSubsystemVersion;
|
||||
uint16 minorSubsystemVersion;
|
||||
uint32 reserved;
|
||||
uint32 sizeImage;
|
||||
uint32 sizeHeaders;
|
||||
uint32 checksum;
|
||||
uint16 subsystem;
|
||||
uint16 characteristics;
|
||||
uint64 sizeStackReserve;
|
||||
uint64 sizeStackCommit;
|
||||
uint64 sizeHeapReserve;
|
||||
uint64 sizeHeapCommit;
|
||||
uint32 loaderFlags;
|
||||
uint32 NumRvaAndSizes;
|
||||
PEDataDirectory dataDirectory[16];
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A section header in a PE format file.
|
||||
/// </summary>
|
||||
struct PESectionHeader
|
||||
{
|
||||
char name[8];
|
||||
uint32 virtualSize;
|
||||
uint32 relativeVirtualAddress;
|
||||
uint32 physicalSize;
|
||||
uint32 physicalAddress;
|
||||
uint8 deprecated[12];
|
||||
uint32 flags;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A resource table header within a .rsrc section in a PE format file.
|
||||
/// </summary>
|
||||
struct PEImageResourceDirectory
|
||||
{
|
||||
uint32 flags;
|
||||
uint32 timeDateStamp;
|
||||
uint16 majorVersion;
|
||||
uint16 minorVersion;
|
||||
uint16 numNamedEntries;
|
||||
uint16 numIdEntries;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A single entry in a resource table within a .rsrc section in a PE format file.
|
||||
/// </summary>
|
||||
struct PEImageResourceEntry
|
||||
{
|
||||
uint32 type;
|
||||
uint32 offsetDirectory : 31;
|
||||
uint32 isDirectory : 1;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// An entry in a resource table referencing resource data. Found within a .rsrc section in a PE format file.
|
||||
/// </summary>
|
||||
struct PEImageResourceEntryData
|
||||
{
|
||||
uint32 offsetData;
|
||||
uint32 size;
|
||||
uint32 codePage;
|
||||
uint32 resourceHandle;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Header used in icon file format.
|
||||
/// </summary>
|
||||
struct IconHeader
|
||||
{
|
||||
uint32 size;
|
||||
int32 width;
|
||||
int32 height;
|
||||
uint16 planes;
|
||||
uint16 bitCount;
|
||||
uint32 compression;
|
||||
uint32 sizeImage;
|
||||
int32 xPelsPerMeter;
|
||||
int32 yPelsPerMeter;
|
||||
uint32 clrUsed;
|
||||
uint32 clrImportant;
|
||||
};
|
||||
|
||||
void UpdateIconData(uint8* iconData, const TextureData* icon)
|
||||
{
|
||||
IconHeader* iconHeader = (IconHeader*)iconData;
|
||||
|
||||
if (iconHeader->size != sizeof(IconHeader) || iconHeader->compression != 0 || iconHeader->planes != 1 || iconHeader->bitCount != 32)
|
||||
{
|
||||
// Unsupported format
|
||||
return;
|
||||
}
|
||||
|
||||
uint8* iconPixels = iconData + sizeof(IconHeader);
|
||||
uint32 width = iconHeader->width;
|
||||
uint32 height = iconHeader->height / 2;
|
||||
|
||||
// Check if can use mip from texture data or sample different mip
|
||||
uint32 iconTexSize;
|
||||
if (width != height)
|
||||
{
|
||||
// Only square icons are supported
|
||||
return;
|
||||
}
|
||||
if (Math::IsPowerOfTwo(width))
|
||||
{
|
||||
// Use mip
|
||||
iconTexSize = width;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use resized mip
|
||||
iconTexSize = Math::RoundUpToPowerOf2(width);
|
||||
}
|
||||
|
||||
// Try to pick a proper mip (require the same size)
|
||||
const TextureMipData* srcPixels = nullptr;
|
||||
int32 mipLevels = icon->GetMipLevels();
|
||||
for (int32 mipIndex = 0; mipIndex < mipLevels; mipIndex++)
|
||||
{
|
||||
uint32 iconWidth = Math::Max(1, icon->Width >> mipIndex);
|
||||
uint32 iconHeight = Math::Max(1, icon->Height >> mipIndex);
|
||||
|
||||
if (iconTexSize == iconWidth && iconTexSize == iconHeight)
|
||||
{
|
||||
srcPixels = icon->GetData(0, mipIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (srcPixels == nullptr)
|
||||
{
|
||||
// No icon of this size provided
|
||||
return;
|
||||
}
|
||||
|
||||
// Write colors
|
||||
uint32* colorData = (uint32*)iconPixels;
|
||||
|
||||
uint32 idx = 0;
|
||||
for (int32 y = (int32)height - 1; y >= 0; y--)
|
||||
{
|
||||
float v = (float)y / height;
|
||||
for (uint32 x = 0; x < width; x++)
|
||||
{
|
||||
float u = (float)x / width;
|
||||
|
||||
int32 i = (int32)(v * iconTexSize) * srcPixels->RowPitch + (int32)(u * iconTexSize) * sizeof(Color32);
|
||||
colorData[idx++] = ((Color32*)&srcPixels->Data.Get()[i])->GetAsBGRA();
|
||||
}
|
||||
}
|
||||
|
||||
// Write AND mask
|
||||
uint32 colorDataSize = width * height * sizeof(uint32);
|
||||
uint8* maskData = iconPixels + colorDataSize;
|
||||
|
||||
// One per bit in byte
|
||||
uint32 numPackedPixels = width / 8;
|
||||
|
||||
for (int32 y = (int32)height - 1; y >= 0; y--)
|
||||
{
|
||||
uint8 mask = 0;
|
||||
float v = (float)y / height;
|
||||
for (uint32 packedX = 0; packedX < numPackedPixels; packedX++)
|
||||
{
|
||||
for (uint32 pixelIdx = 0; pixelIdx < 8; pixelIdx++)
|
||||
{
|
||||
uint32 x = packedX * 8 + pixelIdx;
|
||||
float u = (float)x / width;
|
||||
int32 i = (int32)(v * iconTexSize) * srcPixels->RowPitch + (int32)(u * iconTexSize) * sizeof(Color32);
|
||||
Color32 color = *((Color32*)&srcPixels->Data.Get()[i]);
|
||||
if (color.A < 64)
|
||||
mask |= 1 << (7 - pixelIdx);
|
||||
}
|
||||
|
||||
*maskData = mask;
|
||||
maskData++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetIconData(PEImageResourceDirectory* base, PEImageResourceDirectory* current, uint8* imageData, uint32 sectionAddress, const TextureData* iconRGBA8)
|
||||
{
|
||||
uint32 numEntries = current->numIdEntries; // Not supporting name entries
|
||||
PEImageResourceEntry* entries = (PEImageResourceEntry*)(current + 1);
|
||||
|
||||
for (uint32 i = 0; i < numEntries; i++)
|
||||
{
|
||||
// Only at root does the type identify resource type
|
||||
if (base == current && entries[i].type != PE_IMAGE_RT_ICON)
|
||||
continue;
|
||||
|
||||
if (entries[i].isDirectory)
|
||||
{
|
||||
PEImageResourceDirectory* child = (PEImageResourceDirectory*)(((uint8*)base) + entries[i].offsetDirectory);
|
||||
SetIconData(base, child, imageData, sectionAddress, iconRGBA8);
|
||||
}
|
||||
else
|
||||
{
|
||||
PEImageResourceEntryData* data = (PEImageResourceEntryData*)(((uint8*)base) + entries[i].offsetDirectory);
|
||||
|
||||
uint8* iconData = imageData + (data->offsetData - sectionAddress);
|
||||
UpdateIconData(iconData, iconRGBA8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool EditorUtilities::UpdateExeIcon(const String& path, const TextureData& icon)
|
||||
{
|
||||
// Validate input
|
||||
if (!FileSystem::FileExists(path))
|
||||
{
|
||||
LOG(Warning, "Missing file");
|
||||
return true;
|
||||
}
|
||||
if (icon.Width < 1 || icon.Height < 1 || icon.GetMipLevels() <= 0)
|
||||
{
|
||||
LOG(Warning, "Inalid icon data");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert to RGBA8 format if need to
|
||||
const TextureData* iconRGBA8 = &icon;
|
||||
TextureData tmpData1;
|
||||
if (icon.Format != PixelFormat::R8G8B8A8_UNorm)
|
||||
{
|
||||
if (TextureTool::Convert(tmpData1, *iconRGBA8, PixelFormat::R8G8B8A8_UNorm))
|
||||
{
|
||||
LOG(Warning, "Failed convert icon data.");
|
||||
return true;
|
||||
}
|
||||
iconRGBA8 = &tmpData1;
|
||||
}
|
||||
|
||||
// Resize if need to
|
||||
TextureData tmpData2;
|
||||
if (iconRGBA8->Width > 256 || iconRGBA8->Height > 256)
|
||||
{
|
||||
if (TextureTool::Resize(tmpData2, *iconRGBA8, 256, 256))
|
||||
{
|
||||
LOG(Warning, "Failed resize icon data.");
|
||||
return true;
|
||||
}
|
||||
iconRGBA8 = &tmpData2;
|
||||
}
|
||||
|
||||
// A PE file is structured as such:
|
||||
// - MSDOS Header
|
||||
// - PE Signature
|
||||
// - COFF Header
|
||||
// - PE Optional Header
|
||||
// - One or multiple sections
|
||||
// - .code
|
||||
// - .data
|
||||
// - ...
|
||||
// - .rsrc
|
||||
// - icon/cursor/etc data
|
||||
|
||||
std::fstream stream;
|
||||
#if PLATFORM_WINDOWS
|
||||
stream.open(path.Get(), std::ios::in | std::ios::out | std::ios::binary);
|
||||
#else
|
||||
StringAsANSI<> pathAnsi(path.Get());
|
||||
stream.open(pathAnsi.Get(), std::ios::in | std::ios::out | std::ios::binary);
|
||||
#endif
|
||||
if (!stream.is_open())
|
||||
{
|
||||
LOG(Warning, "Cannot open file");
|
||||
return true;
|
||||
}
|
||||
|
||||
// First check magic number to ensure file is even an executable
|
||||
uint16 magicNum;
|
||||
stream.read((char*)&magicNum, sizeof(magicNum));
|
||||
if (magicNum != MSDOS_SIGNATURE)
|
||||
{
|
||||
LOG(Warning, "Provided file is not a valid executable.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read the MSDOS header and skip over it
|
||||
stream.seekg(0);
|
||||
|
||||
MSDOSHeader msdosHeader;
|
||||
stream.read((char*)&msdosHeader, sizeof(MSDOSHeader));
|
||||
|
||||
// Read PE signature
|
||||
stream.seekg(msdosHeader.lfanew);
|
||||
|
||||
uint32 peSignature;
|
||||
stream.read((char*)&peSignature, sizeof(peSignature));
|
||||
|
||||
if (peSignature != PE_SIGNATURE)
|
||||
{
|
||||
LOG(Warning, "Provided file is not in PE format.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read COFF header
|
||||
COFFHeader coffHeader;
|
||||
stream.read((char*)&coffHeader, sizeof(COFFHeader));
|
||||
|
||||
// .exe files always have an optional header
|
||||
if (coffHeader.sizeOptHeader == 0)
|
||||
{
|
||||
LOG(Warning, "Provided file is not a valid executable.");
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32 numSectionHeaders = coffHeader.numSections;
|
||||
|
||||
// Read optional header
|
||||
auto optionalHeaderPos = stream.tellg();
|
||||
|
||||
uint16 optionalHeaderSignature;
|
||||
stream.read((char*)&optionalHeaderSignature, sizeof(optionalHeaderSignature));
|
||||
|
||||
PEDataDirectory* dataDirectory = nullptr;
|
||||
stream.seekg(optionalHeaderPos);
|
||||
if (optionalHeaderSignature == PE_32BIT_SIGNATURE)
|
||||
{
|
||||
PEOptionalHeader32 optionalHeader;
|
||||
stream.read((char*)&optionalHeader, sizeof(optionalHeader));
|
||||
|
||||
dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
|
||||
}
|
||||
else if (optionalHeaderSignature == PE_64BIT_SIGNATURE)
|
||||
{
|
||||
PEOptionalHeader64 optionalHeader;
|
||||
stream.read((char*)&optionalHeader, sizeof(optionalHeader));
|
||||
|
||||
dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(Warning, "Unrecognized PE format.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read section headers
|
||||
auto sectionHeaderPos = optionalHeaderPos + (std::ifstream::pos_type)coffHeader.sizeOptHeader;
|
||||
stream.seekg(sectionHeaderPos);
|
||||
|
||||
Array<PESectionHeader> sectionHeaders;
|
||||
sectionHeaders.Resize(numSectionHeaders);
|
||||
stream.read((char*)sectionHeaders.Get(), sizeof(PESectionHeader) * numSectionHeaders);
|
||||
|
||||
// Look for .rsrc section header
|
||||
for (uint32 i = 0; i < numSectionHeaders; i++)
|
||||
{
|
||||
if (sectionHeaders[i].flags & PE_SECTION_UNINITIALIZED_DATA)
|
||||
continue;
|
||||
|
||||
if (strcmp(sectionHeaders[i].name, ".rsrc") == 0)
|
||||
{
|
||||
uint32 imageSize = sectionHeaders[i].physicalSize;
|
||||
Array<uint8> imageData;
|
||||
imageData.Resize(imageSize);
|
||||
|
||||
stream.seekg(sectionHeaders[i].physicalAddress);
|
||||
stream.read((char*)imageData.Get(), imageSize);
|
||||
|
||||
uint32 resourceDirOffset = dataDirectory->virtualAddress - sectionHeaders[i].relativeVirtualAddress;
|
||||
PEImageResourceDirectory* resourceDirectory = (PEImageResourceDirectory*)&imageData.Get()[resourceDirOffset];
|
||||
|
||||
SetIconData(resourceDirectory, resourceDirectory, imageData.Get(), sectionHeaders[i].relativeVirtualAddress, iconRGBA8);
|
||||
stream.seekp(sectionHeaders[i].physicalAddress);
|
||||
stream.write((char*)imageData.Get(), imageSize);
|
||||
}
|
||||
}
|
||||
|
||||
stream.close();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String EditorUtilities::GetOutputName()
|
||||
{
|
||||
|
||||
@@ -22,14 +22,6 @@ public:
|
||||
SplashScreen,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Win32 executable file icon.
|
||||
/// </summary>
|
||||
/// <param name="path">The exe path.</param>
|
||||
/// <param name="icon">The icon image data.</param>
|
||||
/// <returns>True if fails, otherwise false.</returns>
|
||||
static bool UpdateExeIcon(const String& path, const TextureData& icon);
|
||||
|
||||
static String GetOutputName();
|
||||
static bool FormatAppPackageName(String& packageName);
|
||||
static bool GetApplicationImage(const Guid& imageId, TextureData& imageData, ApplicationImageType type = ApplicationImageType::Icon);
|
||||
|
||||
@@ -140,8 +140,7 @@ namespace FlaxEditor.Utilities
|
||||
// Check if start the matching sequence
|
||||
if (matchStartPos == -1)
|
||||
{
|
||||
if (ranges == null)
|
||||
ranges = new List<Range>();
|
||||
ranges ??= new List<Range>();
|
||||
matchStartPos = textPos;
|
||||
}
|
||||
}
|
||||
@@ -152,7 +151,7 @@ namespace FlaxEditor.Utilities
|
||||
{
|
||||
var length = textPos - matchStartPos;
|
||||
if (length >= MinLength)
|
||||
ranges.Add(new Range(matchStartPos, length));
|
||||
ranges!.Add(new Range(matchStartPos, length));
|
||||
textPos = matchStartPos + length;
|
||||
matchStartPos = -1;
|
||||
}
|
||||
@@ -165,13 +164,13 @@ namespace FlaxEditor.Utilities
|
||||
{
|
||||
var length = endPos - matchStartPos;
|
||||
if (length >= MinLength)
|
||||
ranges.Add(new Range(matchStartPos, length));
|
||||
ranges!.Add(new Range(matchStartPos, length));
|
||||
textPos = matchStartPos + length;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if has any range
|
||||
if (ranges != null && ranges.Count > 0)
|
||||
if (ranges is { Count: > 0 })
|
||||
{
|
||||
matches = ranges.ToArray();
|
||||
return true;
|
||||
|
||||
@@ -174,7 +174,10 @@ namespace FlaxEditor.Viewport.Cameras
|
||||
}
|
||||
else
|
||||
{
|
||||
position = sphere.Center - Vector3.Forward * orientation * (sphere.Radius * 2.5f);
|
||||
// calculate the min. distance so that the sphere fits roughly 70% in FOV
|
||||
// clip to far plane as a disappearing big object might be confusing
|
||||
var distance = Mathf.Min(1.4f * sphere.Radius / Mathf.Tan(Mathf.DegreesToRadians * Viewport.FieldOfView / 2), Viewport.FarPlane);
|
||||
position = sphere.Center - Vector3.Forward * orientation * distance;
|
||||
}
|
||||
TargetPoint = sphere.Center;
|
||||
MoveViewport(position, orientation);
|
||||
|
||||
@@ -334,6 +334,22 @@ namespace FlaxEditor.Viewport
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bounding frustum of the current viewport camera.
|
||||
/// </summary>
|
||||
public BoundingFrustum ViewFrustum
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector3 viewOrigin = Task.View.Origin;
|
||||
Float3 position = ViewPosition - viewOrigin;
|
||||
CreateViewMatrix(position, out var view);
|
||||
CreateProjectionMatrix(out var projection);
|
||||
Matrix.Multiply(ref view, ref projection, out var viewProjection);
|
||||
return new BoundingFrustum(ref viewProjection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the yaw angle (in degrees).
|
||||
/// </summary>
|
||||
|
||||
@@ -234,7 +234,6 @@ namespace FlaxEditor.Viewport
|
||||
LocalOrientation = RootNode.RaycastNormalRotation(ref hitNormal),
|
||||
Name = item.ShortName
|
||||
};
|
||||
DebugDraw.DrawWireArrow(PostProcessSpawnedActorLocation(actor, ref hitNormal), actor.LocalOrientation, 1.0f, Color.Red, 1000000);
|
||||
Spawn(actor, ref hitLocation, ref hitNormal);
|
||||
}
|
||||
else if (hit is StaticModelNode staticModelNode)
|
||||
|
||||
@@ -154,10 +154,11 @@ namespace FlaxEditor.Windows.Assets
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct AnimGraphDebugFlowInfo
|
||||
private unsafe struct AnimGraphDebugFlowInfo
|
||||
{
|
||||
public uint NodeId;
|
||||
public int BoxId;
|
||||
public fixed uint NodePath[8];
|
||||
}
|
||||
|
||||
private FlaxObjectRefPickerControl _debugPicker;
|
||||
@@ -252,25 +253,26 @@ namespace FlaxEditor.Windows.Assets
|
||||
return obj is AnimatedModel player && player.AnimationGraph == OriginalAsset;
|
||||
}
|
||||
|
||||
private void OnDebugFlow(Asset asset, Object obj, uint nodeId, uint boxId)
|
||||
private unsafe void OnDebugFlow(Animations.DebugFlowInfo flowInfo)
|
||||
{
|
||||
// Filter the flow
|
||||
if (_debugPicker.Value != null)
|
||||
{
|
||||
if (asset != OriginalAsset || _debugPicker.Value != obj)
|
||||
if (flowInfo.Asset != OriginalAsset || _debugPicker.Value != flowInfo.Instance)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (asset != Asset || _preview.PreviewActor != obj)
|
||||
if (flowInfo.Asset != Asset || _preview.PreviewActor != flowInfo.Instance)
|
||||
return;
|
||||
}
|
||||
|
||||
// Register flow to show it in UI on a surface
|
||||
var flowInfo = new AnimGraphDebugFlowInfo { NodeId = nodeId, BoxId = (int)boxId };
|
||||
var flow = new AnimGraphDebugFlowInfo { NodeId = flowInfo.NodeId, BoxId = (int)flowInfo.BoxId };
|
||||
Utils.MemoryCopy(new IntPtr(flow.NodePath), new IntPtr(flowInfo.NodePath0), sizeof(uint) * 8ul);
|
||||
lock (_debugFlows)
|
||||
{
|
||||
_debugFlows.Add(flowInfo);
|
||||
_debugFlows.Add(flow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +396,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnUpdate()
|
||||
public override unsafe void OnUpdate()
|
||||
{
|
||||
// Extract animations playback state from the events tracing
|
||||
var debugActor = _debugPicker.Value as AnimatedModel;
|
||||
@@ -413,7 +415,8 @@ namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
foreach (var debugFlow in _debugFlows)
|
||||
{
|
||||
var node = Surface.Context.FindNode(debugFlow.NodeId);
|
||||
var context = Surface.FindContext(new Span<uint>(debugFlow.NodePath, 8));
|
||||
var node = context?.FindNode(debugFlow.NodeId);
|
||||
var box = node?.GetBox(debugFlow.BoxId);
|
||||
box?.HighlightConnections();
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
|
||||
// Use virtual animation graph to playback the animation
|
||||
_animGraph = FlaxEngine.Content.CreateVirtualAsset<AnimationGraph>();
|
||||
_animGraph.InitAsAnimation(model, _window.Asset);
|
||||
_animGraph.InitAsAnimation(model, _window.Asset, true, true);
|
||||
PreviewActor.AnimationGraph = _animGraph;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
Asset.WaitForLoaded();
|
||||
_textPreview.Font = new FontReference(Asset.CreateFont(30));
|
||||
_textPreview.Font = new FontReference(Asset, 30);
|
||||
_inputText.Text = string.Format("This is a sample text using font {0}.", Asset.FamilyName);
|
||||
var options = Asset.Options;
|
||||
_proxy.Set(ref options);
|
||||
|
||||
@@ -79,6 +79,13 @@ namespace FlaxEditor.Windows
|
||||
|
||||
if (item.HasDefaultThumbnail == false)
|
||||
{
|
||||
if (_view.SelectedCount > 1)
|
||||
cm.AddButton("Refresh thumbnails", () =>
|
||||
{
|
||||
foreach (var e in _view.Selection)
|
||||
e.RefreshThumbnail();
|
||||
});
|
||||
else
|
||||
cm.AddButton("Refresh thumbnail", item.RefreshThumbnail);
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,8 @@ namespace FlaxEditor.Windows
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (IsDisposing)
|
||||
return;
|
||||
OnExit();
|
||||
|
||||
// Unregister
|
||||
|
||||
@@ -485,6 +485,16 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
base.OnShowContextMenu(menu);
|
||||
|
||||
// Focus on play
|
||||
{
|
||||
var focus = menu.AddButton("Start Focused");
|
||||
focus.CloseMenuOnClick = false;
|
||||
var checkbox = new CheckBox(140, 2, FocusOnPlay) { Parent = focus };
|
||||
checkbox.StateChanged += state => FocusOnPlay = state.Checked;
|
||||
}
|
||||
|
||||
menu.AddSeparator();
|
||||
|
||||
// Viewport Brightness
|
||||
{
|
||||
var brightness = menu.AddButton("Viewport Brightness");
|
||||
|
||||
@@ -20,24 +20,20 @@ namespace FlaxEngine
|
||||
get
|
||||
{
|
||||
fixed (short* name = Name0)
|
||||
{
|
||||
return new string((char*)name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal unsafe bool NameStartsWith(string prefix)
|
||||
{
|
||||
fixed (short* name = Name0)
|
||||
{
|
||||
fixed (char* p = prefix)
|
||||
{
|
||||
return Utils.MemoryCompare(new IntPtr(name), new IntPtr(p), (ulong)(prefix.Length * 2)) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace FlaxEditor.Windows.Profiler
|
||||
|
||||
@@ -132,10 +132,13 @@ namespace FlaxEditor.Windows
|
||||
b = contextMenu.AddButton("Cut", inputOptions.Cut, Editor.SceneEditing.Cut);
|
||||
b.Enabled = canEditScene;
|
||||
|
||||
// Prefab options
|
||||
// Create option
|
||||
|
||||
contextMenu.AddSeparator();
|
||||
|
||||
b = contextMenu.AddButton("Create parent for selected actors", Editor.SceneEditing.CreateParentForSelectedActors);
|
||||
b.Enabled = canEditScene && hasSthSelected;
|
||||
|
||||
b = contextMenu.AddButton("Create Prefab", Editor.Prefabs.CreatePrefab);
|
||||
b.Enabled = isSingleActorSelected &&
|
||||
((ActorNode)Editor.SceneEditing.Selection[0]).CanCreatePrefab &&
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.GUI.Tree;
|
||||
@@ -14,7 +13,6 @@ using FlaxEditor.Scripting;
|
||||
using FlaxEditor.States;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
using static FlaxEditor.GUI.ItemsListContextMenu;
|
||||
|
||||
namespace FlaxEditor.Windows
|
||||
{
|
||||
@@ -35,6 +33,11 @@ namespace FlaxEditor.Windows
|
||||
private DragScriptItems _dragScriptItems;
|
||||
private DragHandlers _dragHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Scene tree panel.
|
||||
/// </summary>
|
||||
public Panel SceneTreePanel => _sceneTreePanel;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneTreeWindow"/> class.
|
||||
/// </summary>
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace FlaxEngine
|
||||
public string Path;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelectorAny"/> structure.
|
||||
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelector{T}"/> structure.
|
||||
/// </summary>
|
||||
/// <param name="path">The selector path.</param>
|
||||
public BehaviorKnowledgeSelector(string path)
|
||||
@@ -155,7 +155,7 @@ namespace FlaxEngine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelectorAny"/> structure.
|
||||
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelector{T}"/> structure.
|
||||
/// </summary>
|
||||
/// <param name="other">The other selector.</param>
|
||||
public BehaviorKnowledgeSelector(BehaviorKnowledgeSelectorAny other)
|
||||
|
||||
@@ -91,6 +91,13 @@ API_STRUCT(InBuild, Template, MarshalAs=StringAnsi) struct FLAXENGINE_API Behavi
|
||||
return false;
|
||||
}
|
||||
|
||||
BehaviorKnowledgeSelector() = default;
|
||||
|
||||
BehaviorKnowledgeSelector(const StringAnsi& other)
|
||||
{
|
||||
Path = other;
|
||||
}
|
||||
|
||||
BehaviorKnowledgeSelector& operator=(const StringAnsiView& other) noexcept
|
||||
{
|
||||
Path = other;
|
||||
|
||||
84
Source/Engine/Animations/AnimationData.cpp
Normal file
84
Source/Engine/Animations/AnimationData.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "AnimationData.h"
|
||||
|
||||
void NodeAnimationData::Evaluate(float time, Transform* result, bool loop) const
|
||||
{
|
||||
if (Position.GetKeyframes().HasItems())
|
||||
#if USE_LARGE_WORLDS
|
||||
{
|
||||
Float3 position;
|
||||
Position.Evaluate(position, time, loop);
|
||||
result->Translation = position;
|
||||
}
|
||||
#else
|
||||
Position.Evaluate(result->Translation, time, loop);
|
||||
#endif
|
||||
if (Rotation.GetKeyframes().HasItems())
|
||||
Rotation.Evaluate(result->Orientation, time, loop);
|
||||
if (Scale.GetKeyframes().HasItems())
|
||||
Scale.Evaluate(result->Scale, time, loop);
|
||||
}
|
||||
|
||||
void NodeAnimationData::EvaluateAll(float time, Transform* result, bool loop) const
|
||||
{
|
||||
Float3 position;
|
||||
Position.Evaluate(position, time, loop);
|
||||
result->Translation = position;
|
||||
Rotation.Evaluate(result->Orientation, time, loop);
|
||||
Scale.Evaluate(result->Scale, time, loop);
|
||||
}
|
||||
|
||||
int32 NodeAnimationData::GetKeyframesCount() const
|
||||
{
|
||||
return Position.GetKeyframes().Count() + Rotation.GetKeyframes().Count() + Scale.GetKeyframes().Count();
|
||||
}
|
||||
|
||||
uint64 NodeAnimationData::GetMemoryUsage() const
|
||||
{
|
||||
return NodeName.Length() * sizeof(Char) + Position.GetMemoryUsage() + Rotation.GetMemoryUsage() + Scale.GetMemoryUsage();
|
||||
}
|
||||
|
||||
uint64 AnimationData::GetMemoryUsage() const
|
||||
{
|
||||
uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
|
||||
for (const auto& e : Channels)
|
||||
result += e.GetMemoryUsage();
|
||||
return result;
|
||||
}
|
||||
|
||||
int32 AnimationData::GetKeyframesCount() const
|
||||
{
|
||||
int32 result = 0;
|
||||
for (int32 i = 0; i < Channels.Count(); i++)
|
||||
result += Channels[i].GetKeyframesCount();
|
||||
return result;
|
||||
}
|
||||
|
||||
NodeAnimationData* AnimationData::GetChannel(const StringView& name)
|
||||
{
|
||||
for (auto& e : Channels)
|
||||
if (e.NodeName == name)
|
||||
return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AnimationData::Swap(AnimationData& other)
|
||||
{
|
||||
::Swap(Duration, other.Duration);
|
||||
::Swap(FramesPerSecond, other.FramesPerSecond);
|
||||
::Swap(RootMotionFlags, other.RootMotionFlags);
|
||||
::Swap(Name, other.Name);
|
||||
::Swap(RootNodeName, other.RootNodeName);
|
||||
Channels.Swap(other.Channels);
|
||||
}
|
||||
|
||||
void AnimationData::Dispose()
|
||||
{
|
||||
Name.Clear();
|
||||
Duration = 0.0;
|
||||
FramesPerSecond = 0.0;
|
||||
RootNodeName.Clear();
|
||||
RootMotionFlags = AnimationRootMotionFlags::None;
|
||||
Channels.Resize(0);
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Core/Types/String.h"
|
||||
#include "Engine/Animations/Curve.h"
|
||||
#include "Engine/Core/Math/Transform.h"
|
||||
#include "Engine/Animations/Curve.h"
|
||||
|
||||
/// <summary>
|
||||
/// Single node animation data container.
|
||||
@@ -50,19 +50,7 @@ public:
|
||||
/// <param name="time">The time to evaluate the curves at.</param>
|
||||
/// <param name="result">The interpolated value from the curve at provided time.</param>
|
||||
/// <param name="loop">If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.</param>
|
||||
void Evaluate(float time, Transform* result, bool loop = true) const
|
||||
{
|
||||
if (Position.GetKeyframes().HasItems())
|
||||
{
|
||||
Float3 position;
|
||||
Position.Evaluate(position, time, loop);
|
||||
result->Translation = position;
|
||||
}
|
||||
if (Rotation.GetKeyframes().HasItems())
|
||||
Rotation.Evaluate(result->Orientation, time, loop);
|
||||
if (Scale.GetKeyframes().HasItems())
|
||||
Scale.Evaluate(result->Scale, time, loop);
|
||||
}
|
||||
void Evaluate(float time, Transform* result, bool loop = true) const;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the animation transformation at the specified time.
|
||||
@@ -70,29 +58,37 @@ public:
|
||||
/// <param name="time">The time to evaluate the curves at.</param>
|
||||
/// <param name="result">The interpolated value from the curve at provided time.</param>
|
||||
/// <param name="loop">If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.</param>
|
||||
void EvaluateAll(float time, Transform* result, bool loop = true) const
|
||||
{
|
||||
Float3 position;
|
||||
Position.Evaluate(position, time, loop);
|
||||
result->Translation = position;
|
||||
Rotation.Evaluate(result->Orientation, time, loop);
|
||||
Scale.Evaluate(result->Scale, time, loop);
|
||||
}
|
||||
void EvaluateAll(float time, Transform* result, bool loop = true) const;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total amount of keyframes in the animation curves.
|
||||
/// </summary>
|
||||
int32 GetKeyframesCount() const
|
||||
{
|
||||
return Position.GetKeyframes().Count() + Rotation.GetKeyframes().Count() + Scale.GetKeyframes().Count();
|
||||
}
|
||||
int32 GetKeyframesCount() const;
|
||||
|
||||
uint64 GetMemoryUsage() const
|
||||
{
|
||||
return NodeName.Length() * sizeof(Char) + Position.GetMemoryUsage() + Rotation.GetMemoryUsage() + Scale.GetMemoryUsage();
|
||||
}
|
||||
uint64 GetMemoryUsage() const;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Root Motion modes that can be applied by the animation. Used as flags for selective behavior.
|
||||
/// </summary>
|
||||
API_ENUM(Attributes="Flags") enum class AnimationRootMotionFlags : byte
|
||||
{
|
||||
// No root motion.
|
||||
None = 0,
|
||||
// Root node position along XZ plane. Applies horizontal movement. Good for stationary animations (eg. idle).
|
||||
RootPositionXZ = 1 << 0,
|
||||
// Root node position along Y axis (up). Applies vertical movement. Good for all 'grounded' animations unless jumping is handled from code.
|
||||
RootPositionY = 1 << 1,
|
||||
// Root node rotation. Applies orientation changes. Good for animations that have baked-in root rotation (eg. turn animations).
|
||||
RootRotation = 1 << 2,
|
||||
// Root node position.
|
||||
RootPosition = RootPositionXZ | RootPositionY,
|
||||
// Root node position and rotation.
|
||||
RootTransform = RootPosition | RootRotation,
|
||||
};
|
||||
|
||||
DECLARE_ENUM_OPERATORS(AnimationRootMotionFlags);
|
||||
|
||||
/// <summary>
|
||||
/// Skeleton nodes animation data container. Includes metadata about animation sampling, duration and node animations curves.
|
||||
/// </summary>
|
||||
@@ -111,7 +107,7 @@ struct AnimationData
|
||||
/// <summary>
|
||||
/// Enables root motion extraction support from this animation.
|
||||
/// </summary>
|
||||
bool EnableRootMotion = false;
|
||||
AnimationRootMotionFlags RootMotionFlags = AnimationRootMotionFlags::None;
|
||||
|
||||
/// <summary>
|
||||
/// The animation name.
|
||||
@@ -140,49 +136,23 @@ public:
|
||||
return static_cast<float>(Duration / FramesPerSecond);
|
||||
}
|
||||
|
||||
uint64 GetMemoryUsage() const
|
||||
{
|
||||
uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
|
||||
for (const auto& e : Channels)
|
||||
result += e.GetMemoryUsage();
|
||||
return result;
|
||||
}
|
||||
uint64 GetMemoryUsage() const;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total amount of keyframes in the all animation channels.
|
||||
/// </summary>
|
||||
int32 GetKeyframesCount() const
|
||||
{
|
||||
int32 result = 0;
|
||||
for (int32 i = 0; i < Channels.Count(); i++)
|
||||
result += Channels[i].GetKeyframesCount();
|
||||
return result;
|
||||
}
|
||||
int32 GetKeyframesCount() const;
|
||||
|
||||
NodeAnimationData* GetChannel(const StringView& name);
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange.
|
||||
/// </summary>
|
||||
/// <param name="other">The other object.</param>
|
||||
void Swap(AnimationData& other)
|
||||
{
|
||||
::Swap(Duration, other.Duration);
|
||||
::Swap(FramesPerSecond, other.FramesPerSecond);
|
||||
::Swap(EnableRootMotion, other.EnableRootMotion);
|
||||
::Swap(Name, other.Name);
|
||||
::Swap(RootNodeName, other.RootNodeName);
|
||||
Channels.Swap(other.Channels);
|
||||
}
|
||||
void Swap(AnimationData& other);
|
||||
|
||||
/// <summary>
|
||||
/// Releases data.
|
||||
/// </summary>
|
||||
void Dispose()
|
||||
{
|
||||
Name.Clear();
|
||||
Duration = 0.0;
|
||||
FramesPerSecond = 0.0;
|
||||
RootNodeName.Clear();
|
||||
EnableRootMotion = false;
|
||||
Channels.Resize(0);
|
||||
}
|
||||
void Dispose();
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace
|
||||
AnimationsService AnimationManagerInstance;
|
||||
TaskGraphSystem* Animations::System = nullptr;
|
||||
#if USE_EDITOR
|
||||
Delegate<Asset*, ScriptingObject*, uint32, uint32> Animations::DebugFlow;
|
||||
Delegate<Animations::DebugFlowInfo> Animations::DebugFlow;
|
||||
#endif
|
||||
|
||||
AnimEvent::AnimEvent(const SpawnParams& params)
|
||||
@@ -127,7 +127,7 @@ void AnimationsSystem::Execute(TaskGraph* graph)
|
||||
#if USE_EDITOR
|
||||
// If debug flow is registered, then warm it up (eg. static cached method inside DebugFlow_ManagedWrapper) so it doesn't crash on highly multi-threaded code
|
||||
if (Animations::DebugFlow.IsBinded())
|
||||
Animations::DebugFlow(nullptr, nullptr, 0, 0);
|
||||
Animations::DebugFlow(Animations::DebugFlowInfo());
|
||||
#endif
|
||||
|
||||
// Schedule work to update all animated models in async
|
||||
|
||||
@@ -22,8 +22,25 @@ API_CLASS(Static) class FLAXENGINE_API Animations
|
||||
API_FIELD(ReadOnly) static TaskGraphSystem* System;
|
||||
|
||||
#if USE_EDITOR
|
||||
// Custom event that is called every time the Anim Graph signal flows over the graph (including the data connections). Can be used to read and visualize the animation blending logic. Args are: anim graph asset, animated object, node id, box id
|
||||
API_EVENT() static Delegate<Asset*, ScriptingObject*, uint32, uint32> DebugFlow;
|
||||
// Data wrapper for the debug flow information.
|
||||
API_STRUCT(NoDefault) struct DebugFlowInfo
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(DebugFlowInfo);
|
||||
|
||||
// Anim Graph asset
|
||||
API_FIELD() Asset* Asset = nullptr;
|
||||
// Animated actor
|
||||
API_FIELD() ScriptingObject* Instance = nullptr;
|
||||
// Graph node id.
|
||||
API_FIELD() uint32 NodeId = 0;
|
||||
// Graph box id.
|
||||
API_FIELD() uint32 BoxId = 0;
|
||||
// Ids of graph nodes (call of hierarchy).
|
||||
API_FIELD(Internal, NoArray) uint32 NodePath[8] = {};
|
||||
};
|
||||
|
||||
// Custom event that is called every time the Anim Graph signal flows over the graph (including the data connections). Can be used to read and visualize the animation blending logic.
|
||||
API_EVENT() static Delegate<DebugFlowInfo> DebugFlow;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -730,7 +730,7 @@ public:
|
||||
void TransformTime(float timeScale, float timeOffset)
|
||||
{
|
||||
for (int32 i = 0; i < _keyframes.Count(); i++)
|
||||
_keyframes[i].Time = _keyframes[i].Time * timeScale + timeOffset;;
|
||||
_keyframes[i].Time = _keyframes[i].Time * timeScale + timeOffset;
|
||||
}
|
||||
|
||||
uint64 GetMemoryUsage() const
|
||||
|
||||
@@ -157,7 +157,7 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
|
||||
if (_rootNode->Values.Count() < 1)
|
||||
{
|
||||
_rootNode->Values.Resize(1);
|
||||
_rootNode->Values[0] = (int32)RootMotionMode::NoExtraction;
|
||||
_rootNode->Values[0] = (int32)RootMotionExtraction::NoExtraction;
|
||||
}
|
||||
break;
|
||||
// Animation
|
||||
|
||||
@@ -89,7 +89,7 @@ void AnimGraphExecutor::initRuntime()
|
||||
void AnimGraphExecutor::ProcessGroupCustom(Box* boxBase, Node* nodeBase, Value& value)
|
||||
{
|
||||
#if USE_CSHARP
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
if (context.ValueCache.TryGet(boxBase, value))
|
||||
return;
|
||||
auto box = (AnimGraphBox*)boxBase;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
extern void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 i);
|
||||
|
||||
ThreadLocal<AnimGraphContext> AnimGraphExecutor::Context;
|
||||
ThreadLocal<AnimGraphContext*> AnimGraphExecutor::Context;
|
||||
|
||||
Transform AnimGraphImpulse::GetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex) const
|
||||
{
|
||||
@@ -104,7 +104,7 @@ AnimGraphInstanceData::OutgoingEvent AnimGraphInstanceData::ActiveEvent::End(Ani
|
||||
|
||||
AnimGraphImpulse* AnimGraphNode::GetNodes(AnimGraphExecutor* executor)
|
||||
{
|
||||
auto& context = AnimGraphExecutor::Context.Get();
|
||||
auto& context = *AnimGraphExecutor::Context.Get();
|
||||
const int32 count = executor->_skeletonNodesCount;
|
||||
if (context.PoseCacheSize == context.PoseCache.Count())
|
||||
context.PoseCache.AddOne();
|
||||
@@ -204,19 +204,24 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
|
||||
|
||||
// Initialize
|
||||
auto& skeleton = _graph.BaseModel->Skeleton;
|
||||
auto& context = Context.Get();
|
||||
auto& contextPtr = Context.Get();
|
||||
if (!contextPtr)
|
||||
contextPtr = New<AnimGraphContext>();
|
||||
auto& context = *contextPtr;
|
||||
{
|
||||
ANIM_GRAPH_PROFILE_EVENT("Init");
|
||||
|
||||
// Init data from base model
|
||||
_skeletonNodesCount = skeleton.Nodes.Count();
|
||||
_rootMotionMode = (RootMotionMode)(int32)_graph._rootNode->Values[0];
|
||||
_rootMotionMode = (RootMotionExtraction)(int32)_graph._rootNode->Values[0];
|
||||
|
||||
// Prepare context data for the evaluation
|
||||
context.GraphStack.Clear();
|
||||
context.GraphStack.Push((Graph*)&_graph);
|
||||
context.NodePath.Clear();
|
||||
context.Data = &data;
|
||||
context.DeltaTime = dt;
|
||||
context.StackOverFlow = false;
|
||||
context.CurrentFrameIndex = ++data.CurrentFrame;
|
||||
context.CallStack.Clear();
|
||||
context.Functions.Clear();
|
||||
@@ -377,12 +382,12 @@ void AnimGraphExecutor::GetInputValue(Box* box, Value& result)
|
||||
|
||||
AnimGraphImpulse* AnimGraphExecutor::GetEmptyNodes()
|
||||
{
|
||||
return &Context.Get().EmptyNodes;
|
||||
return &Context.Get()->EmptyNodes;
|
||||
}
|
||||
|
||||
void AnimGraphExecutor::InitNodes(AnimGraphImpulse* nodes) const
|
||||
{
|
||||
const auto& emptyNodes = Context.Get().EmptyNodes;
|
||||
const auto& emptyNodes = Context.Get()->EmptyNodes;
|
||||
Platform::MemoryCopy(nodes->Nodes.Get(), emptyNodes.Nodes.Get(), sizeof(Transform) * _skeletonNodesCount);
|
||||
nodes->RootMotion = emptyNodes.RootMotion;
|
||||
nodes->Position = emptyNodes.Position;
|
||||
@@ -404,12 +409,15 @@ void AnimGraphExecutor::ResetBuckets(AnimGraphContext& context, AnimGraphBase* g
|
||||
|
||||
VisjectExecutor::Value AnimGraphExecutor::eatBox(Node* caller, Box* box)
|
||||
{
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
|
||||
// Check if graph is looped or is too deep
|
||||
if (context.StackOverFlow)
|
||||
return Value::Zero;
|
||||
if (context.CallStack.Count() >= ANIM_GRAPH_MAX_CALL_STACK)
|
||||
{
|
||||
OnError(caller, box, TEXT("Graph is looped or too deep!"));
|
||||
context.StackOverFlow = true;
|
||||
return Value::Zero;
|
||||
}
|
||||
#if !BUILD_RELEASE
|
||||
@@ -424,7 +432,15 @@ VisjectExecutor::Value AnimGraphExecutor::eatBox(Node* caller, Box* box)
|
||||
context.CallStack.Add(caller);
|
||||
|
||||
#if USE_EDITOR
|
||||
Animations::DebugFlow(_graph._owner, context.Data->Object, box->GetParent<Node>()->ID, box->ID);
|
||||
Animations::DebugFlowInfo flowInfo;
|
||||
flowInfo.Asset = _graph._owner;
|
||||
flowInfo.Instance = context.Data->Object;
|
||||
flowInfo.NodeId = box->GetParent<Node>()->ID;
|
||||
flowInfo.BoxId = box->ID;
|
||||
const auto* nodePath = context.NodePath.Get();
|
||||
for (int32 i = 0; i < context.NodePath.Count(); i++)
|
||||
flowInfo.NodePath[i] = nodePath[i];
|
||||
Animations::DebugFlow(flowInfo);
|
||||
#endif
|
||||
|
||||
// Call per group custom processing event
|
||||
@@ -441,6 +457,6 @@ VisjectExecutor::Value AnimGraphExecutor::eatBox(Node* caller, Box* box)
|
||||
|
||||
VisjectExecutor::Graph* AnimGraphExecutor::GetCurrentGraph() const
|
||||
{
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
return context.GraphStack.Peek();
|
||||
}
|
||||
|
||||
@@ -92,9 +92,9 @@ enum class BoneTransformMode
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The animated model root motion mode.
|
||||
/// The animated model root motion extraction modes.
|
||||
/// </summary>
|
||||
enum class RootMotionMode
|
||||
enum class RootMotionExtraction
|
||||
{
|
||||
/// <summary>
|
||||
/// Don't extract nor apply the root motion.
|
||||
@@ -205,7 +205,7 @@ struct FLAXENGINE_API AnimGraphSlot
|
||||
/// <summary>
|
||||
/// The animation graph state container for a single node playback trace (eg. animation sample info or state transition). Can be used by Anim Graph debugging or custom scripting.
|
||||
/// </summary>
|
||||
API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent
|
||||
API_STRUCT(NoDefault) struct FLAXENGINE_API AnimGraphTraceEvent
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(AnimGraphTraceEvent);
|
||||
|
||||
@@ -215,6 +215,8 @@ API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent
|
||||
API_FIELD() float Value = 0;
|
||||
// Identifier of the node in the graph.
|
||||
API_FIELD() uint32 NodeId = 0;
|
||||
// Ids of graph nodes (call of hierarchy).
|
||||
API_FIELD(Internal, NoArray) uint32 NodePath[8] = {};
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -794,12 +796,16 @@ struct AnimGraphContext
|
||||
AnimGraphInstanceData* Data;
|
||||
AnimGraphImpulse EmptyNodes;
|
||||
AnimGraphTransitionData TransitionData;
|
||||
bool StackOverFlow;
|
||||
Array<VisjectExecutor::Node*, FixedAllocation<ANIM_GRAPH_MAX_CALL_STACK>> CallStack;
|
||||
Array<VisjectExecutor::Graph*, FixedAllocation<32>> GraphStack;
|
||||
Array<uint32, FixedAllocation<ANIM_GRAPH_MAX_CALL_STACK> > NodePath;
|
||||
Dictionary<VisjectExecutor::Node*, VisjectExecutor::Graph*> Functions;
|
||||
ChunkedArray<AnimGraphImpulse, 256> PoseCache;
|
||||
int32 PoseCacheSize;
|
||||
Dictionary<VisjectExecutor::Box*, Variant> ValueCache;
|
||||
|
||||
AnimGraphTraceEvent& AddTraceEvent(const AnimGraphNode* node);
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -810,11 +816,11 @@ class AnimGraphExecutor : public VisjectExecutor
|
||||
friend AnimGraphNode;
|
||||
private:
|
||||
AnimGraph& _graph;
|
||||
RootMotionMode _rootMotionMode = RootMotionMode::NoExtraction;
|
||||
RootMotionExtraction _rootMotionMode = RootMotionExtraction::NoExtraction;
|
||||
int32 _skeletonNodesCount = 0;
|
||||
|
||||
// Per-thread context to allow async execution
|
||||
static ThreadLocal<AnimGraphContext> Context;
|
||||
static ThreadLocal<AnimGraphContext*> Context;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
@@ -891,7 +897,7 @@ private:
|
||||
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, float speedA, float speedB, float alpha);
|
||||
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC);
|
||||
Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode);
|
||||
Variant SampleState(AnimGraphNode* state);
|
||||
Variant SampleState(AnimGraphContext& context, const AnimGraphNode* state);
|
||||
void InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition = nullptr);
|
||||
AnimGraphStateTransition* UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState = nullptr);
|
||||
void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData);
|
||||
|
||||
@@ -21,13 +21,13 @@ namespace
|
||||
base += additive;
|
||||
}
|
||||
|
||||
FORCE_INLINE void NormalizeRotations(AnimGraphImpulse* nodes, RootMotionMode rootMotionMode)
|
||||
FORCE_INLINE void NormalizeRotations(AnimGraphImpulse* nodes, RootMotionExtraction rootMotionMode)
|
||||
{
|
||||
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
|
||||
{
|
||||
nodes->Nodes[i].Orientation.Normalize();
|
||||
}
|
||||
if (rootMotionMode != RootMotionMode::NoExtraction)
|
||||
if (rootMotionMode != RootMotionExtraction::NoExtraction)
|
||||
{
|
||||
nodes->RootMotion.Orientation.Normalize();
|
||||
}
|
||||
@@ -52,6 +52,17 @@ void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData
|
||||
node = value;
|
||||
}
|
||||
|
||||
AnimGraphTraceEvent& AnimGraphContext::AddTraceEvent(const AnimGraphNode* node)
|
||||
{
|
||||
auto& trace = Data->TraceEvents.AddOne();
|
||||
trace.Value = 0.0f;
|
||||
trace.NodeId = node->ID;
|
||||
const auto* nodePath = NodePath.Get();
|
||||
for (int32 i = 0; i < NodePath.Count(); i++)
|
||||
trace.NodePath[i] = nodePath[i];
|
||||
return trace;
|
||||
}
|
||||
|
||||
int32 AnimGraphExecutor::GetRootNodeIndex(Animation* anim)
|
||||
{
|
||||
// TODO: cache the root node index (use dictionary with Animation* -> int32 for fast lookups)
|
||||
@@ -76,7 +87,7 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
|
||||
if (anim->Events.Count() == 0)
|
||||
return;
|
||||
ANIM_GRAPH_PROFILE_EVENT("Events");
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
float eventTimeMin = animPrevPos;
|
||||
float eventTimeMax = animPos;
|
||||
if (loop && context.DeltaTime * speed < 0)
|
||||
@@ -220,13 +231,12 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed);
|
||||
|
||||
// Add to trace
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
if (context.Data->EnableTracing)
|
||||
{
|
||||
auto& trace = context.Data->TraceEvents.AddOne();
|
||||
auto& trace = context.AddTraceEvent(node);
|
||||
trace.Asset = anim;
|
||||
trace.Value = animPos;
|
||||
trace.NodeId = node->ID;
|
||||
}
|
||||
|
||||
// Evaluate nested animations
|
||||
@@ -313,16 +323,21 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
}
|
||||
|
||||
// Handle root motion
|
||||
if (_rootMotionMode != RootMotionMode::NoExtraction && anim->Data.EnableRootMotion)
|
||||
if (_rootMotionMode != RootMotionExtraction::NoExtraction && anim->Data.RootMotionFlags != AnimationRootMotionFlags::None)
|
||||
{
|
||||
// Calculate the root motion node transformation
|
||||
const bool motionPositionXZ = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionXZ);
|
||||
const bool motionPositionY = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionY);
|
||||
const bool motionRotation = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootRotation);
|
||||
const Vector3 motionPositionMask(motionPositionXZ ? 1.0f : 0.0f, motionPositionY ? 1.0f : 0.0f, motionPositionXZ ? 1.0f : 0.0f);
|
||||
const bool motionPosition = motionPositionXZ | motionPositionY;
|
||||
const int32 rootNodeIndex = GetRootNodeIndex(anim);
|
||||
const Transform& refPose = emptyNodes->Nodes[rootNodeIndex];
|
||||
Transform& rootNode = nodes->Nodes[rootNodeIndex];
|
||||
Transform& dstNode = nodes->RootMotion;
|
||||
Transform srcNode = Transform::Identity;
|
||||
const int32 nodeToChannel = mapping.NodesMapping[rootNodeIndex];
|
||||
if (_rootMotionMode == RootMotionMode::Enable && nodeToChannel != -1)
|
||||
if (_rootMotionMode == RootMotionExtraction::Enable && nodeToChannel != -1)
|
||||
{
|
||||
// Get the root bone transformation
|
||||
Transform rootBefore = refPose;
|
||||
@@ -346,7 +361,9 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
// Complex motion calculation to preserve the looped movement
|
||||
// (end - before + now - begin)
|
||||
// It sums the motion since the last update to anim end and since the start to now
|
||||
srcNode.Translation = rootEnd.Translation - rootBefore.Translation + rootNode.Translation - rootBegin.Translation;
|
||||
if (motionPosition)
|
||||
srcNode.Translation = (rootEnd.Translation - rootBefore.Translation + rootNode.Translation - rootBegin.Translation) * motionPositionMask;
|
||||
if (motionRotation)
|
||||
srcNode.Orientation = rootEnd.Orientation * rootBefore.Orientation.Conjugated() * (rootNode.Orientation * rootBegin.Orientation.Conjugated());
|
||||
//srcNode.Orientation = Quaternion::Identity;
|
||||
}
|
||||
@@ -354,7 +371,9 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
{
|
||||
// Simple motion delta
|
||||
// (now - before)
|
||||
srcNode.Translation = rootNode.Translation - rootBefore.Translation;
|
||||
if (motionPosition)
|
||||
srcNode.Translation = (rootNode.Translation - rootBefore.Translation) * motionPositionMask;
|
||||
if (motionRotation)
|
||||
srcNode.Orientation = rootBefore.Orientation.Conjugated() * rootNode.Orientation;
|
||||
}
|
||||
|
||||
@@ -369,28 +388,40 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
}
|
||||
}
|
||||
|
||||
// Remove root node motion after extraction
|
||||
rootNode = refPose;
|
||||
// Remove root node motion after extraction (only extracted components)
|
||||
if (motionPosition)
|
||||
rootNode.Translation = refPose.Translation * motionPositionMask + rootNode.Translation * (Vector3::One - motionPositionMask);
|
||||
if (motionRotation)
|
||||
rootNode.Orientation = refPose.Orientation;
|
||||
|
||||
// Blend root motion
|
||||
if (mode == ProcessAnimationMode::BlendAdditive)
|
||||
{
|
||||
dstNode.Translation += srcNode.Translation * weight;
|
||||
if (motionPosition)
|
||||
dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
|
||||
if (motionRotation)
|
||||
BlendAdditiveWeightedRotation(dstNode.Orientation, srcNode.Orientation, weight);
|
||||
}
|
||||
else if (mode == ProcessAnimationMode::Add)
|
||||
{
|
||||
dstNode.Translation += srcNode.Translation * weight;
|
||||
if (motionPosition)
|
||||
dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
|
||||
if (motionRotation)
|
||||
dstNode.Orientation += srcNode.Orientation * weight;
|
||||
}
|
||||
else if (weighted)
|
||||
{
|
||||
dstNode.Translation = srcNode.Translation * weight;
|
||||
if (motionPosition)
|
||||
dstNode.Translation = srcNode.Translation * weight * motionPositionMask;
|
||||
if (motionRotation)
|
||||
dstNode.Orientation = srcNode.Orientation * weight;
|
||||
}
|
||||
else
|
||||
{
|
||||
dstNode = srcNode;
|
||||
if (motionPosition)
|
||||
dstNode.Translation = srcNode.Translation * motionPositionMask;
|
||||
if (motionRotation)
|
||||
dstNode.Orientation = srcNode.Orientation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,19 +525,30 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
|
||||
return nodes;
|
||||
}
|
||||
|
||||
Variant AnimGraphExecutor::SampleState(AnimGraphNode* state)
|
||||
Variant AnimGraphExecutor::SampleState(AnimGraphContext& context, const AnimGraphNode* state)
|
||||
{
|
||||
auto& data = state->Data.State;
|
||||
if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr)
|
||||
return Value::Null;
|
||||
|
||||
// Add to trace
|
||||
if (context.Data->EnableTracing)
|
||||
{
|
||||
auto& trace = context.AddTraceEvent(state);
|
||||
}
|
||||
|
||||
ANIM_GRAPH_PROFILE_EVENT("Evaluate State");
|
||||
context.NodePath.Add(state->ID);
|
||||
auto rootNode = data.Graph->GetRootNode();
|
||||
return eatBox((Node*)rootNode, &rootNode->Boxes[0]);
|
||||
auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]);
|
||||
context.NodePath.Pop();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition)
|
||||
{
|
||||
// Reset transiton
|
||||
// Reset transition
|
||||
stateMachineBucket.ActiveTransition = transition;
|
||||
stateMachineBucket.TransitionPosition = 0.0f;
|
||||
|
||||
@@ -537,7 +579,7 @@ AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphCon
|
||||
}
|
||||
|
||||
// Evaluate source state transition data (position, length, etc.)
|
||||
const Value sourceStatePtr = SampleState(state);
|
||||
const Value sourceStatePtr = SampleState(context, state);
|
||||
auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule?
|
||||
if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr))
|
||||
{
|
||||
@@ -634,7 +676,7 @@ void ComputeMultiBlendLength(float& length, AnimGraphNode* node)
|
||||
|
||||
void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value)
|
||||
{
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
switch (node->TypeID)
|
||||
{
|
||||
// Get
|
||||
@@ -745,7 +787,7 @@ void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& valu
|
||||
|
||||
void AnimGraphExecutor::ProcessGroupTools(Box* box, Node* nodeBase, Value& value)
|
||||
{
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
auto node = (AnimGraphNode*)nodeBase;
|
||||
switch (node->TypeID)
|
||||
{
|
||||
@@ -769,7 +811,7 @@ void AnimGraphExecutor::ProcessGroupTools(Box* box, Node* nodeBase, Value& value
|
||||
|
||||
void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Value& value)
|
||||
{
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
if (context.ValueCache.TryGet(boxBase, value))
|
||||
return;
|
||||
auto box = (AnimGraphBox*)boxBase;
|
||||
@@ -1660,6 +1702,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
|
||||
bucket.CurrentState = bucket.ActiveTransition->Destination; \
|
||||
InitStateTransition(context, bucket)
|
||||
|
||||
context.NodePath.Push(node->ID);
|
||||
|
||||
// Update the active transition
|
||||
if (bucket.ActiveTransition)
|
||||
{
|
||||
@@ -1767,11 +1811,11 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
|
||||
if (bucket.BaseTransitionState)
|
||||
{
|
||||
// Sample the other state (eg. when blending from interrupted state to the another state from the old destination)
|
||||
value = SampleState(bucket.BaseTransitionState);
|
||||
value = SampleState(context, bucket.BaseTransitionState);
|
||||
if (bucket.BaseTransition)
|
||||
{
|
||||
// Evaluate the base pose from the time when transition was interrupted
|
||||
const auto destinationState = SampleState(bucket.BaseTransition->Destination);
|
||||
const auto destinationState = SampleState(context, bucket.BaseTransition->Destination);
|
||||
const float alpha = bucket.BaseTransitionPosition / bucket.BaseTransition->BlendDuration;
|
||||
value = Blend(node, value, destinationState, alpha, bucket.BaseTransition->BlendMode);
|
||||
}
|
||||
@@ -1779,14 +1823,14 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
|
||||
else
|
||||
{
|
||||
// Sample the current state
|
||||
value = SampleState(bucket.CurrentState);
|
||||
value = SampleState(context, bucket.CurrentState);
|
||||
}
|
||||
|
||||
// Handle active transition blending
|
||||
if (bucket.ActiveTransition)
|
||||
{
|
||||
// Sample the active transition destination state
|
||||
const auto destinationState = SampleState(bucket.ActiveTransition->Destination);
|
||||
const auto destinationState = SampleState(context, bucket.ActiveTransition->Destination);
|
||||
|
||||
// Perform blending
|
||||
const float alpha = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration;
|
||||
@@ -1794,6 +1838,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
|
||||
}
|
||||
|
||||
bucket.LastUpdateFrame = context.CurrentFrameIndex;
|
||||
context.NodePath.Pop();
|
||||
#undef END_TRANSITION
|
||||
break;
|
||||
}
|
||||
@@ -2248,7 +2293,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
|
||||
|
||||
void AnimGraphExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& value)
|
||||
{
|
||||
auto& context = Context.Get();
|
||||
auto& context = *Context.Get();
|
||||
if (context.ValueCache.TryGet(boxBase, value))
|
||||
return;
|
||||
switch (node->TypeID)
|
||||
|
||||
@@ -413,10 +413,10 @@ bool Animation::Save(const StringView& path)
|
||||
MemoryWriteStream stream(4096);
|
||||
|
||||
// Info
|
||||
stream.WriteInt32(102);
|
||||
stream.WriteInt32(103);
|
||||
stream.WriteDouble(Data.Duration);
|
||||
stream.WriteDouble(Data.FramesPerSecond);
|
||||
stream.WriteBool(Data.EnableRootMotion);
|
||||
stream.WriteByte((byte)Data.RootMotionFlags);
|
||||
stream.WriteString(Data.RootNodeName, 13);
|
||||
|
||||
// Animation channels
|
||||
@@ -532,17 +532,22 @@ Asset::LoadResult Animation::load()
|
||||
int32 headerVersion = *(int32*)stream.GetPositionHandle();
|
||||
switch (headerVersion)
|
||||
{
|
||||
case 100:
|
||||
case 101:
|
||||
case 102:
|
||||
{
|
||||
case 103:
|
||||
stream.ReadInt32(&headerVersion);
|
||||
stream.ReadDouble(&Data.Duration);
|
||||
stream.ReadDouble(&Data.FramesPerSecond);
|
||||
Data.EnableRootMotion = stream.ReadBool();
|
||||
stream.ReadByte((byte*)&Data.RootMotionFlags);
|
||||
stream.ReadString(&Data.RootNodeName, 13);
|
||||
break;
|
||||
case 100:
|
||||
case 101:
|
||||
case 102:
|
||||
stream.ReadInt32(&headerVersion);
|
||||
stream.ReadDouble(&Data.Duration);
|
||||
stream.ReadDouble(&Data.FramesPerSecond);
|
||||
Data.RootMotionFlags = stream.ReadBool() ? AnimationRootMotionFlags::RootPositionXZ : AnimationRootMotionFlags::None;
|
||||
stream.ReadString(&Data.RootNodeName, 13);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
stream.ReadDouble(&Data.Duration);
|
||||
stream.ReadDouble(&Data.FramesPerSecond);
|
||||
|
||||
@@ -67,7 +67,7 @@ void AnimationGraph::OnDependencyModified(BinaryAsset* asset)
|
||||
|
||||
#endif
|
||||
|
||||
bool AnimationGraph::InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop)
|
||||
bool AnimationGraph::InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop, bool rootMotion)
|
||||
{
|
||||
if (!IsVirtual())
|
||||
{
|
||||
@@ -89,7 +89,7 @@ bool AnimationGraph::InitAsAnimation(SkinnedModel* baseModel, Animation* anim, b
|
||||
rootNode.Type = GRAPH_NODE_MAKE_TYPE(9, 1);
|
||||
rootNode.ID = 1;
|
||||
rootNode.Values.Resize(1);
|
||||
rootNode.Values[0] = (int32)RootMotionMode::NoExtraction;
|
||||
rootNode.Values[0] = (int32)(rootMotion ? RootMotionExtraction::Enable : RootMotionExtraction::Ignore);
|
||||
rootNode.Boxes.Resize(1);
|
||||
rootNode.Boxes[0] = AnimGraphBox(&rootNode, 0, VariantType::Void);
|
||||
auto& animNode = graph.Nodes[1];
|
||||
|
||||
@@ -37,8 +37,9 @@ public:
|
||||
/// <param name="baseModel">The base model asset.</param>
|
||||
/// <param name="anim">The animation to play.</param>
|
||||
/// <param name="loop">True if play animation in a loop.</param>
|
||||
/// <param name="rootMotion">True if apply root motion. Otherwise it will be ignored.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() bool InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop = true);
|
||||
API_FUNCTION() bool InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop = true, bool rootMotion = false);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load surface graph from the asset.
|
||||
|
||||
@@ -277,6 +277,8 @@ bool Content::GetAssetInfo(const StringView& path, AssetInfo& info)
|
||||
// Find asset in registry
|
||||
if (Cache.FindAsset(path, info))
|
||||
return true;
|
||||
if (!FileSystem::FileExists(path))
|
||||
return false;
|
||||
PROFILE_CPU();
|
||||
|
||||
const auto extension = FileSystem::GetExtension(path).ToLower();
|
||||
|
||||
@@ -393,35 +393,58 @@ bool JsonAsset::CreateInstance()
|
||||
if (typeHandle)
|
||||
{
|
||||
auto& type = typeHandle.GetType();
|
||||
switch (type.Type)
|
||||
{
|
||||
case ScriptingTypes::Class:
|
||||
{
|
||||
|
||||
// Ensure that object can deserialized
|
||||
const ScriptingType::InterfaceImplementation* interface = type.GetInterface(ISerializable::TypeInitializer);
|
||||
if (!interface)
|
||||
{
|
||||
LOG(Warning, "Cannot deserialize {0} from Json Asset because it doesn't implement ISerializable interface.", type.ToString());
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
auto modifier = Cache::ISerializeModifier.Get();
|
||||
modifier->EngineBuild = DataEngineBuild;
|
||||
|
||||
// Allocate object
|
||||
// Create object
|
||||
switch (type.Type)
|
||||
{
|
||||
case ScriptingTypes::Class:
|
||||
case ScriptingTypes::Structure:
|
||||
{
|
||||
const auto instance = Allocator::Allocate(type.Size);
|
||||
if (!instance)
|
||||
return true;
|
||||
Instance = instance;
|
||||
InstanceType = typeHandle;
|
||||
if (type.Type == ScriptingTypes::Class)
|
||||
{
|
||||
_dtor = type.Class.Dtor;
|
||||
type.Class.Ctor(instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dtor = type.Struct.Dtor;
|
||||
type.Struct.Ctor(instance);
|
||||
}
|
||||
|
||||
// Deserialize object
|
||||
auto modifier = Cache::ISerializeModifier.Get();
|
||||
modifier->EngineBuild = DataEngineBuild;
|
||||
((ISerializable*)((byte*)instance + interface->VTableOffset))->Deserialize(*Data, modifier.Value);
|
||||
break;
|
||||
}
|
||||
case ScriptingTypes::Script:
|
||||
{
|
||||
const ScriptingObjectSpawnParams params(Guid::New(), typeHandle);
|
||||
const auto instance = type.Script.Spawn(params);
|
||||
if (!instance)
|
||||
return true;
|
||||
Instance = instance;
|
||||
_dtor = nullptr;
|
||||
|
||||
// Deserialize object
|
||||
ToInterface<ISerializable>(instance)->Deserialize(*Data, modifier.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
InstanceType = typeHandle;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -441,13 +464,20 @@ void JsonAsset::DeleteInstance()
|
||||
}
|
||||
|
||||
// C++ instance
|
||||
if (!Instance || !_dtor)
|
||||
if (!Instance)
|
||||
return;
|
||||
if (_dtor)
|
||||
{
|
||||
_dtor(Instance);
|
||||
InstanceType = ScriptingTypeHandle();
|
||||
Allocator::Free(Instance);
|
||||
Instance = nullptr;
|
||||
_dtor = nullptr;
|
||||
Allocator::Free(Instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
Delete((ScriptingObject*)Instance);
|
||||
}
|
||||
InstanceType = ScriptingTypeHandle();
|
||||
Instance = nullptr;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
@@ -139,7 +139,8 @@ public:
|
||||
T* GetInstance() const
|
||||
{
|
||||
const_cast<JsonAsset*>(this)->CreateInstance();
|
||||
return Instance && InstanceType.IsAssignableFrom(T::TypeInitializer) ? (T*)Instance : nullptr;
|
||||
const ScriptingTypeHandle& type = T::TypeInitializer;
|
||||
return Instance && type.IsAssignableFrom(InstanceType) ? (T*)Instance : nullptr;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
133
Source/Engine/Content/JsonAssetReference.cs
Normal file
133
Source/Engine/Content/JsonAssetReference.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace FlaxEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Json asset reference utility. References resource with a typed data type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the asset instance type.</typeparam>
|
||||
#if FLAX_EDITOR
|
||||
[CustomEditor(typeof(FlaxEditor.CustomEditors.Editors.AssetRefEditor))]
|
||||
#endif
|
||||
public struct JsonAssetReference<T> : IComparable, IComparable<JsonAssetReference<T>>, IEquatable<JsonAssetReference<T>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the referenced asset.
|
||||
/// </summary>
|
||||
public JsonAsset Asset;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance of the serialized object from the json asset data. Cached internally.
|
||||
/// </summary>
|
||||
public T Instance => (T)Asset?.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonAssetReference{T}"/> structure.
|
||||
/// </summary>
|
||||
/// <param name="asset">The Json Asset.</param>
|
||||
public JsonAssetReference(JsonAsset asset)
|
||||
{
|
||||
Asset = asset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicit cast operator.
|
||||
/// </summary>
|
||||
public static implicit operator JsonAsset(JsonAssetReference<T> value)
|
||||
{
|
||||
return value.Asset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicit cast operator.
|
||||
/// </summary>
|
||||
public static implicit operator IntPtr(JsonAssetReference<T> value)
|
||||
{
|
||||
return Object.GetUnmanagedPtr(value.Asset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicit cast operator.
|
||||
/// </summary>
|
||||
public static implicit operator JsonAssetReference<T>(JsonAsset value)
|
||||
{
|
||||
return new JsonAssetReference<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicit cast operator.
|
||||
/// </summary>
|
||||
public static implicit operator JsonAssetReference<T>(IntPtr valuePtr)
|
||||
{
|
||||
return new JsonAssetReference<T>(Object.FromUnmanagedPtr(valuePtr) as JsonAsset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the object exists (reference is not null and the unmanaged object pointer is valid).
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to check.</param>
|
||||
/// <returns>True if object is valid, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator bool(JsonAssetReference<T> obj)
|
||||
{
|
||||
return obj.Asset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the two objects are equal.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(JsonAssetReference<T> left, JsonAssetReference<T> right)
|
||||
{
|
||||
return left.Asset == right.Asset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the two objects are not equal.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(JsonAssetReference<T> left, JsonAssetReference<T> right)
|
||||
{
|
||||
return left.Asset != right.Asset;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(JsonAssetReference<T> other)
|
||||
{
|
||||
return Asset == other.Asset;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(JsonAssetReference<T> other)
|
||||
{
|
||||
return Object.GetUnmanagedPtr(Asset).CompareTo(Object.GetUnmanagedPtr(other.Asset));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is JsonAssetReference<T> other && Asset == other.Asset;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Asset?.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
return obj is JsonAssetReference<T> other ? CompareTo(other) : 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (Asset != null ? Asset.GetHashCode() : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Source/Engine/Content/JsonAssetReference.h
Normal file
41
Source/Engine/Content/JsonAssetReference.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Content/JsonAsset.h"
|
||||
#include "Engine/Content/AssetReference.h"
|
||||
|
||||
/// <summary>
|
||||
/// Json asset reference utility. References resource with a typed data type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the asset instance type.</typeparam>
|
||||
template<typename T>
|
||||
API_STRUCT(NoDefault, Template, MarshalAs=JsonAsset*) struct JsonAssetReference : AssetReference<JsonAsset>
|
||||
{
|
||||
JsonAssetReference() = default;
|
||||
|
||||
JsonAssetReference(JsonAsset* asset)
|
||||
{
|
||||
OnSet(asset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the deserialized native object instance of the given type. Returns null if asset is not loaded or loaded object has different type.
|
||||
/// </summary>
|
||||
/// <returns>The asset instance object or null.</returns>
|
||||
FORCE_INLINE T* GetInstance() const
|
||||
{
|
||||
return _asset ? Get()->template GetInstance<T>() : nullptr;
|
||||
}
|
||||
|
||||
JsonAssetReference& operator=(JsonAsset* asset) noexcept
|
||||
{
|
||||
OnSet(asset);
|
||||
return *this;
|
||||
}
|
||||
|
||||
operator JsonAsset*() const
|
||||
{
|
||||
return Get();
|
||||
}
|
||||
};
|
||||
@@ -58,8 +58,8 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b
|
||||
Locker.Lock();
|
||||
|
||||
// Try fast lookup
|
||||
FlaxStorage* result;
|
||||
if (!StorageMap.TryGet(path, result))
|
||||
FlaxStorage* storage;
|
||||
if (!StorageMap.TryGet(path, storage))
|
||||
{
|
||||
// Detect storage type and create object
|
||||
const bool isPackage = path.EndsWith(StringView(PACKAGE_FILES_EXTENSION));
|
||||
@@ -67,39 +67,42 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b
|
||||
{
|
||||
auto package = New<FlaxPackage>(path);
|
||||
Packages.Add(package);
|
||||
result = package;
|
||||
storage = package;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto file = New<FlaxFile>(path);
|
||||
Files.Add(file);
|
||||
result = file;
|
||||
storage = file;
|
||||
}
|
||||
|
||||
// Register storage container
|
||||
StorageMap.Add(path, result);
|
||||
StorageMap.Add(path, storage);
|
||||
}
|
||||
|
||||
// Build reference (before releasing the lock so ContentStorageSystem::Job won't delete it when running from async thread)
|
||||
FlaxStorageReference result(storage);
|
||||
|
||||
Locker.Unlock();
|
||||
|
||||
if (loadIt)
|
||||
{
|
||||
// Initialize storage container
|
||||
result->LockChunks();
|
||||
const bool loadFailed = result->Load();
|
||||
result->UnlockChunks();
|
||||
storage->LockChunks();
|
||||
const bool loadFailed = storage->Load();
|
||||
storage->UnlockChunks();
|
||||
if (loadFailed)
|
||||
{
|
||||
LOG(Error, "Failed to load {0}.", path);
|
||||
Locker.Lock();
|
||||
StorageMap.Remove(path);
|
||||
if (result->IsPackage())
|
||||
Packages.Remove((FlaxPackage*)result);
|
||||
if (storage->IsPackage())
|
||||
Packages.Remove((FlaxPackage*)storage);
|
||||
else
|
||||
Files.Remove((FlaxFile*)result);
|
||||
Files.Remove((FlaxFile*)storage);
|
||||
Locker.Unlock();
|
||||
Delete(result);
|
||||
return nullptr;
|
||||
result = nullptr;
|
||||
Delete(storage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,13 @@ FlaxStorage::~FlaxStorage()
|
||||
|
||||
#if USE_EDITOR
|
||||
// Ensure to close any outstanding file handles to prevent file locking in case it failed to load
|
||||
_file.DeleteAll();
|
||||
Array<FileReadStream*> streams;
|
||||
_file.GetValues(streams);
|
||||
for (FileReadStream* stream : streams)
|
||||
{
|
||||
if (stream)
|
||||
Delete(stream);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1266,7 +1272,6 @@ bool FlaxStorage::LoadAssetHeader(const Entry& e, AssetInitData& data)
|
||||
}
|
||||
|
||||
#if ASSETS_LOADING_EXTRA_VERIFICATION
|
||||
|
||||
// Validate loaded header (asset ID and type ID must be the same)
|
||||
if (e.ID != data.Header.ID)
|
||||
{
|
||||
@@ -1276,7 +1281,6 @@ bool FlaxStorage::LoadAssetHeader(const Entry& e, AssetInitData& data)
|
||||
{
|
||||
LOG(Error, "Loading asset header data mismatch! Expected Type Name: {0}, loaded header: {1}.\nSource: {2}", e.TypeName, data.Header.ToString(), ToString());
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return false;
|
||||
@@ -1339,7 +1343,14 @@ bool FlaxStorage::CloseFileHandles()
|
||||
return true; // Failed, someone is still accessing the file
|
||||
|
||||
// Close file handles (from all threads)
|
||||
_file.DeleteAll();
|
||||
Array<FileReadStream*> streams;
|
||||
_file.GetValues(streams);
|
||||
for (FileReadStream* stream : streams)
|
||||
{
|
||||
if (stream)
|
||||
Delete(stream);
|
||||
}
|
||||
_file.Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ protected:
|
||||
CriticalSection _loadLocker;
|
||||
|
||||
// Storage
|
||||
ThreadLocalObject<FileReadStream> _file;
|
||||
ThreadLocal<FileReadStream*> _file;
|
||||
Array<FlaxChunk*> _chunks;
|
||||
|
||||
// Metadata
|
||||
|
||||
@@ -58,17 +58,17 @@ public:
|
||||
return _storage != nullptr;
|
||||
}
|
||||
|
||||
FORCE_INLINE bool operator ==(const FlaxStorageReference& other) const
|
||||
FORCE_INLINE bool operator==(const FlaxStorageReference& other) const
|
||||
{
|
||||
return _storage == other._storage;
|
||||
}
|
||||
|
||||
FORCE_INLINE bool operator !=(const FlaxStorageReference& other) const
|
||||
FORCE_INLINE bool operator!=(const FlaxStorageReference& other) const
|
||||
{
|
||||
return _storage != other._storage;
|
||||
}
|
||||
|
||||
FORCE_INLINE FlaxStorage* operator ->() const
|
||||
FORCE_INLINE FlaxStorage* operator->() const
|
||||
{
|
||||
return _storage;
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@ bool AssetsImportingManager::Create(const String& tag, const StringView& outputP
|
||||
LOG(Warning, "Cannot find asset creator object for tag \'{0}\'.", tag);
|
||||
return true;
|
||||
}
|
||||
|
||||
return Create(creator->Callback, outputPath, assetId, arg);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ CreateAssetResult CreateAnimationGraph::Create(CreateAssetContext& context)
|
||||
rootNode.Type = GRAPH_NODE_MAKE_TYPE(9, 1);
|
||||
rootNode.ID = 1;
|
||||
rootNode.Values.Resize(1);
|
||||
rootNode.Values[0] = (int32)RootMotionMode::NoExtraction;
|
||||
rootNode.Values[0] = (int32)RootMotionExtraction::NoExtraction;
|
||||
rootNode.Boxes.Resize(1);
|
||||
rootNode.Boxes[0] = AnimGraphBox(&rootNode, 0, VariantType::Void);
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ private:
|
||||
/// <summary>
|
||||
/// Asset importer entry
|
||||
/// </summary>
|
||||
struct AssetImporter
|
||||
struct FLAXENGINE_API AssetImporter
|
||||
{
|
||||
public:
|
||||
/// <summary>
|
||||
@@ -135,7 +135,7 @@ public:
|
||||
/// <summary>
|
||||
/// Asset creator entry
|
||||
/// </summary>
|
||||
struct AssetCreator
|
||||
struct FLAXENGINE_API AssetCreator
|
||||
{
|
||||
public:
|
||||
/// <summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user