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:
2024-02-19 22:26:16 +01:00
219 changed files with 4189 additions and 2372 deletions
@@ -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;
+54
View File
@@ -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.");
}
+1 -1
View File
@@ -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.");
}
}
+1 -2
View File
@@ -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;
+4 -3
View File
@@ -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);
}
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;
// 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;
}
// 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);
}
+50 -5
View File
@@ -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);
+5
View File
@@ -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>
+17 -11
View File
@@ -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,20 +454,19 @@ namespace FlaxEditor.GUI
}
// Focus the first visible item or then next one
var items = GetVisibleItems();
var focusedIndex = items.IndexOf(focusedItem);
if (focusedIndex == -1)
focusedIndex = -1;
if (focusedIndex + 1 < items.Count)
{
var items = GetVisibleItems();
var focusedIndex = items.IndexOf(focusedItem);
if (focusedIndex == -1)
focusedIndex = -1;
if (focusedIndex + 1 < items.Count)
{
var item = items[focusedIndex + 1];
item.Focus();
_scrollPanel.ScrollViewTo(item);
return true;
}
var item = items[focusedIndex + 1];
item.Focus();
_scrollPanel.ScrollViewTo(item);
return true;
}
break;
}
case KeyboardKeys.ArrowUp:
if (focusedItem != null)
{
+3 -2
View File
@@ -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 />
+1 -1
View File
@@ -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;
@@ -634,13 +562,11 @@ bool ManagedEditor::Import(const String& inputPath, const String& outputPath, co
bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String assetPath)
{
// Initialize defaults
// Initialize defaults
if (const auto* graphicsSettings = GraphicsSettings::Get())
{
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);
}
+7
View File
@@ -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
{
+17 -22
View File
@@ -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>
+11 -1
View File
@@ -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;
-2
View File
@@ -725,9 +725,7 @@ 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
+11 -3
View File
@@ -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.
+9 -1
View File
@@ -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;
@@ -217,12 +219,18 @@ namespace FlaxEditor.Options
if (styleName == ThemeOptions.LightDefault)
{
Style.Current = CreateLightStyle();
}
}
else
{
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);
}
}
}
+171 -5
View File
@@ -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);
}
}
}
+18 -3
View File
@@ -115,16 +115,31 @@ 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)
{
traceEvent = e;
return true;
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;
}
}
}
}
@@ -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 />
+1 -6
View File
@@ -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,8 +492,7 @@ 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;
SelectedItem = _groups.Find(g => g.Visible)?.Children.Find(c => c.Visible && c is VisjectCMItem) as VisjectCMItem;
PerformLayout();
if (SelectedItem != null)
_panel1.ScrollViewTo(SelectedItem);
@@ -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,72 +186,87 @@ 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))
{
GetTextRectangle(out var textRect);
if (QueryFilterHelper.Match(filterText, _archetype.Title, out var ranges))
// Update highlights
if (_highlights == null)
_highlights = new List<Rectangle>(ranges.Length);
else
_highlights.Clear();
var style = Style.Current;
var font = style.FontSmall;
for (int i = 0; i < ranges.Length; i++)
{
// Update highlights
if (_highlights == null)
_highlights = new List<Rectangle>(ranges.Length);
else
_highlights.Clear();
var style = Style.Current;
var font = style.FontSmall;
for (int i = 0; i < ranges.Length; i++)
var start = font.GetCharPosition(_archetype.Title, ranges[i].StartIndex);
var end = font.GetCharPosition(_archetype.Title, ranges[i].EndIndex);
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
if (ranges[i].StartIndex <= 0)
{
var start = font.GetCharPosition(_archetype.Title, ranges[i].StartIndex);
var end = font.GetCharPosition(_archetype.Title, ranges[i].EndIndex);
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
if (ranges[i].StartIndex <= 0)
{
_isStartsWithMatch = true;
if (ranges[i].Length == _archetype.Title.Length)
_isFullMatch = true;
}
_isStartsWithMatch = true;
if (ranges[i].Length == _archetype.Title.Length)
_isFullMatch = true;
}
Visible = true;
}
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)
{
// Hide
_highlights?.Clear();
Visible = false;
}
Visible = true;
return;
}
// Check archetype synonyms
if (_archetype.AlternativeTitles!= null && _archetype.AlternativeTitles.Any(altTitle => QueryFilterHelper.Match(filterText, altTitle, out ranges)))
{
// 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 />
+33
View File
@@ -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.
+15
View File
@@ -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);
+27 -1
View File
@@ -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
@@ -131,11 +135,16 @@ namespace FlaxEditor.Tools.Terrain.Paint
var sourceData = TerrainTools.GetSplatMapData(terrain, ref patch.PatchCoord, splatmapIndex);
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);
}
}
@@ -197,16 +207,32 @@ namespace FlaxEditor.Tools.Terrain.Paint
/// The splatmap texture index.
/// </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).
@@ -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>
+3 -3
View File
@@ -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
-493
View File
@@ -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);
+4 -5
View File
@@ -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;
+4 -1
View File
@@ -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);
+16
View File
@@ -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;
}
+1 -1
View File
@@ -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,7 +79,14 @@ namespace FlaxEditor.Windows
if (item.HasDefaultThumbnail == false)
{
cm.AddButton("Refresh thumbnail", item.RefreshThumbnail);
if (_view.SelectedCount > 1)
cm.AddButton("Refresh thumbnails", () =>
{
foreach (var e in _view.Selection)
e.RefreshThumbnail();
});
else
cm.AddButton("Refresh thumbnail", item.RefreshThumbnail);
}
if (!isFolder)
+2
View File
@@ -263,6 +263,8 @@ namespace FlaxEditor.Windows
/// <inheritdoc />
public override void OnDestroy()
{
if (IsDisposing)
return;
OnExit();
// Unregister
+10
View File
@@ -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");
-4
View File
@@ -20,9 +20,7 @@ namespace FlaxEngine
get
{
fixed (short* name = Name0)
{
return new string((char*)name);
}
}
}
@@ -31,9 +29,7 @@ namespace FlaxEngine
fixed (short* name = Name0)
{
fixed (char* p = prefix)
{
return Utils.MemoryCompare(new IntPtr(name), new IntPtr(p), (ulong)(prefix.Length * 2)) == 0;
}
}
}
}
@@ -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 &&
+5 -2
View File
@@ -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>