diff --git a/Ghost.App/Controls/BasicInput/Vector3Field.cs b/Ghost.App/Controls/BasicInput/Vector3Field.cs deleted file mode 100644 index fa31410..0000000 --- a/Ghost.App/Controls/BasicInput/Vector3Field.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Ghost.Editor.Event; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using System.Numerics; - -namespace Ghost.Editor.Controls; - -// TODO: value update event -public sealed partial class Vector3Field : Control -{ - private bool _suppressCallback; - - public Vector3 Value - { - get => new((float)X, (float)Y, (float)Z); - set - { - if (value == Value) - { - return; - } - - _suppressCallback = true; - - X = value.X; - Y = value.Y; - Z = value.Z; - - _suppressCallback = false; - } - } - - public double X - { - get => (double)GetValue(XProperty); - set => SetValue(XProperty, value); - } - - public static readonly DependencyProperty XProperty = - DependencyProperty.Register(nameof(X), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged)); - - public double Y - { - get => (double)GetValue(YProperty); - set => SetValue(YProperty, value); - } - - public static readonly DependencyProperty YProperty = - DependencyProperty.Register(nameof(Y), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged)); - - public double Z - { - get => (double)GetValue(ZProperty); - set => SetValue(ZProperty, value); - } - - public static readonly DependencyProperty ZProperty = - DependencyProperty.Register(nameof(Z), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged)); - - public event ValueChangedEventHandler? OnValueChanged; - - private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is Vector3Field vector3Field) - { - if (vector3Field._suppressCallback) - { - return; - } - - var oldValue = vector3Field.Value; - if (e.Property == XProperty) - { - var f = (float)(double)e.OldValue; - oldValue.X = f; - } - else if (e.Property == YProperty) - { - var f = (float)(double)e.OldValue; - oldValue.Y = f; - } - else if (e.Property == ZProperty) - { - var f = (float)(double)e.OldValue; - oldValue.Z = f; - } - - vector3Field.OnValueChanged?.Invoke(vector3Field, new ValueChangedEventArgs(oldValue, vector3Field.Value)); - } - } - - public Vector3Field() - { - DefaultStyleKey = typeof(Vector3Field); - } -} \ No newline at end of file diff --git a/Ghost.App/Controls/EditorControls.xaml b/Ghost.App/Controls/EditorControls.xaml deleted file mode 100644 index b2f05a0..0000000 --- a/Ghost.App/Controls/EditorControls.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Ghost.App/Controls/Internal/InternalControls.xaml b/Ghost.App/Controls/Internal/InternalControls.xaml deleted file mode 100644 index b543b4e..0000000 --- a/Ghost.App/Controls/Internal/InternalControls.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Ghost.App/Core/AssetHandle/Asset.cs b/Ghost.App/Core/AssetHandle/Asset.cs deleted file mode 100644 index b29a045..0000000 --- a/Ghost.App/Core/AssetHandle/Asset.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Ghost.Editor.Core.AssetHandle; - -public abstract class Asset -{ - /// - /// Get the Guid of the asset. - /// - public Guid GUID - { - get; - } = Guid.NewGuid(); - - /// - /// True if the asset is a folder, false if it is a file. - /// - public bool IsFolder - { - get; - } - - internal void GenerateMetadata() - { - } -} \ No newline at end of file diff --git a/Ghost.App/Core/Inspector/IComponentEditor.cs b/Ghost.App/Core/Inspector/IComponentEditor.cs deleted file mode 100644 index 9ce7849..0000000 --- a/Ghost.App/Core/Inspector/IComponentEditor.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Core.Inspector; - -interface IComponentEditor -{ - /// - /// Called when the component editor is created. - /// - /// The component data to edit. - /// The container to add the editor controls to. - public void Create(ComponentObject componentObject, StackPanel container); - - /// - /// Called when the component editor needs to update its UI based on the current state of the component data. - /// - /// The component data to edit. - public void Update(ComponentObject componentObject); - - /// - /// Called when the component editor is destroyed. - /// - /// The component data to edit. - public void Destroy(ComponentObject componentObject); -} \ No newline at end of file diff --git a/Ghost.App/Ghost.Editor.csproj b/Ghost.App/Ghost.Editor.csproj deleted file mode 100644 index 3e316a6..0000000 --- a/Ghost.App/Ghost.Editor.csproj +++ /dev/null @@ -1,203 +0,0 @@ - - - WinExe - net9.0-windows10.0.22621.0 - 10.0.17763.0 - x86;x64;ARM64 - win-x86;win-x64;win-arm64 - win-$(Platform).pubxml - true - true - preview - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - - - true - - - - - False - True - True - enable - 10.0.20348.0 - app.manifest - False - False - Ghost.Editor - enable - - \ No newline at end of file diff --git a/Ghost.App/Models/MessageType.cs b/Ghost.App/Models/MessageType.cs deleted file mode 100644 index c559200..0000000 --- a/Ghost.App/Models/MessageType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ghost.Editor.Models; - -public enum MessageType -{ - Informational, - Success, - Warning, - Error -} \ No newline at end of file diff --git a/Ghost.App/Resources/FileExtensions.cs b/Ghost.App/Resources/FileExtensions.cs deleted file mode 100644 index 9b38981..0000000 --- a/Ghost.App/Resources/FileExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Ghost.Editor.Resources; - -internal static class FileExtensions -{ - public const string PROJECT_FILE_EXTENSION = ".ghostproj"; - public const string TEMPLATE_FILE_EXTENSION = ".ghosttemplate"; - public const string SCENE_FILE_EXTENSION = ".ghostscene"; - public const string ASSET_FILE_EXTENSION = ".ghostasset"; - public const string SHADER_FILE_EXTENSION = ".ghostshader"; - public const string MATERIAL_FILE_EXTENSION = ".ghostmaterial"; -} \ No newline at end of file diff --git a/Ghost.App/Services/Contracts/IInspectorService.cs b/Ghost.App/Services/Contracts/IInspectorService.cs deleted file mode 100644 index 8dccc97..0000000 --- a/Ghost.App/Services/Contracts/IInspectorService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Ghost.Editor.Core.Inspector; - -namespace Ghost.Editor.Services.Contracts; - -internal interface IInspectorService -{ - public IInspectable? SelectedInspectable - { - get; - set; - } - - public event Action? OnSelectionChanged; -} \ No newline at end of file diff --git a/Ghost.App/Services/Contracts/INotificationService.cs b/Ghost.App/Services/Contracts/INotificationService.cs deleted file mode 100644 index cef71e7..0000000 --- a/Ghost.App/Services/Contracts/INotificationService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Ghost.Editor.Models; - -namespace Ghost.Editor.Services.Contracts; - -public interface INotificationService -{ - public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null); -} diff --git a/Ghost.App/Services/Contracts/IProgressService.cs b/Ghost.App/Services/Contracts/IProgressService.cs deleted file mode 100644 index fce0331..0000000 --- a/Ghost.App/Services/Contracts/IProgressService.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ghost.Editor.Services.Contracts; - -public interface IProgressService -{ - public void ShowProgress(string message, double progress = 0.0); - public void ShowIndeterminateProgress(string message); - public void SetProgress(double progress); - public void HideProgress(); -} \ No newline at end of file diff --git a/Ghost.Core/Ghost.Core.csproj b/Ghost.Core/Ghost.Core.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/Ghost.Core/Ghost.Core.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/Ghost.Data/Models/Result.cs b/Ghost.Core/Result.cs similarity index 57% rename from Ghost.Data/Models/Result.cs rename to Ghost.Core/Result.cs index 134cca9..d4c8d55 100644 --- a/Ghost.Data/Models/Result.cs +++ b/Ghost.Core/Result.cs @@ -1,4 +1,4 @@ -namespace Ghost.Data.Models; +namespace Ghost.Core; public readonly struct Result { @@ -22,20 +22,28 @@ public readonly struct Result return new Result(false, message); } + public void CheckSuccess() + { + if (!success) + { + throw new InvalidOperationException($"Operation failed: {message}"); + } + } + public override string ToString() => success ? "OK" : $"Error: {message}"; } public readonly struct Result { public readonly bool success; - public readonly T? data; + public readonly T value; public readonly string? message; - public Result(bool success, T? data, string? message = null) + public Result(bool success, T data, string? message = null) { this.success = success; - this.data = data; + this.value = data; this.message = message; } @@ -46,8 +54,16 @@ public readonly struct Result public static Result Error(string? message) { - return new Result(false, default, message); + return new Result(false, default!, message); } - public override string ToString() => success ? $"OK: {data}" : $"Error: {message}"; + public void CheckSuccess() + { + if (!success) + { + throw new InvalidOperationException($"Operation failed: {message}"); + } + } + + public override string ToString() => success ? $"OK: {value}" : $"Error: {message}"; } diff --git a/Ghost.Entities/Utilities/TypeHandle.cs b/Ghost.Core/TypeHandle.cs similarity index 55% rename from Ghost.Entities/Utilities/TypeHandle.cs rename to Ghost.Core/TypeHandle.cs index 2332f89..02b1050 100644 --- a/Ghost.Entities/Utilities/TypeHandle.cs +++ b/Ghost.Core/TypeHandle.cs @@ -1,18 +1,17 @@ using System.Runtime.CompilerServices; -namespace Ghost.Entities.Utilities; +namespace Ghost.Core; -internal static class TypeHandle +public readonly struct TypeHandle { - /// - /// Gets the type handle for the specified type. - /// - /// The type to get the handle for. - /// The type handle as a nint. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static nint Get() + public readonly IntPtr Value { - return typeof(T).TypeHandle.Value; + get; + } + + private TypeHandle(IntPtr value) + { + Value = value; } /// @@ -21,13 +20,23 @@ internal static class TypeHandle /// The type to get the handle for. /// The type handle as a nint. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static nint Get(Type type) - { - return type.TypeHandle.Value; - } + public static TypeHandle Get(Type type) => new TypeHandle(type.TypeHandle.Value); - public static Type? ToType(nint handle) + /// + /// Gets the type handle for the specified type. + /// + /// The type to get the handle for. + /// The type handle as a nint. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TypeHandle Get() => Get(typeof(T)); + + /// + /// Converts a TypeHandle to a Type. + /// + /// The TypeHandle to convert. + /// The corresponding Type. + public Type? ToType() { - return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(handle)); + return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(Value)); } } \ No newline at end of file diff --git a/Ghost.Data/AssemblyInfo.cs b/Ghost.Data/AssemblyInfo.cs index ef8099a..af1d141 100644 --- a/Ghost.Data/AssemblyInfo.cs +++ b/Ghost.Data/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Ghost.Editor")] \ No newline at end of file +[assembly: InternalsVisibleTo("Ghost.Editor")] +[assembly: InternalsVisibleTo("Ghost.Editor.Core")] \ No newline at end of file diff --git a/Ghost.Data/Ghost.Data.csproj b/Ghost.Data/Ghost.Data.csproj index d2c294e..e09919d 100644 --- a/Ghost.Data/Ghost.Data.csproj +++ b/Ghost.Data/Ghost.Data.csproj @@ -26,4 +26,8 @@ + + + + diff --git a/Ghost.Data/Services/ProjectService.cs b/Ghost.Data/Services/ProjectService.cs index fdb5bd7..11ccaa3 100644 --- a/Ghost.Data/Services/ProjectService.cs +++ b/Ghost.Data/Services/ProjectService.cs @@ -1,4 +1,5 @@ -using Ghost.Data.Models; +using Ghost.Core; +using Ghost.Data.Models; using Ghost.Data.Repository; using Ghost.Data.Resources; using System.IO.Compression; @@ -197,12 +198,12 @@ internal partial class ProjectService return result; } - if (await HasProjectAsync(result.data.Path)) + if (await HasProjectAsync(result.value.Path)) { return Result.Error("Project already exists."); } - await AddProjectAsync(result.data.Metadata.Name, result.data.Path); + await AddProjectAsync(result.value.Metadata.Name, result.value.Path); return result; } diff --git a/Ghost.App/Core/AppState/AppStateMachine.cs b/Ghost.Editor.Core/AppState/AppStateMachine.cs similarity index 100% rename from Ghost.App/Core/AppState/AppStateMachine.cs rename to Ghost.Editor.Core/AppState/AppStateMachine.cs diff --git a/Ghost.App/Core/AppState/IAppState.cs b/Ghost.Editor.Core/AppState/IAppState.cs similarity index 100% rename from Ghost.App/Core/AppState/IAppState.cs rename to Ghost.Editor.Core/AppState/IAppState.cs diff --git a/Ghost.App/Core/AppState/StateKey.cs b/Ghost.Editor.Core/AppState/StateKey.cs similarity index 100% rename from Ghost.App/Core/AppState/StateKey.cs rename to Ghost.Editor.Core/AppState/StateKey.cs diff --git a/Ghost.App/AssemblyInfo.cs b/Ghost.Editor.Core/AssemblyInfo.cs similarity index 66% rename from Ghost.App/AssemblyInfo.cs rename to Ghost.Editor.Core/AssemblyInfo.cs index d3b1fb4..c790e7f 100644 --- a/Ghost.App/AssemblyInfo.cs +++ b/Ghost.Editor.Core/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Ghost.UnitTest")] +[assembly: InternalsVisibleTo("Ghost.Editor")] \ No newline at end of file diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.Meta.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Meta.cs new file mode 100644 index 0000000..a28eb0a --- /dev/null +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Meta.cs @@ -0,0 +1,158 @@ +using Ghost.Core; +using Ghost.Editor.Core.Utilities; +using Ghost.Engine.Services; +using System.Reflection; +using System.Text.Json; + +namespace Ghost.Editor.Core.AssetHandle; +public static partial class AssetDatabase +{ + private static readonly Dictionary _importerTypeLookup = new(); + + private static void InitializeMetaData() + { + if (_watcher == null) + { + throw new InvalidOperationException("AssetDatabase is not initialized. Ensure that Initialize() is called before registering asset importers."); + } + + var importerTypes = TypeCache.GetTypes().Where(t => t.GetCustomAttribute() != null); + foreach (var type in importerTypes) + { + var attribute = type.GetCustomAttribute()!; + foreach (var extension in attribute.SupportedExtensions) + { + _importerTypeLookup[extension] = type; + } + } + + _watcher.Created += OnAssetCreated; + _watcher.Deleted += OnAssetDeleted; + _watcher.Renamed += OnAssetRenamed; + } + + private static Result GetMetaFilePath(string assetPath) + { + if (Directory.Exists(assetPath)) + { + return Result.Error("Folder does not have meta data"); + } + + if (Path.GetExtension(assetPath).Equals(".meta", StringComparison.OrdinalIgnoreCase)) + { + return Result.Error("Asset path cannot be a meta file"); + } + + return Result.OK(assetPath + ".meta"); + } + + private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath) + { + var extension = Path.GetExtension(assetPath); + + if (_importerTypeLookup.TryGetValue(extension, out var importerType)) + { + var settingsType = importerType.BaseType?.GetGenericArguments()[0]; + if (settingsType == null || !typeof(ImporterSettings).IsAssignableFrom(settingsType)) + { + return null; + } + + return (ImporterSettings?)Activator.CreateInstance(settingsType); + } + + return null; + } + + private static void WriteMetaFile(string metaFilePath, AssetMeta metaData) + { + using var fileStream = File.Create(metaFilePath); + + try + { + JsonSerializer.Serialize(fileStream, metaData); + } + catch (Exception ex) + { + Logger.LogError(ex); + } + } + + internal static void GenerateMetaFile(string assetPath) + { + var metaFileResult = GetMetaFilePath(assetPath); + if (!metaFileResult.success) + { + Logger.LogError(metaFileResult.message); + return; + } + + if (File.Exists(metaFileResult.value)) + { + var existingMeta = JsonSerializer.Deserialize(File.ReadAllText(metaFileResult.value)); + if (existingMeta != null && _assetPathLookup.TryGetValue(existingMeta.Guid, out var path)) + { + if (assetPath != path) + { + existingMeta.Guid = Guid.NewGuid(); + WriteMetaFile(metaFileResult.value, existingMeta); + } + } + + return; + } + + var defaultSettings = GetDefaultSettingsForAsset(assetPath); + var metaData = new AssetMeta + { + Guid = Guid.NewGuid(), + Settings = defaultSettings + }; + + WriteMetaFile(metaFileResult.value, metaData); + } + + private static void OnAssetCreated(object sender, FileSystemEventArgs e) + { + GenerateMetaFile(e.FullPath); + } + + private static void OnAssetDeleted(object sender, FileSystemEventArgs e) + { + var metaFileResult = GetMetaFilePath(e.FullPath); + if (metaFileResult.success && File.Exists(metaFileResult.value)) + { + try + { + var meta = JsonSerializer.Deserialize(File.ReadAllText(metaFileResult.value)); + if (meta != null + && _assetPathLookup.TryGetValue(meta.Guid, out var path) + && path == e.FullPath) + { + _assetPathLookup.Remove(meta.Guid); + } + + File.Delete(metaFileResult.value); + } + catch (Exception ex) + { + Logger.LogError(ex); + } + } + } + + private static void OnAssetRenamed(object sender, RenamedEventArgs e) + { + var oldMetaPath = e.OldFullPath + ".meta"; + var newMetaPath = e.FullPath + ".meta"; + + if (File.Exists(oldMetaPath)) + { + File.Move(oldMetaPath, newMetaPath); + } + else + { + GenerateMetaFile(e.FullPath); + } + } +} \ No newline at end of file diff --git a/Ghost.App/Core/AssetHandle/AssetDatabase.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Open.cs similarity index 87% rename from Ghost.App/Core/AssetHandle/AssetDatabase.cs rename to Ghost.Editor.Core/AssetHandle/AssetDatabase.Open.cs index 3714b67..8d180a6 100644 --- a/Ghost.App/Core/AssetHandle/AssetDatabase.cs +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.Open.cs @@ -3,21 +3,11 @@ using System.Reflection; namespace Ghost.Editor.Core.AssetHandle; -public static class AssetDatabase +public static partial class AssetDatabase { private static readonly Dictionary> _assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase); - static AssetDatabase() - { - Initialize(); - } - - internal static void Initialize() - { - RegisterAssetHandles(); - } - - private static void RegisterAssetHandles() + private static void InitializeAssetHandle() { var methods = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(a => a.GetTypes()) diff --git a/Ghost.Editor.Core/AssetHandle/AssetDatabase.cs b/Ghost.Editor.Core/AssetHandle/AssetDatabase.cs new file mode 100644 index 0000000..1eb5502 --- /dev/null +++ b/Ghost.Editor.Core/AssetHandle/AssetDatabase.cs @@ -0,0 +1,35 @@ +using Ghost.Data.Services; + +namespace Ghost.Editor.Core.AssetHandle; + +public static partial class AssetDatabase +{ + private static FileSystemWatcher? _watcher; + + private static readonly Dictionary _assetPathLookup = new(); + + public static DirectoryInfo? AssetsDirectory + { + get; + private set; + } + + internal static void Initialize() + { + if (ProjectService.CurrentProject.Metadata == null) + { + throw new InvalidOperationException("Project metadata is not initialized. Ensure that the project is loaded before accessing the AssetDatabase."); + } + + AssetsDirectory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER)); + _watcher = new FileSystemWatcher + { + Path = AssetsDirectory.FullName, + IncludeSubdirectories = true, + EnableRaisingEvents = true + }; + + InitializeAssetHandle(); + InitializeMetaData(); + } +} \ No newline at end of file diff --git a/Ghost.Editor.Core/AssetHandle/AssetImporterAttribute.cs b/Ghost.Editor.Core/AssetHandle/AssetImporterAttribute.cs new file mode 100644 index 0000000..7324c13 --- /dev/null +++ b/Ghost.Editor.Core/AssetHandle/AssetImporterAttribute.cs @@ -0,0 +1,15 @@ +namespace Ghost.Editor.Core.AssetHandle; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class AssetImporterAttribute : Attribute +{ + public string[] SupportedExtensions + { + get; + } + + public AssetImporterAttribute(params string[] supportedExtensions) + { + SupportedExtensions = supportedExtensions; + } +} \ No newline at end of file diff --git a/Ghost.Editor.Core/AssetHandle/AssetMeta.cs b/Ghost.Editor.Core/AssetHandle/AssetMeta.cs new file mode 100644 index 0000000..0dec40e --- /dev/null +++ b/Ghost.Editor.Core/AssetHandle/AssetMeta.cs @@ -0,0 +1,16 @@ +namespace Ghost.Editor.Core.AssetHandle; + +internal class AssetMeta +{ + public Guid Guid + { + get; + internal set; + } + + public ImporterSettings? Settings + { + get; + set; + } +} \ No newline at end of file diff --git a/Ghost.App/Core/AssetHandle/AssetOpenHandlerAttribute .cs b/Ghost.Editor.Core/AssetHandle/AssetOpenHandlerAttribute .cs similarity index 100% rename from Ghost.App/Core/AssetHandle/AssetOpenHandlerAttribute .cs rename to Ghost.Editor.Core/AssetHandle/AssetOpenHandlerAttribute .cs diff --git a/Ghost.Editor.Core/AssetHandle/ImporterSettings.cs b/Ghost.Editor.Core/AssetHandle/ImporterSettings.cs new file mode 100644 index 0000000..93a0480 --- /dev/null +++ b/Ghost.Editor.Core/AssetHandle/ImporterSettings.cs @@ -0,0 +1,5 @@ +namespace Ghost.Editor.Core.AssetHandle; + +public abstract class ImporterSettings +{ +} \ No newline at end of file diff --git a/Ghost.App/Contracts/INavigationAware.cs b/Ghost.Editor.Core/Contracts/INavigationAware.cs similarity index 73% rename from Ghost.App/Contracts/INavigationAware.cs rename to Ghost.Editor.Core/Contracts/INavigationAware.cs index 4dcd428..76ab1b4 100644 --- a/Ghost.App/Contracts/INavigationAware.cs +++ b/Ghost.Editor.Core/Contracts/INavigationAware.cs @@ -1,4 +1,4 @@ -namespace Ghost.Editor.Contracts; +namespace Ghost.Editor.Core.Contracts; public interface INavigationAware { diff --git a/Ghost.App/Controls/BasicInput/PropertyField.cs b/Ghost.Editor.Core/Controls/BasicInput/PropertyField.cs similarity index 100% rename from Ghost.App/Controls/BasicInput/PropertyField.cs rename to Ghost.Editor.Core/Controls/BasicInput/PropertyField.cs diff --git a/Ghost.App/Controls/BasicInput/PropertyField.xaml b/Ghost.Editor.Core/Controls/BasicInput/PropertyField.xaml similarity index 100% rename from Ghost.App/Controls/BasicInput/PropertyField.xaml rename to Ghost.Editor.Core/Controls/BasicInput/PropertyField.xaml diff --git a/Ghost.Editor.Core/Controls/BasicInput/Vector3Field.cs b/Ghost.Editor.Core/Controls/BasicInput/Vector3Field.cs new file mode 100644 index 0000000..1845095 --- /dev/null +++ b/Ghost.Editor.Core/Controls/BasicInput/Vector3Field.cs @@ -0,0 +1,70 @@ +using Ghost.Editor.Core.Controls; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Numerics; + +namespace Ghost.Editor.Controls; + +[TemplatePart(Name = "XComponent", Type = typeof(NumberBox))] +[TemplatePart(Name = "YComponent", Type = typeof(NumberBox))] +[TemplatePart(Name = "ZComponent", Type = typeof(NumberBox))] +public sealed partial class Vector3Field : ValueControl +{ + private NumberBox? _xComponent; + private NumberBox? _yComponent; + private NumberBox? _zComponent; + + public Vector3Field() + { + DefaultStyleKey = typeof(Vector3Field); + } + + protected override void ValueChanged(Vector3 oldValue, Vector3 newValue) + { + SyncFromValue(); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _xComponent?.ValueChanged -= OnComponentChanged; + _yComponent?.ValueChanged -= OnComponentChanged; + _zComponent?.ValueChanged -= OnComponentChanged; + + _xComponent = GetTemplateChild("XComponent") as NumberBox; + _yComponent = GetTemplateChild("YComponent") as NumberBox; + _zComponent = GetTemplateChild("ZComponent") as NumberBox; + + SyncFromValue(); + + _xComponent?.ValueChanged += OnComponentChanged; + _yComponent?.ValueChanged += OnComponentChanged; + _zComponent?.ValueChanged += OnComponentChanged; + } + + private void SyncFromValue() + { + SuppressChangedEvent = true; + _xComponent?.Value = Value.X; + _yComponent?.Value = Value.Y; + _zComponent?.Value = Value.Z; + SuppressChangedEvent = false; + } + + private void OnComponentChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) + { + if (SuppressChangedEvent) + { + return; + } + + var newValue = new Vector3( + (float)(_xComponent?.Value ?? 0), + (float)(_yComponent?.Value ?? 0), + (float)(_zComponent?.Value ?? 0)); + + RiseChangedEvent(Value, newValue); + Value = newValue; + } +} \ No newline at end of file diff --git a/Ghost.App/Controls/BasicInput/Vector3Field.xaml b/Ghost.Editor.Core/Controls/BasicInput/Vector3Field.xaml similarity index 82% rename from Ghost.App/Controls/BasicInput/Vector3Field.xaml rename to Ghost.Editor.Core/Controls/BasicInput/Vector3Field.xaml index f41c3be..4a4226c 100644 --- a/Ghost.App/Controls/BasicInput/Vector3Field.xaml +++ b/Ghost.Editor.Core/Controls/BasicInput/Vector3Field.xaml @@ -22,17 +22,17 @@ Grid.Column="0" VerticalAlignment="Center" Text="X" /> - + - + - + diff --git a/Ghost.Editor.Core/Controls/ControlsDictionary.cs b/Ghost.Editor.Core/Controls/ControlsDictionary.cs new file mode 100644 index 0000000..5a89f08 --- /dev/null +++ b/Ghost.Editor.Core/Controls/ControlsDictionary.cs @@ -0,0 +1,13 @@ +using Microsoft.UI.Xaml; + +namespace Ghost.Editor.Core.Controls; + +public partial class ControlsDictionary : ResourceDictionary +{ + private const string _DICTIONARY_PATH = "ms-appx:///Ghost.Editor.Core/Controls/ControlsDictionary.xaml"; + + public ControlsDictionary() + { + Source = new Uri(_DICTIONARY_PATH, UriKind.Absolute); + } +} \ No newline at end of file diff --git a/Ghost.Editor.Core/Controls/ControlsDictionary.xaml b/Ghost.Editor.Core/Controls/ControlsDictionary.xaml new file mode 100644 index 0000000..7bc6fe2 --- /dev/null +++ b/Ghost.Editor.Core/Controls/ControlsDictionary.xaml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Ghost.App/Controls/Internal/ComponentDataView.cs b/Ghost.Editor.Core/Controls/Internal/ComponentDataView.cs similarity index 72% rename from Ghost.App/Controls/Internal/ComponentDataView.cs rename to Ghost.Editor.Core/Controls/Internal/ComponentDataView.cs index bf96072..353019e 100644 --- a/Ghost.App/Controls/Internal/ComponentDataView.cs +++ b/Ghost.Editor.Core/Controls/Internal/ComponentDataView.cs @@ -1,25 +1,27 @@ +using Ghost.Core; using Ghost.Editor.Core.Inspector; -using Ghost.Editor.Resources; -using Ghost.Editor.Utilities; +using Ghost.Editor.Core.Resources; +using Ghost.Editor.Core.Utilities; using Ghost.Entities; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; using System.Reflection; namespace Ghost.Editor.Controls.Internal; internal unsafe sealed partial class ComponentDataView : Control { + private delegate void EditorUpdate(); + private StackPanel? _contentContainer; private readonly World? _world; private readonly Entity _entity = Entity.Invalid; private readonly Type? _componentType; - private EventHandler? _updateHandler; - private IComponentEditor? _customEditor; + private ComponentEditor? _customEditor; private PropertyField[]? _propertyFields; + private EditorUpdate? _editorUpdate; public string HeaderText { @@ -36,7 +38,8 @@ internal unsafe sealed partial class ComponentDataView : Control Unloaded += (s, e) => { - CompositionTarget.Rendering -= _updateHandler; + _customEditor?.Destroy(); + _contentContainer = null; _customEditor = null; _propertyFields = null; @@ -59,7 +62,7 @@ internal unsafe sealed partial class ComponentDataView : Control ReBuild(); } - private void ReflectionUpdate(object? sender, object e) + private void ReflectionUpdate() { if (_propertyFields == null) { @@ -72,9 +75,9 @@ internal unsafe sealed partial class ComponentDataView : Control } } - private void CustomEditorUpdate(object? sender, object e) + private void CustomEditorUpdate() { - _customEditor!.Update(new ComponentObject(_world!, _entity)); + _customEditor!.Update(); } public void ReBuild() @@ -92,13 +95,14 @@ internal unsafe sealed partial class ComponentDataView : Control var componentObject = new ComponentObject(_world, _entity); var editorType = TypeCache.GetTypes().FirstOrDefault(t => - typeof(IComponentEditor).IsAssignableFrom(t) && + typeof(ComponentEditor).IsAssignableFrom(t) && t.GetCustomAttribute()?.TargetType.IsAssignableFrom(_componentType) == true); if (editorType != null) { - _customEditor = (IComponentEditor)Activator.CreateInstance(editorType)!; - _customEditor.Create(componentObject, _contentContainer); + _customEditor = (ComponentEditor)Activator.CreateInstance(editorType)!; + _customEditor.Initialize(componentObject); + _customEditor.Create(_contentContainer); } else { @@ -108,7 +112,7 @@ internal unsafe sealed partial class ComponentDataView : Control for (var i = 0; i < fields.Length; i++) { var field = fields[i]; - var component = _world.ComponentStorage.ComponentPools[_componentType.TypeHandle.Value].Get(_entity); + var component = _world.ComponentStorage.ComponentPools[TypeHandle.Get(_componentType)].Get(_entity); var propertyField = PropertyField.Create(field.Name, field, component); _propertyFields[i] = propertyField; @@ -116,7 +120,21 @@ internal unsafe sealed partial class ComponentDataView : Control } } - _updateHandler = _customEditor == null ? ReflectionUpdate : CustomEditorUpdate; - CompositionTarget.Rendering += _updateHandler; + _editorUpdate = _customEditor == null ? ReflectionUpdate : CustomEditorUpdate; + _editorUpdate(); + + _world.ComponentChanged += OnComponentChanged; + } + + private void OnComponentChanged(World world, Entity entity, Type type) + { + if (world != _world + || entity != _entity + || type != _componentType) + { + return; + } + + _editorUpdate?.Invoke(); } } \ No newline at end of file diff --git a/Ghost.App/Controls/Internal/ComponentDataView.xaml b/Ghost.Editor.Core/Controls/Internal/ComponentDataView.xaml similarity index 100% rename from Ghost.App/Controls/Internal/ComponentDataView.xaml rename to Ghost.Editor.Core/Controls/Internal/ComponentDataView.xaml diff --git a/Ghost.App/Controls/Internal/NavigationTabView.cs b/Ghost.Editor.Core/Controls/Internal/NavigationTabView.cs similarity index 77% rename from Ghost.App/Controls/Internal/NavigationTabView.cs rename to Ghost.Editor.Core/Controls/Internal/NavigationTabView.cs index cf33757..e5e4289 100644 --- a/Ghost.App/Controls/Internal/NavigationTabView.cs +++ b/Ghost.Editor.Core/Controls/Internal/NavigationTabView.cs @@ -1,4 +1,5 @@ -using Ghost.Editor.Contracts; +using Ghost.Editor.Core.Contracts; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.Controls.Internal; @@ -18,7 +19,9 @@ public sealed partial class NavigationTabView : TabView { public NavigationTabView() { - this.SelectionChanged += NavigationTabView_SelectionChanged; + HorizontalAlignment = HorizontalAlignment.Stretch; + VerticalAlignment = VerticalAlignment.Stretch; + SelectionChanged += NavigationTabView_SelectionChanged; } private void NavigationTabView_SelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/Ghost.App/Controls/Internal/NavigationTabView.xaml b/Ghost.Editor.Core/Controls/Internal/NavigationTabView.xaml similarity index 100% rename from Ghost.App/Controls/Internal/NavigationTabView.xaml rename to Ghost.Editor.Core/Controls/Internal/NavigationTabView.xaml diff --git a/Ghost.Editor.Core/Controls/ValueControl.cs b/Ghost.Editor.Core/Controls/ValueControl.cs new file mode 100644 index 0000000..6c83bdc --- /dev/null +++ b/Ghost.Editor.Core/Controls/ValueControl.cs @@ -0,0 +1,70 @@ +using Ghost.Editor.Core.Event; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Ghost.Editor.Core.Controls; + +public partial class ValueControl : Control +{ + private bool _suppressChangedEvent; + + protected bool SuppressChangedEvent + { + get => _suppressChangedEvent; + set => _suppressChangedEvent = value; + } + + public T Value + { + get => (T)GetValue(ValueProperty); + set + { + if (EqualityComparer.Default.Equals(Value, value)) + { + return; + } + + SetValue(ValueProperty, value); + } + } + + public static readonly DependencyProperty ValueProperty = + DependencyProperty.Register(nameof(Value), typeof(T), typeof(ValueControl), new PropertyMetadata(default(T), ChangedCallback)); + + public event ValueChangedEventHandler? OnValueChanged; + + private static void ChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ValueControl valueControl) + { + valueControl.ValueChanged((T)e.OldValue, (T)e.NewValue); + + if (!valueControl._suppressChangedEvent) + { + valueControl.OnValueChanged?.Invoke(valueControl, new((T)e.OldValue, (T)e.NewValue)); + } + } + } + + protected virtual void ValueChanged(T oldValue, T newValue) + { + } + + protected void RiseChangedEvent(T oldValue, T newValue) + { + OnValueChanged?.Invoke(this, new(oldValue, newValue)); + } + + /// + /// Sets the value without notifying the change event. + /// + /// The new value to set. + /// This method only suppresses the change event notification, not the method. + /// Useful when you need to change the value programmatically without triggering the change event. + public void SetValueWithoutNotifying(T value) + { + _suppressChangedEvent = true; + SetValue(ValueProperty, value); + _suppressChangedEvent = false; + } +} \ No newline at end of file diff --git a/Ghost.App/Event/ValueChangedEventHandler.cs b/Ghost.Editor.Core/Event/ValueChangedEventHandler.cs similarity index 90% rename from Ghost.App/Event/ValueChangedEventHandler.cs rename to Ghost.Editor.Core/Event/ValueChangedEventHandler.cs index f04242d..76a5845 100644 --- a/Ghost.App/Event/ValueChangedEventHandler.cs +++ b/Ghost.Editor.Core/Event/ValueChangedEventHandler.cs @@ -1,4 +1,4 @@ -namespace Ghost.Editor.Event; +namespace Ghost.Editor.Core.Event; public delegate void ValueChangedEventHandler(object? sender, ValueChangedEventArgs args); diff --git a/Ghost.Editor.Core/Ghost.Editor.Core.csproj b/Ghost.Editor.Core/Ghost.Editor.Core.csproj new file mode 100644 index 0000000..cf4ff40 --- /dev/null +++ b/Ghost.Editor.Core/Ghost.Editor.Core.csproj @@ -0,0 +1,48 @@ + + + net9.0-windows10.0.22621.0 + 10.0.17763.0 + Ghost.Editor.Core + win-x86;win-x64;win-arm64 + true + enable + 10.0.20348.0 + enable + True + preview + + + + + + + + + + + + + + + + + Designer + + + Designer + + + Designer + + + Designer + + + Designer + + + + + + + \ No newline at end of file diff --git a/Ghost.Editor.Core/Inspector/ComponentEditor.cs b/Ghost.Editor.Core/Inspector/ComponentEditor.cs new file mode 100644 index 0000000..ec0e26b --- /dev/null +++ b/Ghost.Editor.Core/Inspector/ComponentEditor.cs @@ -0,0 +1,40 @@ +using Microsoft.UI.Xaml.Controls; + +namespace Ghost.Editor.Core.Inspector; + +public abstract class ComponentEditor +{ + private ComponentObject _componentObject; + + /// + /// Represents the underlying component object used by this class to manage its functionality. + /// + protected ComponentObject ComponentObject => _componentObject; + + internal void Initialize(ComponentObject componentObject) + { + _componentObject = componentObject; + } + + /// + /// Called when the component editor is created. + /// + /// The container to add the editor controls to. + public virtual void Create(StackPanel container) + { + } + + /// + /// Called when the component editor needs to update its UI based on the current state of the component data. + /// + public virtual void Update() + { + } + + /// + /// Called when the component editor is destroyed. + /// + public virtual void Destroy() + { + } +} \ No newline at end of file diff --git a/Ghost.App/Core/Inspector/ComponentObject.cs b/Ghost.Editor.Core/Inspector/ComponentObject.cs similarity index 100% rename from Ghost.App/Core/Inspector/ComponentObject.cs rename to Ghost.Editor.Core/Inspector/ComponentObject.cs diff --git a/Ghost.App/Core/Inspector/CustomEditorAttribute.cs b/Ghost.Editor.Core/Inspector/CustomEditorAttribute.cs similarity index 100% rename from Ghost.App/Core/Inspector/CustomEditorAttribute.cs rename to Ghost.Editor.Core/Inspector/CustomEditorAttribute.cs diff --git a/Ghost.App/Core/Inspector/IInspectable.cs b/Ghost.Editor.Core/Inspector/IInspectable.cs similarity index 100% rename from Ghost.App/Core/Inspector/IInspectable.cs rename to Ghost.Editor.Core/Inspector/IInspectable.cs diff --git a/Ghost.Editor/Services/Contracts/IInspectorService.cs b/Ghost.Editor.Core/Inspector/IInspectorService.cs similarity index 68% rename from Ghost.Editor/Services/Contracts/IInspectorService.cs rename to Ghost.Editor.Core/Inspector/IInspectorService.cs index d2881c6..17ce3f6 100644 --- a/Ghost.Editor/Services/Contracts/IInspectorService.cs +++ b/Ghost.Editor.Core/Inspector/IInspectorService.cs @@ -1,6 +1,4 @@ -using Ghost.Editor.Contracts; - -namespace Ghost.Editor.Services.Contracts; +namespace Ghost.Editor.Core.Inspector; internal interface IInspectorService { diff --git a/Ghost.App/Services/InspectorService.cs b/Ghost.Editor.Core/Inspector/InspectorService.cs similarity index 75% rename from Ghost.App/Services/InspectorService.cs rename to Ghost.Editor.Core/Inspector/InspectorService.cs index b1bb2be..4a16c82 100644 --- a/Ghost.App/Services/InspectorService.cs +++ b/Ghost.Editor.Core/Inspector/InspectorService.cs @@ -1,7 +1,4 @@ -using Ghost.Editor.Core.Inspector; -using Ghost.Editor.Services.Contracts; - -namespace Ghost.Editor.Services; +namespace Ghost.Editor.Core.Inspector; public class InspectorService : IInspectorService { diff --git a/Ghost.Editor/Services/Contracts/INotificationService.cs b/Ghost.Editor.Core/Notifications/INotificationService.cs similarity index 51% rename from Ghost.Editor/Services/Contracts/INotificationService.cs rename to Ghost.Editor.Core/Notifications/INotificationService.cs index cef71e7..780f3f7 100644 --- a/Ghost.Editor/Services/Contracts/INotificationService.cs +++ b/Ghost.Editor.Core/Notifications/INotificationService.cs @@ -1,8 +1,9 @@ -using Ghost.Editor.Models; +using CommunityToolkit.WinUI.Behaviors; -namespace Ghost.Editor.Services.Contracts; +namespace Ghost.Editor.Core.Notifications; public interface INotificationService { public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null); + public void ShowNotification(Notification notification); } diff --git a/Ghost.Editor/Models/MessageType.cs b/Ghost.Editor.Core/Notifications/MessageType.cs similarity index 63% rename from Ghost.Editor/Models/MessageType.cs rename to Ghost.Editor.Core/Notifications/MessageType.cs index c559200..8e52904 100644 --- a/Ghost.Editor/Models/MessageType.cs +++ b/Ghost.Editor.Core/Notifications/MessageType.cs @@ -1,4 +1,4 @@ -namespace Ghost.Editor.Models; +namespace Ghost.Editor.Core.Notifications; public enum MessageType { diff --git a/Ghost.App/Services/NotificationService.cs b/Ghost.Editor.Core/Notifications/NotificationService.cs similarity index 91% rename from Ghost.App/Services/NotificationService.cs rename to Ghost.Editor.Core/Notifications/NotificationService.cs index 3915c2e..d49207c 100644 --- a/Ghost.App/Services/NotificationService.cs +++ b/Ghost.Editor.Core/Notifications/NotificationService.cs @@ -1,10 +1,7 @@ using CommunityToolkit.WinUI.Behaviors; -using Ghost.Editor.Models; -using Ghost.Editor.Services.Contracts; using Microsoft.UI.Xaml.Controls; -using System; -namespace Ghost.Editor.Services; +namespace Ghost.Editor.Core.Notifications; public class NotificationService : INotificationService { diff --git a/Ghost.Editor/Services/Contracts/IProgressService.cs b/Ghost.Editor.Core/Progress/IProgressService.cs similarity index 83% rename from Ghost.Editor/Services/Contracts/IProgressService.cs rename to Ghost.Editor.Core/Progress/IProgressService.cs index fce0331..0b1ee1b 100644 --- a/Ghost.Editor/Services/Contracts/IProgressService.cs +++ b/Ghost.Editor.Core/Progress/IProgressService.cs @@ -1,4 +1,4 @@ -namespace Ghost.Editor.Services.Contracts; +namespace Ghost.Editor.Core.Progress; public interface IProgressService { diff --git a/Ghost.App/Services/ProgressService.cs b/Ghost.Editor.Core/Progress/ProgressService.cs similarity index 96% rename from Ghost.App/Services/ProgressService.cs rename to Ghost.Editor.Core/Progress/ProgressService.cs index 14163ed..cbe8ed5 100644 --- a/Ghost.App/Services/ProgressService.cs +++ b/Ghost.Editor.Core/Progress/ProgressService.cs @@ -1,10 +1,9 @@ using CommunityToolkit.WinUI; -using Ghost.Editor.Services.Contracts; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System.Runtime.CompilerServices; -namespace Ghost.Editor.Services; +namespace Ghost.Editor.Core.Progress; public class ProgressService : IProgressService { diff --git a/Ghost.App/Resources/EditorIconSource.cs b/Ghost.Editor.Core/Resources/EditorIconSource.cs similarity index 89% rename from Ghost.App/Resources/EditorIconSource.cs rename to Ghost.Editor.Core/Resources/EditorIconSource.cs index 619a528..2913793 100644 --- a/Ghost.App/Resources/EditorIconSource.cs +++ b/Ghost.Editor.Core/Resources/EditorIconSource.cs @@ -1,6 +1,6 @@ using Microsoft.UI.Xaml.Controls; -namespace Ghost.Editor.Resources; +namespace Ghost.Editor.Core.Resources; public static class EditorIconSource { diff --git a/Ghost.Editor/Resources/FileExtensions.cs b/Ghost.Editor.Core/Resources/FileExtensions.cs similarity index 90% rename from Ghost.Editor/Resources/FileExtensions.cs rename to Ghost.Editor.Core/Resources/FileExtensions.cs index 9b38981..5448d7a 100644 --- a/Ghost.Editor/Resources/FileExtensions.cs +++ b/Ghost.Editor.Core/Resources/FileExtensions.cs @@ -1,4 +1,4 @@ -namespace Ghost.Editor.Resources; +namespace Ghost.Editor.Core.Resources; internal static class FileExtensions { diff --git a/Ghost.App/Resources/StaticResource.cs b/Ghost.Editor.Core/Resources/StaticResource.cs similarity index 82% rename from Ghost.App/Resources/StaticResource.cs rename to Ghost.Editor.Core/Resources/StaticResource.cs index 247849e..bb5a6bf 100644 --- a/Ghost.App/Resources/StaticResource.cs +++ b/Ghost.Editor.Core/Resources/StaticResource.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace Ghost.Editor.Resources; +namespace Ghost.Editor.Core.Resources; internal static class StaticResource { diff --git a/Ghost.App/Core/SceneGraph/EditorWorldManager.cs b/Ghost.Editor.Core/SceneGraph/EditorWorldManager.cs similarity index 93% rename from Ghost.App/Core/SceneGraph/EditorWorldManager.cs rename to Ghost.Editor.Core/SceneGraph/EditorWorldManager.cs index 566c5d7..d196a77 100644 --- a/Ghost.App/Core/SceneGraph/EditorWorldManager.cs +++ b/Ghost.Editor.Core/SceneGraph/EditorWorldManager.cs @@ -1,5 +1,6 @@ -using Ghost.Editor.Resources; -using Ghost.Editor.Services.Contracts; +using Ghost.Editor.Core.Progress; +using Ghost.Editor.Core.Resources; +using Ghost.Editor.Core.Utilities; using System.Text.Json; namespace Ghost.Editor.Core.SceneGraph; diff --git a/Ghost.App/Core/SceneGraph/EntityNode.cs b/Ghost.Editor.Core/SceneGraph/EntityNode.cs similarity index 82% rename from Ghost.App/Core/SceneGraph/EntityNode.cs rename to Ghost.Editor.Core/SceneGraph/EntityNode.cs index edc9bd7..c0dc1d3 100644 --- a/Ghost.App/Core/SceneGraph/EntityNode.cs +++ b/Ghost.Editor.Core/SceneGraph/EntityNode.cs @@ -1,6 +1,6 @@ using Ghost.Editor.Controls.Internal; using Ghost.Editor.Core.Inspector; -using Ghost.Editor.Resources; +using Ghost.Editor.Core.Resources; using Ghost.Engine.Editor; using Ghost.Entities; using Microsoft.UI.Text; @@ -13,16 +13,23 @@ namespace Ghost.Editor.Core.SceneGraph; public partial class EntityNode : SceneGraphNode { - private WorldNode _owner; - private readonly Entity _entity; + public WorldNode Owner + { + get; + set; + } + + public Entity Entity + { + get; + } - public Entity Entity => _entity; public override SceneGraphNodeType NodeType => SceneGraphNodeType.Entity; public EntityNode(WorldNode owner, Entity entity, string name) { - _owner = owner; - _entity = entity; + Owner = owner; + Entity = entity; Name = name; } } @@ -48,7 +55,7 @@ public partial class EntityNode : IInspectable }; var idText = new TextBlock { - Text = $"ID: {_entity.ID} Generation: {_entity.Generation}", + Text = $"ID: {Entity.ID} Generation: {Entity.Generation}", Margin = new Thickness(5, 7, 0, 0), Opacity = 0.75, Style = Application.Current.Resources["CaptionTextBlockStyle"] as Style @@ -79,20 +86,20 @@ public partial class EntityNode : IInspectable VerticalAlignment = VerticalAlignment.Top }; - foreach (var (typeHandle, componentPtr) in _owner.World.EntityManager.GetComponentsUnsafe(_entity)) + foreach (var (typeHandle, componentPtr) in Owner.World.EntityManager.GetComponentsUnsafe(Entity)) { if (componentPtr == IntPtr.Zero) { continue; } - var type = Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(typeHandle)); + var type = typeHandle.ToType(); if (type == null || type.GetCustomAttribute() != null) { continue; } - var dataView = new ComponentDataView(type.Name, _owner.World, _entity, type); + var dataView = new ComponentDataView(type.Name, Owner.World, Entity, type); root.Children.Add(dataView); } diff --git a/Ghost.App/Core/SceneGraph/SceneGraphHelpers.cs b/Ghost.Editor.Core/SceneGraph/SceneGraphHelpers.cs similarity index 100% rename from Ghost.App/Core/SceneGraph/SceneGraphHelpers.cs rename to Ghost.Editor.Core/SceneGraph/SceneGraphHelpers.cs diff --git a/Ghost.App/Core/SceneGraph/SceneGraphNode.cs b/Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs similarity index 100% rename from Ghost.App/Core/SceneGraph/SceneGraphNode.cs rename to Ghost.Editor.Core/SceneGraph/SceneGraphNode.cs diff --git a/Ghost.App/Core/SceneGraph/WorldNode.cs b/Ghost.Editor.Core/SceneGraph/WorldNode.cs similarity index 99% rename from Ghost.App/Core/SceneGraph/WorldNode.cs rename to Ghost.Editor.Core/SceneGraph/WorldNode.cs index 3ca7952..72d8c27 100644 --- a/Ghost.App/Core/SceneGraph/WorldNode.cs +++ b/Ghost.Editor.Core/SceneGraph/WorldNode.cs @@ -1,7 +1,7 @@ using Ghost.Editor.Core.AssetHandle; using Ghost.Editor.Core.Inspector; +using Ghost.Editor.Core.Resources; using Ghost.Editor.Core.Serializer; -using Ghost.Editor.Resources; using Ghost.Engine.Components; using Ghost.Entities; using Microsoft.UI.Xaml; diff --git a/Ghost.App/Core/Serializer/WorldNodeSerializer.cs b/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs similarity index 97% rename from Ghost.App/Core/Serializer/WorldNodeSerializer.cs rename to Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs index d669a2a..d298fbc 100644 --- a/Ghost.App/Core/Serializer/WorldNodeSerializer.cs +++ b/Ghost.Editor.Core/Serializer/WorldNodeSerializer.cs @@ -2,7 +2,6 @@ using Ghost.Engine.Utilities; using Ghost.Entities; using Ghost.Entities.Components; -using Ghost.Entities.Utilities; using System.Text.Json; using System.Text.Json.Serialization; @@ -107,7 +106,7 @@ internal class WorldNodeSerializer : JsonConverter { foreach (var kvp in value.World.ComponentStorage.ComponentPools) { - var type = TypeHandle.ToType(kvp.Key) ?? throw new Exception($"Type {kvp.Key} not found."); + var type = kvp.Key.ToType() ?? throw new Exception($"Type {kvp.Key} not found."); var typeName = type.AssemblyQualifiedName ?? type.Name; writer.WriteArray(typeName, kvp.Value.Enumerate(), data => diff --git a/Ghost.Editor.Core/Utilities/EditorApplication.cs b/Ghost.Editor.Core/Utilities/EditorApplication.cs new file mode 100644 index 0000000..b7e6b6d --- /dev/null +++ b/Ghost.Editor.Core/Utilities/EditorApplication.cs @@ -0,0 +1,26 @@ +using Microsoft.UI.Xaml; + +namespace Ghost.Editor.Core.Utilities; + +public static class EditorApplication +{ + private static IServiceProvider? _serviceProvider; + + public static Application Current => Application.Current; + + internal static void Initialize(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public static T GetService() + where T : class + { + if (_serviceProvider?.GetService(typeof(T)) is not T service) + { + throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs."); + } + + return service; + } +} \ No newline at end of file diff --git a/Ghost.App/Utilities/TypeCache.cs b/Ghost.Editor.Core/Utilities/TypeCache.cs similarity index 86% rename from Ghost.App/Utilities/TypeCache.cs rename to Ghost.Editor.Core/Utilities/TypeCache.cs index 766b435..19223da 100644 --- a/Ghost.App/Utilities/TypeCache.cs +++ b/Ghost.Editor.Core/Utilities/TypeCache.cs @@ -1,7 +1,7 @@ using Ghost.Entities; using System.Reflection; -namespace Ghost.Editor.Utilities; +namespace Ghost.Editor.Core.Utilities; public static class TypeCache { @@ -31,7 +31,7 @@ public static class ComponentTypeCache { var world = World.GetWorld(i); var typeHandles = world.ComponentStorage.ComponentPools.Keys; - _componentTypes[i] = typeHandles.Select(handle => Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(handle))).ToArray(); + _componentTypes[i] = typeHandles.Select(handle => handle.ToType()).ToArray(); } } diff --git a/Ghost.App/ActivationHandler.cs b/Ghost.Editor/ActivationHandler.cs similarity index 85% rename from Ghost.App/ActivationHandler.cs rename to Ghost.Editor/ActivationHandler.cs index 9ac4713..2909a85 100644 --- a/Ghost.App/ActivationHandler.cs +++ b/Ghost.Editor/ActivationHandler.cs @@ -1,7 +1,7 @@ using Ghost.Data.Resources; using Ghost.Data.Services; +using Ghost.Editor.Core.Utilities; using Microsoft.UI.Xaml; -using System.IO; namespace Ghost.Editor; @@ -24,5 +24,7 @@ internal static class ActivationHandler { FolderInitialization(); ProjectService.EnsureDefaultTemplate(); + + EditorApplication.Initialize(((App)(Application.Current)).Host.Services); } -} +} \ No newline at end of file diff --git a/Ghost.App/App.xaml b/Ghost.Editor/App.xaml similarity index 82% rename from Ghost.App/App.xaml rename to Ghost.Editor/App.xaml index d768f52..32a4449 100644 --- a/Ghost.App/App.xaml +++ b/Ghost.Editor/App.xaml @@ -1,14 +1,15 @@ - + diff --git a/Ghost.App/App.xaml.cs b/Ghost.Editor/App.xaml.cs similarity index 86% rename from Ghost.App/App.xaml.cs rename to Ghost.Editor/App.xaml.cs index 2d8668e..01b06df 100644 --- a/Ghost.App/App.xaml.cs +++ b/Ghost.Editor/App.xaml.cs @@ -1,6 +1,7 @@ using Ghost.Editor.Core.AppState; -using Ghost.Editor.Services; -using Ghost.Editor.Services.Contracts; +using Ghost.Editor.Core.Inspector; +using Ghost.Editor.Core.Notifications; +using Ghost.Editor.Core.Progress; using Ghost.Editor.Utilities; using Ghost.Engine.Services; using Microsoft.Extensions.DependencyInjection; @@ -15,16 +16,16 @@ namespace Ghost.Editor; /// /// Provides application-specific behavior to supplement the default Application class. /// -public partial class EditorApplication : Application +public partial class App : Application { private Window? _window; internal static Window? Window { - get => (Current as EditorApplication)!._window; + get => (Current as App)!._window; set { - if (Current is EditorApplication app) + if (Current is App app) { app._window = value; } @@ -40,7 +41,7 @@ public partial class EditorApplication : Application /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). /// - internal EditorApplication() + internal App() { InitializeComponent(); @@ -64,12 +65,12 @@ public partial class EditorApplication : Application internal static IServiceScope CreateScope() { - return (Current as EditorApplication)!.Host.Services.CreateScope(); + return (Current as App)!.Host.Services.CreateScope(); } public static T GetService() where T : class { - if ((Current as EditorApplication)!.Host.Services.GetService(typeof(T)) is not T service) + if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service) { throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs."); } diff --git a/Ghost.Editor/AssemblyInfo.cs b/Ghost.Editor/AssemblyInfo.cs index 5a54667..d3b1fb4 100644 --- a/Ghost.Editor/AssemblyInfo.cs +++ b/Ghost.Editor/AssemblyInfo.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Ghost.App")] \ No newline at end of file +[assembly: InternalsVisibleTo("Ghost.UnitTest")] diff --git a/Ghost.Editor/AssetHandle/Asset.cs b/Ghost.Editor/AssetHandle/Asset.cs deleted file mode 100644 index 22433ab..0000000 --- a/Ghost.Editor/AssetHandle/Asset.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Ghost.Editor.AssetHandle; - -public abstract class Asset -{ - /// - /// Get the Guid of the asset. - /// - public Guid GUID - { - get; - } = Guid.NewGuid(); - - /// - /// True if the asset is a folder, false if it is a file. - /// - public bool IsFolder - { - get; - } - - internal void GenerateMetadata() - { - } -} \ No newline at end of file diff --git a/Ghost.Editor/AssetHandle/AssetDatabase.cs b/Ghost.Editor/AssetHandle/AssetDatabase.cs deleted file mode 100644 index 846a4b5..0000000 --- a/Ghost.Editor/AssetHandle/AssetDatabase.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Diagnostics; -using System.Reflection; - -namespace Ghost.Editor.AssetHandle; - -public static class AssetDatabase -{ - private static readonly Dictionary> _assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary> _asyncAssetOpenHandler = new(StringComparer.OrdinalIgnoreCase); - - static AssetDatabase() - { - RegisterAssetHandles(); - } - - private static void RegisterAssetHandles() - { - var methods = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(a => a.GetTypes()) - .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - .Where(m => m.GetCustomAttribute() != null && - m.GetParameters().Length == 1 && - m.GetParameters()[0].ParameterType == typeof(string)); - - foreach (var method in methods) - { - var attr = method.GetCustomAttribute()!; - var del = (Action)Delegate.CreateDelegate(typeof(Action), method); - foreach (var ext in attr.Extensions) - { - if (_assetOpenHandlers.ContainsKey(ext)) - { - throw new InvalidOperationException($"Duplicate handler for extension '{ext}'"); - } - - _assetOpenHandlers[ext] = del; - } - } - - var asyncMethods = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(a => a.GetTypes()) - .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - .Where(m => m.GetCustomAttribute() != null && - m.GetParameters().Length == 1 && - m.GetParameters()[0].ParameterType == typeof(string) && - m.ReturnType == typeof(Task)); - - foreach (var method in asyncMethods) - { - var attr = method.GetCustomAttribute()!; - var del = (Func)Delegate.CreateDelegate(typeof(Func), method); - foreach (var ext in attr.Extensions) - { - if (_asyncAssetOpenHandler.ContainsKey(ext)) - { - throw new InvalidOperationException($"Duplicate async handler for extension '{ext}'"); - } - _asyncAssetOpenHandler[ext] = del; - } - } - } - - public static async ValueTask OpenAsset(string path) - { - var extension = Path.GetExtension(path); - if (_assetOpenHandlers.TryGetValue(extension, out var handler)) - { - handler(path); - } - else if (_asyncAssetOpenHandler.TryGetValue(extension, out var asyncHandler)) - { - await asyncHandler(path); - } - else - { - Process.Start(new ProcessStartInfo(path) - { - UseShellExecute = true - }); - } - - } -} \ No newline at end of file diff --git a/Ghost.Editor/AssetHandle/AssetOpenHandlerAttribute .cs b/Ghost.Editor/AssetHandle/AssetOpenHandlerAttribute .cs deleted file mode 100644 index 084b565..0000000 --- a/Ghost.Editor/AssetHandle/AssetOpenHandlerAttribute .cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Ghost.Editor.AssetHandle; - -[AttributeUsage(AttributeTargets.Method)] -public class AssetOpenHandlerAttribute : Attribute -{ - public string[] Extensions - { - get; - } - - public AssetOpenHandlerAttribute(params string[] extensions) - { - Extensions = extensions.Select(e => e.StartsWith(".") ? e.ToLowerInvariant() : "." + e.ToLowerInvariant()).ToArray(); - } -} - -[AttributeUsage(AttributeTargets.Method)] -public class AsyncAssetOpenHandlerAttribute : Attribute -{ - public string[] Extensions - { - get; - } - - public AsyncAssetOpenHandlerAttribute(params string[] extensions) - { - Extensions = extensions.Select(e => e.StartsWith(".") ? e.ToLowerInvariant() : "." + e.ToLowerInvariant()).ToArray(); - } -} \ No newline at end of file diff --git a/Ghost.App/Assets/Icon.altform-lightunplated_targetsize-16.png b/Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-16.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-lightunplated_targetsize-16.png rename to Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-16.png diff --git a/Ghost.App/Assets/Icon.altform-lightunplated_targetsize-24.png b/Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-24.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-lightunplated_targetsize-24.png rename to Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-24.png diff --git a/Ghost.App/Assets/Icon.altform-lightunplated_targetsize-256.png b/Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-256.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-lightunplated_targetsize-256.png rename to Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-256.png diff --git a/Ghost.App/Assets/Icon.altform-lightunplated_targetsize-32.png b/Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-32.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-lightunplated_targetsize-32.png rename to Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-32.png diff --git a/Ghost.App/Assets/Icon.altform-lightunplated_targetsize-48.png b/Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-48.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-lightunplated_targetsize-48.png rename to Ghost.Editor/Assets/Icon.altform-lightunplated_targetsize-48.png diff --git a/Ghost.App/Assets/Icon.altform-unplated_targetsize-16.png b/Ghost.Editor/Assets/Icon.altform-unplated_targetsize-16.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-unplated_targetsize-16.png rename to Ghost.Editor/Assets/Icon.altform-unplated_targetsize-16.png diff --git a/Ghost.App/Assets/Icon.altform-unplated_targetsize-24.png b/Ghost.Editor/Assets/Icon.altform-unplated_targetsize-24.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-unplated_targetsize-24.png rename to Ghost.Editor/Assets/Icon.altform-unplated_targetsize-24.png diff --git a/Ghost.App/Assets/Icon.altform-unplated_targetsize-256.png b/Ghost.Editor/Assets/Icon.altform-unplated_targetsize-256.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-unplated_targetsize-256.png rename to Ghost.Editor/Assets/Icon.altform-unplated_targetsize-256.png diff --git a/Ghost.App/Assets/Icon.altform-unplated_targetsize-32.png b/Ghost.Editor/Assets/Icon.altform-unplated_targetsize-32.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-unplated_targetsize-32.png rename to Ghost.Editor/Assets/Icon.altform-unplated_targetsize-32.png diff --git a/Ghost.App/Assets/Icon.altform-unplated_targetsize-48.png b/Ghost.Editor/Assets/Icon.altform-unplated_targetsize-48.png similarity index 100% rename from Ghost.App/Assets/Icon.altform-unplated_targetsize-48.png rename to Ghost.Editor/Assets/Icon.altform-unplated_targetsize-48.png diff --git a/Ghost.App/Assets/Icon.scale-100.png b/Ghost.Editor/Assets/Icon.scale-100.png similarity index 100% rename from Ghost.App/Assets/Icon.scale-100.png rename to Ghost.Editor/Assets/Icon.scale-100.png diff --git a/Ghost.App/Assets/Icon.scale-125.png b/Ghost.Editor/Assets/Icon.scale-125.png similarity index 100% rename from Ghost.App/Assets/Icon.scale-125.png rename to Ghost.Editor/Assets/Icon.scale-125.png diff --git a/Ghost.App/Assets/Icon.scale-150.png b/Ghost.Editor/Assets/Icon.scale-150.png similarity index 100% rename from Ghost.App/Assets/Icon.scale-150.png rename to Ghost.Editor/Assets/Icon.scale-150.png diff --git a/Ghost.App/Assets/Icon.scale-200.png b/Ghost.Editor/Assets/Icon.scale-200.png similarity index 100% rename from Ghost.App/Assets/Icon.scale-200.png rename to Ghost.Editor/Assets/Icon.scale-200.png diff --git a/Ghost.App/Assets/Icon.scale-400.png b/Ghost.Editor/Assets/Icon.scale-400.png similarity index 100% rename from Ghost.App/Assets/Icon.scale-400.png rename to Ghost.Editor/Assets/Icon.scale-400.png diff --git a/Ghost.App/Assets/Icon.targetsize-16.png b/Ghost.Editor/Assets/Icon.targetsize-16.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-16.png rename to Ghost.Editor/Assets/Icon.targetsize-16.png diff --git a/Ghost.App/Assets/Icon.targetsize-16_altform-lightunplated.png b/Ghost.Editor/Assets/Icon.targetsize-16_altform-lightunplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-16_altform-lightunplated.png rename to Ghost.Editor/Assets/Icon.targetsize-16_altform-lightunplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-16_altform-unplated.png b/Ghost.Editor/Assets/Icon.targetsize-16_altform-unplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-16_altform-unplated.png rename to Ghost.Editor/Assets/Icon.targetsize-16_altform-unplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-24.png b/Ghost.Editor/Assets/Icon.targetsize-24.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-24.png rename to Ghost.Editor/Assets/Icon.targetsize-24.png diff --git a/Ghost.App/Assets/Icon.targetsize-24_altform-lightunplated.png b/Ghost.Editor/Assets/Icon.targetsize-24_altform-lightunplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-24_altform-lightunplated.png rename to Ghost.Editor/Assets/Icon.targetsize-24_altform-lightunplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-24_altform-unplated.png b/Ghost.Editor/Assets/Icon.targetsize-24_altform-unplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-24_altform-unplated.png rename to Ghost.Editor/Assets/Icon.targetsize-24_altform-unplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-256.png b/Ghost.Editor/Assets/Icon.targetsize-256.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-256.png rename to Ghost.Editor/Assets/Icon.targetsize-256.png diff --git a/Ghost.App/Assets/Icon.targetsize-256_altform-lightunplated.png b/Ghost.Editor/Assets/Icon.targetsize-256_altform-lightunplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-256_altform-lightunplated.png rename to Ghost.Editor/Assets/Icon.targetsize-256_altform-lightunplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-256_altform-unplated.png b/Ghost.Editor/Assets/Icon.targetsize-256_altform-unplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-256_altform-unplated.png rename to Ghost.Editor/Assets/Icon.targetsize-256_altform-unplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-32.png b/Ghost.Editor/Assets/Icon.targetsize-32.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-32.png rename to Ghost.Editor/Assets/Icon.targetsize-32.png diff --git a/Ghost.App/Assets/Icon.targetsize-32_altform-lightunplated.png b/Ghost.Editor/Assets/Icon.targetsize-32_altform-lightunplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-32_altform-lightunplated.png rename to Ghost.Editor/Assets/Icon.targetsize-32_altform-lightunplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-32_altform-unplated.png b/Ghost.Editor/Assets/Icon.targetsize-32_altform-unplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-32_altform-unplated.png rename to Ghost.Editor/Assets/Icon.targetsize-32_altform-unplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-48.png b/Ghost.Editor/Assets/Icon.targetsize-48.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-48.png rename to Ghost.Editor/Assets/Icon.targetsize-48.png diff --git a/Ghost.App/Assets/Icon.targetsize-48_altform-lightunplated.png b/Ghost.Editor/Assets/Icon.targetsize-48_altform-lightunplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-48_altform-lightunplated.png rename to Ghost.Editor/Assets/Icon.targetsize-48_altform-lightunplated.png diff --git a/Ghost.App/Assets/Icon.targetsize-48_altform-unplated.png b/Ghost.Editor/Assets/Icon.targetsize-48_altform-unplated.png similarity index 100% rename from Ghost.App/Assets/Icon.targetsize-48_altform-unplated.png rename to Ghost.Editor/Assets/Icon.targetsize-48_altform-unplated.png diff --git a/Ghost.App/Assets/LockScreenLogo.scale-200.png b/Ghost.Editor/Assets/LockScreenLogo.scale-200.png similarity index 100% rename from Ghost.App/Assets/LockScreenLogo.scale-200.png rename to Ghost.Editor/Assets/LockScreenLogo.scale-200.png diff --git a/Ghost.App/Assets/SplashScreen.scale-200.png b/Ghost.Editor/Assets/SplashScreen.scale-200.png similarity index 100% rename from Ghost.App/Assets/SplashScreen.scale-200.png rename to Ghost.Editor/Assets/SplashScreen.scale-200.png diff --git a/Ghost.App/Assets/Square150x150Logo.scale-200.png b/Ghost.Editor/Assets/Square150x150Logo.scale-200.png similarity index 100% rename from Ghost.App/Assets/Square150x150Logo.scale-200.png rename to Ghost.Editor/Assets/Square150x150Logo.scale-200.png diff --git a/Ghost.App/Assets/StoreLogo.png b/Ghost.Editor/Assets/StoreLogo.png similarity index 100% rename from Ghost.App/Assets/StoreLogo.png rename to Ghost.Editor/Assets/StoreLogo.png diff --git a/Ghost.App/Assets/Wide310x150Logo.scale-200.png b/Ghost.Editor/Assets/Wide310x150Logo.scale-200.png similarity index 100% rename from Ghost.App/Assets/Wide310x150Logo.scale-200.png rename to Ghost.Editor/Assets/Wide310x150Logo.scale-200.png diff --git a/Ghost.App/Assets/icon-256.ico b/Ghost.Editor/Assets/icon-256.ico similarity index 100% rename from Ghost.App/Assets/icon-256.ico rename to Ghost.Editor/Assets/icon-256.ico diff --git a/Ghost.App/Assets/icon-256.png b/Ghost.Editor/Assets/icon-256.png similarity index 100% rename from Ghost.App/Assets/icon-256.png rename to Ghost.Editor/Assets/icon-256.png diff --git a/Ghost.App/Components/HierarchyEditor.cs b/Ghost.Editor/Components/HierarchyEditor.cs similarity index 88% rename from Ghost.App/Components/HierarchyEditor.cs rename to Ghost.Editor/Components/HierarchyEditor.cs index 706025e..ec16f55 100644 --- a/Ghost.App/Components/HierarchyEditor.cs +++ b/Ghost.Editor/Components/HierarchyEditor.cs @@ -4,7 +4,7 @@ using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.Components; //[CustomEditor(typeof(Hierarchy))] -internal class HierarchyEditor : IComponentEditor +internal class HierarchyEditor : ComponentEditor { public void Create(ComponentObject componentObject, StackPanel container) { diff --git a/Ghost.App/Components/LocalToWorldEditor.cs b/Ghost.Editor/Components/LocalToWorldEditor.cs similarity index 57% rename from Ghost.App/Components/LocalToWorldEditor.cs rename to Ghost.Editor/Components/LocalToWorldEditor.cs index 6083106..08cb11f 100644 --- a/Ghost.App/Components/LocalToWorldEditor.cs +++ b/Ghost.Editor/Components/LocalToWorldEditor.cs @@ -7,13 +7,13 @@ using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.Components; [CustomEditor(typeof(LocalToWorld))] -internal class LocalToWorldEditor : IComponentEditor +internal class LocalToWorldEditor : ComponentEditor { private Vector3Field _translationField = null!; private Vector3Field _rotationField = null!; private Vector3Field _scaleField = null!; - public void Create(ComponentObject componentObject, StackPanel container) + public override void Create(StackPanel container) { _translationField = new Vector3Field(); _rotationField = new Vector3Field(); @@ -21,22 +21,22 @@ internal class LocalToWorldEditor : IComponentEditor _translationField.OnValueChanged += (s, e) => { - var data = componentObject.GetData(); - MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var oldRotation, out var oldScale); + var data = ComponentObject.GetData(); + MatrixUtility.GetTRS(data.ValueRO.matrix, out var _, out var oldRotation, out var oldScale); data.ValueRW.matrix = MatrixUtility.CreateTRS(e.NewValue, oldRotation, oldScale); }; _rotationField.OnValueChanged += (s, e) => { - var data = componentObject.GetData(); - MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var oldRotation, out var oldScale); + var data = ComponentObject.GetData(); + MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var _, out var oldScale); data.ValueRW.matrix = MatrixUtility.CreateTRS(oldTranslation, e.NewValue.ToQuaternion(), oldScale); }; _scaleField.OnValueChanged += (s, e) => { - var data = componentObject.GetData(); - MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var oldRotation, out var oldScale); + var data = ComponentObject.GetData(); + MatrixUtility.GetTRS(data.ValueRO.matrix, out var oldTranslation, out var oldRotation, out var _); data.ValueRW.matrix = MatrixUtility.CreateTRS(oldTranslation, oldRotation, e.NewValue); }; @@ -45,28 +45,17 @@ internal class LocalToWorldEditor : IComponentEditor container.Children.Add(new PropertyField() { Label = "Scale", Content = _scaleField }); } - public void Update(ComponentObject componentObject) + public override void Update() { - var data = componentObject.GetData(); + var data = ComponentObject.GetData(); MatrixUtility.GetTRS(data.ValueRO.matrix, out var translation, out var rotation, out var scale); - if (_translationField.FocusState == Microsoft.UI.Xaml.FocusState.Unfocused) - { - _translationField.Value = translation; - } - - if (_rotationField.FocusState == Microsoft.UI.Xaml.FocusState.Unfocused) - { - _rotationField.Value = VectorUtility.CreateFromQuaternion(rotation); - } - - if (_scaleField.FocusState == Microsoft.UI.Xaml.FocusState.Unfocused) - { - _scaleField.Value = scale; - } + _translationField.Value = translation; + _rotationField.Value = VectorUtility.CreateFromQuaternion(rotation); + _scaleField.Value = scale; } - public void Destroy(ComponentObject componentObject) + public override void Destroy() { } } \ No newline at end of file diff --git a/Ghost.Editor/Contracts/IInspectable.cs b/Ghost.Editor/Contracts/IInspectable.cs deleted file mode 100644 index f959a81..0000000 --- a/Ghost.Editor/Contracts/IInspectable.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.UI.Xaml; - -namespace Ghost.Editor.Contracts; -internal interface IInspectable -{ - public UIElement HeaderContent(); - public UIElement InspectorContent(); -} \ No newline at end of file diff --git a/Ghost.App/Controls/ViewModelPage.cs b/Ghost.Editor/Controls/ViewModelPage.cs similarity index 95% rename from Ghost.App/Controls/ViewModelPage.cs rename to Ghost.Editor/Controls/ViewModelPage.cs index 06f3870..91014e7 100644 --- a/Ghost.App/Controls/ViewModelPage.cs +++ b/Ghost.Editor/Controls/ViewModelPage.cs @@ -1,5 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; -using Ghost.Editor.Contracts; +using Ghost.Editor.Core.Contracts; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; diff --git a/Ghost.App/Core/AppState/EditorState.cs b/Ghost.Editor/Core/AppState/EditorState.cs similarity index 69% rename from Ghost.App/Core/AppState/EditorState.cs rename to Ghost.Editor/Core/AppState/EditorState.cs index 0fa6514..2abf0cf 100644 --- a/Ghost.App/Core/AppState/EditorState.cs +++ b/Ghost.Editor/Core/AppState/EditorState.cs @@ -13,14 +13,14 @@ internal class EditorState : IAppState public Task OnExitingAsync() { - if (EditorApplication.Window == _window) + if (App.Window == _window) { - EditorApplication.Window = null; + App.Window = null; } return Task.CompletedTask; } - public async Task OnEnteringAsync(object? parameter) + public Task OnEnteringAsync(object? parameter) { if (parameter is not ProjectMetadataInfo metadataInfo) { @@ -29,13 +29,12 @@ internal class EditorState : IAppState ProjectService.CurrentProject = metadataInfo; - _engineCore = EditorApplication.GetService(); - await _engineCore.StartAsync(new Engine.Models.LaunchArgument()); - - _window = EditorApplication.GetService(); + _window = App.GetService(); _window.Activate(); - EditorApplication.Window = _window; + App.Window = _window; + + return Task.CompletedTask; } public async Task OnExitedAsync() @@ -45,18 +44,20 @@ internal class EditorState : IAppState await _engineCore.ShutDownAsync(); } - if (EditorApplication.Window == _window) + if (App.Window == _window) { - EditorApplication.Window = null; + App.Window = null; } _window?.Close(); _window = null; } - public Task OnEnteredAsync(object? parameter) + public async Task OnEnteredAsync(object? parameter) { AssetDatabase.Initialize(); - return Task.CompletedTask; + + _engineCore = App.GetService(); + await _engineCore.StartAsync(new Engine.Models.LaunchArgument()); } } \ No newline at end of file diff --git a/Ghost.App/Core/AppState/LandingState.cs b/Ghost.Editor/Core/AppState/LandingState.cs similarity index 69% rename from Ghost.App/Core/AppState/LandingState.cs rename to Ghost.Editor/Core/AppState/LandingState.cs index e6f7ec4..a9936f2 100644 --- a/Ghost.App/Core/AppState/LandingState.cs +++ b/Ghost.Editor/Core/AppState/LandingState.cs @@ -10,17 +10,17 @@ internal class LandingState : IAppState public Task OnExitingAsync() { - if (EditorApplication.Window == _window) + if (App.Window == _window) { - EditorApplication.Window = null; + App.Window = null; } return Task.CompletedTask; } public Task OnEnteringAsync(object? parameter) { - _window = EditorApplication.GetService(); - EditorApplication.Window = _window; + _window = App.GetService(); + App.Window = _window; _window.Activate(); return Task.CompletedTask; @@ -28,9 +28,9 @@ internal class LandingState : IAppState public Task OnExitedAsync() { - if (EditorApplication.Window == _window) + if (App.Window == _window) { - EditorApplication.Window = null; + App.Window = null; } _window?.Close(); diff --git a/Ghost.Editor/EditorApplication.cs b/Ghost.Editor/EditorApplication.cs deleted file mode 100644 index 54c8ca0..0000000 --- a/Ghost.Editor/EditorApplication.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Ghost.Editor; - -public static class EditorApplication -{ - private static IServiceProvider? _serviceProvider; - - public static bool IsInitialized => _serviceProvider != null; - - internal static void Activate(object? parameters, IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - - public static T GetService() where T : class - { - if (!IsInitialized) - { - throw new InvalidOperationException("EditorApplication is not initialized."); - } - - if (_serviceProvider!.GetService(typeof(T)) is not T service) - { - throw new ArgumentException($"{typeof(T)} needs to be registered in the service provider."); - } - - return service; - } -} \ No newline at end of file diff --git a/Ghost.Editor/Ghost.Editor.csproj b/Ghost.Editor/Ghost.Editor.csproj index a666739..a28fcaf 100644 --- a/Ghost.Editor/Ghost.Editor.csproj +++ b/Ghost.Editor/Ghost.Editor.csproj @@ -1,23 +1,167 @@ + WinExe net9.0-windows10.0.22621.0 10.0.17763.0 - Ghost.Editor + x86;x64;ARM64 win-x86;win-x64;win-arm64 + win-$(Platform).pubxml true - enable + true preview - enable - 10.0.20348.0 - - - + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + + + true + + + + + False + True + True + enable + 10.0.20348.0 + app.manifest + False + False + Ghost.Editor + enable + \ No newline at end of file diff --git a/Ghost.App/Models/AssetItem.cs b/Ghost.Editor/Models/AssetItem.cs similarity index 100% rename from Ghost.App/Models/AssetItem.cs rename to Ghost.Editor/Models/AssetItem.cs diff --git a/Ghost.App/Models/ExplorerItem.cs b/Ghost.Editor/Models/ExplorerItem.cs similarity index 100% rename from Ghost.App/Models/ExplorerItem.cs rename to Ghost.Editor/Models/ExplorerItem.cs diff --git a/Ghost.Editor/Models/WorldAsset.cs b/Ghost.Editor/Models/WorldAsset.cs deleted file mode 100644 index d601f04..0000000 --- a/Ghost.Editor/Models/WorldAsset.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Ghost.Editor.AssetHandle; -using Ghost.Editor.Resources; -using Ghost.Editor.SceneGraph; - -namespace Ghost.Editor.Models; - -public class WorldAsset : Asset -{ - [AsyncAssetOpenHandler(FileExtensions.SCENE_FILE_EXTENSION)] - public static async Task Open(string path) - { - await EditorWorldManager.LoadWorld(path); - } -} \ No newline at end of file diff --git a/Ghost.App/Package.appxmanifest b/Ghost.Editor/Package.appxmanifest similarity index 100% rename from Ghost.App/Package.appxmanifest rename to Ghost.Editor/Package.appxmanifest diff --git a/Ghost.App/Properties/launchSettings.json b/Ghost.Editor/Properties/launchSettings.json similarity index 100% rename from Ghost.App/Properties/launchSettings.json rename to Ghost.Editor/Properties/launchSettings.json diff --git a/Ghost.Editor/SceneGraph/EditorWorldManager.cs b/Ghost.Editor/SceneGraph/EditorWorldManager.cs deleted file mode 100644 index 4941d72..0000000 --- a/Ghost.Editor/SceneGraph/EditorWorldManager.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Ghost.Editor.Resources; -using Ghost.Editor.Services.Contracts; -using Ghost.Engine.Resources; -using System.Text.Json; - -namespace Ghost.Editor.SceneGraph; - -public enum OpenWorldMode -{ - Single, - Additive, - AdditiveWithoutLoading -} - -public static class EditorWorldManager -{ - // TODO: Use guid keys instead of string paths for better performance and uniqueness - private static readonly Dictionary _loadedWorlds = new(); - public static IEnumerable LoadedWorlds => _loadedWorlds.Values; - - public static event Action? OnWorldLoaded; - public static event Action? OnWorldUnloaded; - - public static async Task LoadWorld(string worldPath) - { - if (_loadedWorlds.ContainsKey(worldPath) - || !File.Exists(worldPath) - || Path.GetExtension(worldPath) != FileExtensions.SCENE_FILE_EXTENSION) - { - return; - } - - var progressService = EditorApplication.GetService(); - progressService.ShowIndeterminateProgress("Loading world..."); - - foreach (var world in _loadedWorlds) - { - world.Value.Unload(); - OnWorldUnloaded?.Invoke(world.Value); - } - - await using var readStream = new FileStream(worldPath, FileMode.Open, FileAccess.Read, FileShare.Read); - var deserializedScene = await JsonSerializer.DeserializeAsync(readStream, StaticResource.defaultSerializerOptions) ?? throw new Exception("Deserialization failed."); - - _loadedWorlds.Clear(); - - _loadedWorlds[worldPath] = deserializedScene; - await deserializedScene.LoadAsync(); - - progressService.HideProgress(); - OnWorldLoaded?.Invoke(deserializedScene); - } -} \ No newline at end of file diff --git a/Ghost.Editor/SceneGraph/EntityNode.cs b/Ghost.Editor/SceneGraph/EntityNode.cs deleted file mode 100644 index fe0b992..0000000 --- a/Ghost.Editor/SceneGraph/EntityNode.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Ghost.Entities; - -namespace Ghost.Editor.SceneGraph; - -public partial class EntityNode : SceneGraphNode -{ - private readonly Entity _entity; - - public Entity Entity => _entity; - public override SceneGraphNodeType NodeType => SceneGraphNodeType.Entity; - - public EntityNode(Entity entity, string name) - { - _entity = entity; - Name = name; - } - - internal EntityNode() - { - } -} \ No newline at end of file diff --git a/Ghost.Editor/SceneGraph/SceneGraphHelpers.cs b/Ghost.Editor/SceneGraph/SceneGraphHelpers.cs deleted file mode 100644 index 309d559..0000000 --- a/Ghost.Editor/SceneGraph/SceneGraphHelpers.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Ghost.Engine.Components; -using Ghost.Entities; - -namespace Ghost.Editor.SceneGraph; - -public class SceneGraphHelpers -{ - /// - /// Creates a new entity with default components. - /// - /// The world context where the entity will be created. - /// The entity to be wrapped in the . - public static EntityNode CreateEntityNode(World world, Entity entity, string name) - { - world.EntityManager.AddComponent(entity, LocalToWorld.Identity); - world.EntityManager.AddComponent(entity, Hierarchy.Root); - return new EntityNode(entity, name); - } - - /// - /// Creates a new and entity with default components. - /// - /// The world context where the entity will be created. - public static EntityNode CreateEntityNode(World world, string name) - { - var entity = world.EntityManager.CreateEntity(); - return CreateEntityNode(world, entity, name); - } - - /// - /// Attaches childEntity to parentEntity in the scene graph. - /// - /// The world context where the entities exist. - /// The parent entity to which the child will be attached. - /// The child entity to be attached. - public static void AttachChild(WorldNode scene, EntityNode parentNode, EntityNode childNode) - { - // 1) If the child already has a parent, detach it first - var childHierarchy = scene.World.EntityManager.GetComponent(childNode.Entity); - if (childHierarchy.ValueRO.parent != Entity.Invalid) - { - DetachFromParent(scene, childNode); - } - - // 2) Link child to new parent - childHierarchy.ValueRW.parent = parentNode.Entity; - - // 3) Insert child at the head of parent's child list - var parentHierarchy = scene.World.EntityManager.GetComponent(parentNode.Entity); - - childHierarchy.ValueRW.nextSibling = parentHierarchy.ValueRO.firstChild; - parentHierarchy.ValueRW.firstChild = childNode.Entity; - - // 4) Write back - scene.World.EntityManager.SetComponent(parentNode.Entity, in parentHierarchy.ValueRO); - scene.World.EntityManager.SetComponent(childNode.Entity, in childHierarchy.ValueRO); - - // 5) Update children list in parent node - parentNode.AddChild(childNode); - } - - /// - /// Detaches the specified entity from its parent in the scene graph. - /// - /// The world context where the entities exist. - /// The entity to detach from its parent. - public static void DetachFromParent(WorldNode scene, EntityNode node) - { - var hierarchy = scene.World.EntityManager.GetComponent(node.Entity); - var parent = hierarchy.ValueRO.parent; - if (parent == Entity.Invalid) - { - return; // already root - } - - var parentHierarchy = scene.World.EntityManager.GetComponent(parent); - - // If entity is the first child, simply move head - if (parentHierarchy.ValueRO.firstChild == node.Entity) - { - parentHierarchy.ValueRW.firstChild = hierarchy.ValueRO.nextSibling; - } - else - { - // Otherwise, find the previous sibling in the linked list - var prevSibling = parentHierarchy.ValueRO.firstChild; - while (prevSibling != Entity.Invalid) - { - var prevHierarchy = scene.World.EntityManager.GetComponent(prevSibling); - if (prevHierarchy.ValueRW.nextSibling == node.Entity) - { - prevHierarchy.ValueRW.nextSibling = hierarchy.ValueRO.nextSibling; - scene.World.EntityManager.SetComponent(prevSibling, in prevHierarchy.ValueRO); - break; - } - - prevSibling = prevHierarchy.ValueRO.nextSibling; - } - } - - // Clear child's references - hierarchy.ValueRW.parent = Entity.Invalid; - hierarchy.ValueRW.nextSibling = Entity.Invalid; - - // Write back - scene.World.EntityManager.SetComponent(parent, in parentHierarchy.ValueRO); - scene.World.EntityManager.SetComponent(node.Entity, in hierarchy.ValueRO); - - // Remove from parent's children list - scene.EntityNodeLookup[parent].RemoveChild(node); - } -} \ No newline at end of file diff --git a/Ghost.Editor/SceneGraph/SceneGraphNode.cs b/Ghost.Editor/SceneGraph/SceneGraphNode.cs deleted file mode 100644 index f177ce1..0000000 --- a/Ghost.Editor/SceneGraph/SceneGraphNode.cs +++ /dev/null @@ -1,54 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using System.Collections.ObjectModel; - -namespace Ghost.Editor.SceneGraph; - -public enum SceneGraphNodeType -{ - Scene, - Entity, -} - -public abstract partial class SceneGraphNode : ObservableObject -{ - public ObservableCollection? Children - { - get; - private set; - } - - [ObservableProperty] - public partial string Name - { - get; - set; - } - - public abstract SceneGraphNodeType NodeType - { - get; - } - - public int ChildCount => Children?.Count ?? 0; - - public virtual void AddChild(SceneGraphNode child) - { - Children ??= new(); - Children.Add(child); - } - - public virtual bool RemoveChild(SceneGraphNode child) - { - return Children?.Remove(child) ?? false; - } - - public SceneGraphNode GetChild(int index) - { - if (Children == null || index < 0 || index >= Children.Count) - { - throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range."); - } - - return Children[index]; - } -} \ No newline at end of file diff --git a/Ghost.Editor/SceneGraph/WorldNode.cs b/Ghost.Editor/SceneGraph/WorldNode.cs deleted file mode 100644 index ddc6be6..0000000 --- a/Ghost.Editor/SceneGraph/WorldNode.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Ghost.Editor.Contracts; -using Ghost.Editor.Serializer; -using Ghost.Engine.Components; -using Ghost.Entities; -using Microsoft.UI.Xaml; -using System.Text.Json.Serialization; - -namespace Ghost.Editor.SceneGraph; - -[JsonConverter(typeof(WorldNodeSerializer))] -public partial class WorldNode : SceneGraphNode, IEquatable -{ - private World _world; - private Dictionary _entityNodeLookup = new(); - - public World World => _world; - public Dictionary EntityNodeLookup => _entityNodeLookup; - - public override SceneGraphNodeType NodeType => SceneGraphNodeType.Scene; - - public WorldNode(World world, string name) - { - _world = world; - Name = name; - } - - internal WorldNode() - { - _world = World.Create(); - } - - private void UpdateLookup(Entity key, EntityNode value) - { - _entityNodeLookup[key] = value; - if (value.Children == null) - { - return; - } - - foreach (var child in value.Children) - { - if (child is EntityNode entityChild) - { - UpdateLookup(entityChild.Entity, entityChild); - } - } - } - - public override void AddChild(SceneGraphNode child) - { - if (child is not EntityNode entityNode) - { - throw new ArgumentException("Child must be of type EntityNode.", nameof(child)); - } - - base.AddChild(entityNode); - UpdateLookup(entityNode.Entity, entityNode); - } - - public override bool RemoveChild(SceneGraphNode child) - { - if (child is not EntityNode entityNode) - { - throw new ArgumentException("Child must be of type EntityNode.", nameof(child)); - } - - var result = base.RemoveChild(child); - if (result) - { - _entityNodeLookup.Remove(entityNode.Entity); - } - - return result; - } - - private EntityNode BuildNodeRecursive(Entity entity, World world) - { - if (!_entityNodeLookup.TryGetValue(entity, out var node)) - { - node = new EntityNode(entity, "New Entity"); - _entityNodeLookup[entity] = node; - } - - var hc = world.EntityManager.GetComponent(entity); - var child = hc.ValueRO.firstChild; - - while (child != Entity.Invalid) - { - node.AddChild(BuildNodeRecursive(child, world)); - var childHC = world.EntityManager.GetComponent(child); - child = childHC.ValueRO.nextSibling; - } - - return node; - } - - private void BuildGraph() - { - foreach (var (entity, hierarchy) in _world.Query()) - { - if (hierarchy.ValueRO.parent == Entity.Invalid) - { - var node = BuildNodeRecursive(entity, _world); - AddChild(node); - } - } - } - - public Task LoadAsync() - { - return Task.Run(BuildGraph); - } - - public void Unload() - { - _world.Dispose(); - _world = null!; - - Children?.Clear(); - _entityNodeLookup.Clear(); - } - - public override string ToString() - { - return $"WorldNode: {Name} (World ID: {_world.ID})"; - } - - public override int GetHashCode() - { - return HashCode.Combine(_world, Name); - } - - public override bool Equals(object? obj) - { - return obj is WorldNode other && Equals(other); - } - - public bool Equals(WorldNode? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return _world.Equals(other._world) && Name == other.Name; - } - - public static bool operator ==(WorldNode? left, WorldNode? right) - { - if (left is null) - { - return right is null; - } - return left.Equals(right); - } - - public static bool operator !=(WorldNode? left, WorldNode? right) - { - return !(left == right); - } -} - -public partial class WorldNode : IInspectable -{ - public UIElement HeaderContent() - { - throw new NotImplementedException(); - } - - public UIElement InspectorContent() - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/Ghost.Editor/Serializer/WorldNodeSerializer.cs b/Ghost.Editor/Serializer/WorldNodeSerializer.cs deleted file mode 100644 index 011e759..0000000 --- a/Ghost.Editor/Serializer/WorldNodeSerializer.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Ghost.Editor.SceneGraph; -using Ghost.Engine.Utilities; -using Ghost.Entities; -using Ghost.Entities.Components; -using Ghost.Entities.Utilities; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Ghost.Editor.Serializer; - -internal class WorldNodeSerializer : JsonConverter -{ - private static class Property - { - public const string NAME = "Name"; - public const string ENTITIES = "Entities"; - public const string ID = "ID"; - public const string ENTITY_ID = "EntityID"; - public const string COMPONENTS = "Components"; - public const string DATA = "Data"; - public const string SYSTEMS = "Systems"; - } - - public override bool CanConvert(Type typeToConvert) - { - return typeToConvert == typeof(WorldNode) || typeToConvert.IsSubclassOf(typeof(WorldNode)); - } - - public override WorldNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var element = JsonDocument.ParseValue(ref reader).RootElement; - var name = element.GetProperty(Property.NAME).GetString() ?? "New World"; - - var world = World.Create(); - var result = new WorldNode(world, name); - - foreach (var entityElement in element.GetProperty(Property.ENTITIES).EnumerateArray()) - { - var entityName = entityElement.GetProperty(Property.NAME).GetString() ?? "New Entity"; - var entityID = entityElement.GetProperty(Property.ID).GetInt32(); - var entity = new Entity(entityID, 0); - var node = new EntityNode(entity, entityName); - - world.EntityManager.AddEntityInternal(entity); - result.EntityNodeLookup[entity] = node; - } - - foreach (var componentElement in element.GetProperty(Property.COMPONENTS).EnumerateObject()) - { - var typeName = componentElement.Name; - var type = Type.GetType(typeName) ?? throw new Exception($"Type {typeName} not found."); - - foreach (var dataElement in componentElement.Value.EnumerateArray()) - { - var entityID = dataElement.GetProperty(Property.ENTITY_ID).GetInt32(); - var entity = new Entity(entityID, 0); - - var dataProperty = dataElement.GetProperty(Property.DATA); - var component = JsonSerializer.Deserialize(dataProperty.GetRawText(), type, options); - if (component is IComponentData data) - { - world.EntityManager.AddComponent(entity, data, type); - } - } - } - - foreach (var systemElement in element.GetProperty(Property.SYSTEMS).EnumerateArray()) - { - var typeString = systemElement.GetString(); - if (string.IsNullOrEmpty(typeString)) - { - continue; - } - - var systemType = Type.GetType(typeString); - if (systemType == null) - { - continue; - } - - world.SystemStorage.AddSystem(systemType); - } - - return result; - } - - public override void Write(Utf8JsonWriter writer, WorldNode value, JsonSerializerOptions options) - { - writer.WriteObject(() => - { - writer.WriteString(Property.NAME, value.Name); - writer.WriteArray(Property.ENTITIES, value.World.EntityManager.Entities, entity => - { - if (!entity.IsValid) - { - return; - } - - writer.WriteObject(() => - { - writer.WriteString(Property.NAME, value.EntityNodeLookup[entity].Name); - writer.WriteNumber(Property.ID, entity.ID); - }); - }); - - writer.WriteObject(Property.COMPONENTS, () => - { - foreach (var kvp in value.World.ComponentStorage.ComponentPools) - { - var type = TypeHandle.ToType(kvp.Key) ?? throw new Exception($"Type {kvp.Key} not found."); - var typeName = type.AssemblyQualifiedName ?? type.Name; - - writer.WriteArray(typeName, kvp.Value.Enumerate(), data => - { - writer.WriteObject(() => - { - writer.WriteNumber(Property.ENTITY_ID, data.entity.ID); - writer.WritePropertyName(Property.DATA); - JsonSerializer.Serialize(writer, data.component, type, options); - }); - }); - } - }); - - writer.WriteArray(Property.SYSTEMS, value.World.SystemStorage.Systems, systemType => - { - writer.WriteStringValue(systemType.AssemblyQualifiedName ?? systemType.Name); - }); - }); - } -} \ No newline at end of file diff --git a/Ghost.App/Themes/Override.xaml b/Ghost.Editor/Themes/Override.xaml similarity index 100% rename from Ghost.App/Themes/Override.xaml rename to Ghost.Editor/Themes/Override.xaml diff --git a/Ghost.App/Utilities/Converters/AssetPathToGlyphConverter.cs b/Ghost.Editor/Utilities/Converters/AssetPathToGlyphConverter.cs similarity index 100% rename from Ghost.App/Utilities/Converters/AssetPathToGlyphConverter.cs rename to Ghost.Editor/Utilities/Converters/AssetPathToGlyphConverter.cs diff --git a/Ghost.App/Utilities/Converters/GetDirectoryNameConverter .cs b/Ghost.Editor/Utilities/Converters/GetDirectoryNameConverter .cs similarity index 100% rename from Ghost.App/Utilities/Converters/GetDirectoryNameConverter .cs rename to Ghost.Editor/Utilities/Converters/GetDirectoryNameConverter .cs diff --git a/Ghost.App/Utilities/Converters/Vector3ToQuaternionConverter.cs b/Ghost.Editor/Utilities/Converters/Vector3ToQuaternionConverter.cs similarity index 100% rename from Ghost.App/Utilities/Converters/Vector3ToQuaternionConverter.cs rename to Ghost.Editor/Utilities/Converters/Vector3ToQuaternionConverter.cs diff --git a/Ghost.App/Utilities/HostHelpers.Page.cs b/Ghost.Editor/Utilities/HostHelpers.Page.cs similarity index 97% rename from Ghost.App/Utilities/HostHelpers.Page.cs rename to Ghost.Editor/Utilities/HostHelpers.Page.cs index 7941a5b..755ec5d 100644 --- a/Ghost.App/Utilities/HostHelpers.Page.cs +++ b/Ghost.Editor/Utilities/HostHelpers.Page.cs @@ -33,6 +33,8 @@ internal static partial class HostHelper services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); diff --git a/Ghost.App/Utilities/ReflectionBinding.cs b/Ghost.Editor/Utilities/ReflectionBinding.cs similarity index 100% rename from Ghost.App/Utilities/ReflectionBinding.cs rename to Ghost.Editor/Utilities/ReflectionBinding.cs diff --git a/Ghost.App/Utilities/SystemUtility.cs b/Ghost.Editor/Utilities/SystemUtility.cs similarity index 82% rename from Ghost.App/Utilities/SystemUtility.cs rename to Ghost.Editor/Utilities/SystemUtility.cs index 388a4fb..3d3614d 100644 --- a/Ghost.App/Utilities/SystemUtility.cs +++ b/Ghost.Editor/Utilities/SystemUtility.cs @@ -1,8 +1,4 @@ -using Ghost.Editor; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Windows.Storage; +using Windows.Storage; using Windows.Storage.Pickers; using WinRT.Interop; @@ -13,7 +9,7 @@ public static class SystemUtilities public static async Task OpenFolderPickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "") { var openPicker = new FolderPicker(); - var hWnd = WindowNative.GetWindowHandle(EditorApplication.Window); + var hWnd = WindowNative.GetWindowHandle(App.Window); InitializeWithWindow.Initialize(openPicker, hWnd); openPicker.SuggestedStartLocation = startLocation; @@ -27,7 +23,7 @@ public static class SystemUtilities public static async Task OpenFilePickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "", params IEnumerable filter) { var openPicker = new FileOpenPicker(); - var hWnd = WindowNative.GetWindowHandle(EditorApplication.Window); + var hWnd = WindowNative.GetWindowHandle(App.Window); InitializeWithWindow.Initialize(openPicker, hWnd); openPicker.SuggestedStartLocation = startLocation; diff --git a/Ghost.App/View/Pages/EngineEditor/ConsolePage.xaml b/Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml similarity index 100% rename from Ghost.App/View/Pages/EngineEditor/ConsolePage.xaml rename to Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml diff --git a/Ghost.App/View/Pages/EngineEditor/ConsolePage.xaml.cs b/Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml.cs similarity index 81% rename from Ghost.App/View/Pages/EngineEditor/ConsolePage.xaml.cs rename to Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml.cs index c606188..192e038 100644 --- a/Ghost.App/View/Pages/EngineEditor/ConsolePage.xaml.cs +++ b/Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml.cs @@ -12,7 +12,7 @@ internal sealed partial class ConsolePage : Page public ConsolePage() { - ViewModel = EditorApplication.GetService(); + ViewModel = App.GetService(); InitializeComponent(); } diff --git a/Ghost.App/View/Pages/EngineEditor/HierarchyPage.xaml b/Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml similarity index 100% rename from Ghost.App/View/Pages/EngineEditor/HierarchyPage.xaml rename to Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml diff --git a/Ghost.App/View/Pages/EngineEditor/HierarchyPage.xaml.cs b/Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml.cs similarity index 85% rename from Ghost.App/View/Pages/EngineEditor/HierarchyPage.xaml.cs rename to Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml.cs index e088031..6af8a04 100644 --- a/Ghost.App/View/Pages/EngineEditor/HierarchyPage.xaml.cs +++ b/Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml.cs @@ -1,7 +1,6 @@ using Ghost.Editor.Controls.Internal; using Ghost.Editor.Core.Inspector; using Ghost.Editor.Core.SceneGraph; -using Ghost.Editor.Services.Contracts; using Ghost.Editor.ViewModels.Pages.EngineEditor; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -19,16 +18,10 @@ internal sealed partial class HierarchyPage : NavigationTabPage public HierarchyPage() { - _inspectorService = EditorApplication.GetService(); - ViewModel = EditorApplication.GetService(); + _inspectorService = App.GetService(); + ViewModel = App.GetService(); InitializeComponent(); - - Header = "Hierarchy"; - IconSource = new FontIconSource - { - Glyph = "\uE8A4" - }; } public override void OnNavigatedTo(object? parameter) diff --git a/Ghost.App/View/Pages/EngineEditor/InspectorPage.xaml b/Ghost.Editor/View/Pages/EngineEditor/InspectorPage.xaml similarity index 100% rename from Ghost.App/View/Pages/EngineEditor/InspectorPage.xaml rename to Ghost.Editor/View/Pages/EngineEditor/InspectorPage.xaml diff --git a/Ghost.App/View/Pages/EngineEditor/InspectorPage.xaml.cs b/Ghost.Editor/View/Pages/EngineEditor/InspectorPage.xaml.cs similarity index 70% rename from Ghost.App/View/Pages/EngineEditor/InspectorPage.xaml.cs rename to Ghost.Editor/View/Pages/EngineEditor/InspectorPage.xaml.cs index 3a1e0c8..2e01e73 100644 --- a/Ghost.App/View/Pages/EngineEditor/InspectorPage.xaml.cs +++ b/Ghost.Editor/View/Pages/EngineEditor/InspectorPage.xaml.cs @@ -1,6 +1,5 @@ using Ghost.Editor.Controls.Internal; using Ghost.Editor.ViewModels.Pages.EngineEditor; -using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.View.Pages.EngineEditor; @@ -13,15 +12,9 @@ internal sealed partial class InspectorPage : NavigationTabPage public InspectorPage() { - ViewModel = EditorApplication.GetService(); + ViewModel = App.GetService(); InitializeComponent(); - - Header = "Inspector"; - IconSource = new FontIconSource - { - Glyph = "\uEC7A" - }; } public override void OnNavigatedTo(object? parameter) diff --git a/Ghost.App/View/Pages/EngineEditor/ProjectPage.xaml b/Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml similarity index 100% rename from Ghost.App/View/Pages/EngineEditor/ProjectPage.xaml rename to Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml diff --git a/Ghost.App/View/Pages/EngineEditor/ProjectPage.xaml.cs b/Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml.cs similarity index 87% rename from Ghost.App/View/Pages/EngineEditor/ProjectPage.xaml.cs rename to Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml.cs index 1292b88..d5efdf0 100644 --- a/Ghost.App/View/Pages/EngineEditor/ProjectPage.xaml.cs +++ b/Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml.cs @@ -13,7 +13,7 @@ internal sealed partial class ProjectPage : Page public ProjectPage() { - ViewModel = EditorApplication.GetService(); + ViewModel = App.GetService(); InitializeComponent(); } diff --git a/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml b/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml new file mode 100644 index 0000000..c950e0b --- /dev/null +++ b/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml.cs b/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml.cs new file mode 100644 index 0000000..ab2dec2 --- /dev/null +++ b/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml.cs @@ -0,0 +1,54 @@ +using Ghost.Editor.Controls.Internal; +using Ghost.Graphics; +using Ghost.Graphics.Contracts; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using SharpGen.Runtime; +using WinRT; + +namespace Ghost.Editor.View.Pages.EngineEditor; + +internal sealed partial class ScenePage : NavigationTabPage +{ + private IRenderView? _renderer; + + public ScenePage() + { + InitializeComponent(); + + SwapChainPanel.Loaded += SwapChainPanel_Loaded; + SwapChainPanel.Unloaded += SwapChainPanel_Unloaded; + SwapChainPanel.SizeChanged += SwapChainPanel_SizeChanged; + } + + private void OnRendering(object? sender, object e) + { + _renderer?.Render(); + } + + private void SwapChainPanel_Loaded(object sender, RoutedEventArgs e) + { + var guid = typeof(Vortice.WinUI.ISwapChainPanelNative).GUID; + Result result = ((IWinRTObject)SwapChainPanel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle); + result.CheckError(); + + var swapChainPanelNative = new Vortice.WinUI.ISwapChainPanelNative(swapChainPanelNativeHandle); + _renderer = GraphicsPipeline.GraphicsDevice.CreateRenderView(new(swapChainPanelNative, (uint)SwapChainPanel.ActualWidth, (uint)SwapChainPanel.ActualHeight)); + + CompositionTarget.Rendering += OnRendering; + } + + private void SwapChainPanel_Unloaded(object sender, RoutedEventArgs e) + { + CompositionTarget.Rendering -= OnRendering; + _renderer?.Dispose(); + } + + private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (e.NewSize.Width > 8.0 && e.NewSize.Height > 8.0) + { + _renderer?.Resize((uint)e.NewSize.Width, (uint)e.NewSize.Height); + } + } +} \ No newline at end of file diff --git a/Ghost.App/View/Pages/Landing/CreateProjectPage.xaml b/Ghost.Editor/View/Pages/Landing/CreateProjectPage.xaml similarity index 100% rename from Ghost.App/View/Pages/Landing/CreateProjectPage.xaml rename to Ghost.Editor/View/Pages/Landing/CreateProjectPage.xaml diff --git a/Ghost.App/View/Pages/Landing/CreateProjectPage.xaml.cs b/Ghost.Editor/View/Pages/Landing/CreateProjectPage.xaml.cs similarity index 87% rename from Ghost.App/View/Pages/Landing/CreateProjectPage.xaml.cs rename to Ghost.Editor/View/Pages/Landing/CreateProjectPage.xaml.cs index c645e4c..1acca94 100644 --- a/Ghost.App/View/Pages/Landing/CreateProjectPage.xaml.cs +++ b/Ghost.Editor/View/Pages/Landing/CreateProjectPage.xaml.cs @@ -13,7 +13,7 @@ internal sealed partial class CreateProjectPage : Page public CreateProjectPage() { - ViewModel = EditorApplication.GetService(); + ViewModel = App.GetService(); InitializeComponent(); } diff --git a/Ghost.App/View/Pages/Landing/OpenProjectPage.xaml b/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml similarity index 100% rename from Ghost.App/View/Pages/Landing/OpenProjectPage.xaml rename to Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml diff --git a/Ghost.App/View/Pages/Landing/OpenProjectPage.xaml.cs b/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml.cs similarity index 96% rename from Ghost.App/View/Pages/Landing/OpenProjectPage.xaml.cs rename to Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml.cs index a6507b1..bb70791 100644 --- a/Ghost.App/View/Pages/Landing/OpenProjectPage.xaml.cs +++ b/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml.cs @@ -16,7 +16,7 @@ internal sealed partial class OpenProjectPage : Page public OpenProjectPage() { - ViewModel = EditorApplication.GetService(); + ViewModel = App.GetService(); InitializeComponent(); } diff --git a/Ghost.App/View/Windows/EngineEditorWindow.xaml b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml similarity index 92% rename from Ghost.App/View/Windows/EngineEditorWindow.xaml rename to Ghost.Editor/View/Windows/EngineEditorWindow.xaml index a5ec2d1..70defcf 100644 --- a/Ghost.App/View/Windows/EngineEditorWindow.xaml +++ b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml @@ -93,21 +93,21 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + + + + - - + + - - - + + @@ -117,7 +117,11 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + + + + diff --git a/Ghost.App/View/Windows/EngineEditorWindow.xaml.cs b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs similarity index 79% rename from Ghost.App/View/Windows/EngineEditorWindow.xaml.cs rename to Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index 88131db..cb02dbe 100644 --- a/Ghost.App/View/Windows/EngineEditorWindow.xaml.cs +++ b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -1,6 +1,6 @@ using Ghost.Data.Resources; -using Ghost.Editor.Services; -using Ghost.Editor.Services.Contracts; +using Ghost.Editor.Core.Notifications; +using Ghost.Editor.Core.Progress; using Ghost.Editor.ViewModels.Windows; using Ghost.Engine.Resources; using Microsoft.Extensions.DependencyInjection; @@ -27,10 +27,10 @@ internal sealed partial class EngineEditorWindow : WindowEx public EngineEditorWindow() { - ViewModel = EditorApplication.GetService(); + ViewModel = App.GetService(); - _notificationService = (NotificationService)EditorApplication.GetService(); - _progressService = (ProgressService)EditorApplication.GetService(); + _notificationService = (NotificationService)App.GetService(); + _progressService = (ProgressService)App.GetService(); AppWindow.SetIcon(AssetsPath.s_appIconPath); Title = EngineData.ENGINE_NAME; @@ -46,7 +46,7 @@ internal sealed partial class EngineEditorWindow : WindowEx Bindings.Update(); _editorScope?.Dispose(); - _editorScope = EditorApplication.CreateScope(); + _editorScope = App.CreateScope(); _notificationService.SetReference(InfoBar, NotificationQueue); _progressService.SetReference(ProgressBarContainer); diff --git a/Ghost.App/View/Windows/LandingWindow.xaml b/Ghost.Editor/View/Windows/LandingWindow.xaml similarity index 100% rename from Ghost.App/View/Windows/LandingWindow.xaml rename to Ghost.Editor/View/Windows/LandingWindow.xaml diff --git a/Ghost.App/View/Windows/LandingWindow.xaml.cs b/Ghost.Editor/View/Windows/LandingWindow.xaml.cs similarity index 86% rename from Ghost.App/View/Windows/LandingWindow.xaml.cs rename to Ghost.Editor/View/Windows/LandingWindow.xaml.cs index a26c2e9..996ae41 100644 --- a/Ghost.App/View/Windows/LandingWindow.xaml.cs +++ b/Ghost.Editor/View/Windows/LandingWindow.xaml.cs @@ -1,7 +1,6 @@ -using Ghost.Editor.View.Pages.Landing; -using Ghost.Data.Resources; -using Ghost.Editor.Services; -using Ghost.Editor.Services.Contracts; +using Ghost.Data.Resources; +using Ghost.Editor.Core.Notifications; +using Ghost.Editor.View.Pages.Landing; using Ghost.Engine.Resources; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml.Controls; @@ -20,7 +19,7 @@ internal sealed partial class LandingWindow : WindowEx public LandingWindow() { - _notificationService = (NotificationService)EditorApplication.GetService(); + _notificationService = (NotificationService)App.GetService(); AppWindow.SetIcon(AssetsPath.s_appIconPath); Title = EngineData.ENGINE_NAME; @@ -36,7 +35,7 @@ internal sealed partial class LandingWindow : WindowEx private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args) { _landingScope?.Dispose(); - _landingScope = EditorApplication.CreateScope(); + _landingScope = App.CreateScope(); _notificationService.SetReference(InfoBar, NotificationQueue); } diff --git a/Ghost.App/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs b/Ghost.Editor/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs similarity index 100% rename from Ghost.App/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs rename to Ghost.Editor/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs diff --git a/Ghost.App/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs b/Ghost.Editor/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs similarity index 96% rename from Ghost.App/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs rename to Ghost.Editor/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs index 35aec54..36a81d6 100644 --- a/Ghost.App/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs +++ b/Ghost.Editor/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs @@ -1,5 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; -using Ghost.Editor.Contracts; +using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.SceneGraph; using System.Collections.ObjectModel; diff --git a/Ghost.App/ViewModels/Pages/EngineEditor/InspectorViewModel.cs b/Ghost.Editor/ViewModels/Pages/EngineEditor/InspectorViewModel.cs similarity index 92% rename from Ghost.App/ViewModels/Pages/EngineEditor/InspectorViewModel.cs rename to Ghost.Editor/ViewModels/Pages/EngineEditor/InspectorViewModel.cs index 892d783..034cedb 100644 --- a/Ghost.App/ViewModels/Pages/EngineEditor/InspectorViewModel.cs +++ b/Ghost.Editor/ViewModels/Pages/EngineEditor/InspectorViewModel.cs @@ -1,7 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; -using Ghost.Editor.Contracts; +using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Inspector; -using Ghost.Editor.Services.Contracts; namespace Ghost.Editor.ViewModels.Pages.EngineEditor; diff --git a/Ghost.App/ViewModels/Pages/EngineEditor/ProjectViewModel.cs b/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs similarity index 98% rename from Ghost.App/ViewModels/Pages/EngineEditor/ProjectViewModel.cs rename to Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs index 288f0f3..c09a3e3 100644 --- a/Ghost.App/ViewModels/Pages/EngineEditor/ProjectViewModel.cs +++ b/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs @@ -91,7 +91,7 @@ internal partial class ProjectViewModel : ObservableObject private void NavigateToDirectory(string? path) { - EditorApplication.Window?.DispatcherQueue.TryEnqueue(async () => + App.Window?.DispatcherQueue.TryEnqueue(async () => { DirectoryAssets.Clear(); diff --git a/Ghost.App/ViewModels/Pages/Landing/CreateProjectViewModel.cs b/Ghost.Editor/ViewModels/Pages/Landing/CreateProjectViewModel.cs similarity index 93% rename from Ghost.App/ViewModels/Pages/Landing/CreateProjectViewModel.cs rename to Ghost.Editor/ViewModels/Pages/Landing/CreateProjectViewModel.cs index fb6b54d..820eccc 100644 --- a/Ghost.App/ViewModels/Pages/Landing/CreateProjectViewModel.cs +++ b/Ghost.Editor/ViewModels/Pages/Landing/CreateProjectViewModel.cs @@ -2,16 +2,12 @@ using CommunityToolkit.Mvvm.Input; using Ghost.Data.Models; using Ghost.Data.Services; -using Ghost.Editor.Contracts; using Ghost.Editor.Core.AppState; -using Ghost.Editor.Models; -using Ghost.Editor.Services; +using Ghost.Editor.Core.Contracts; +using Ghost.Editor.Core.Notifications; using Ghost.Editor.Utilities; using Ghost.Engine.Resources; using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; namespace Ghost.Editor.ViewModels.Pages.Landing; @@ -85,7 +81,7 @@ internal partial class CreateProjectViewModel(NotificationService notificationSe try { - await stateService.TransitionToAsync(StateKey.EngineEditor, result.data); + await stateService.TransitionToAsync(StateKey.EngineEditor, result.value); } catch (System.Exception e) { diff --git a/Ghost.App/ViewModels/Pages/Landing/OpenProjectViewModel.cs b/Ghost.Editor/ViewModels/Pages/Landing/OpenProjectViewModel.cs similarity index 95% rename from Ghost.App/ViewModels/Pages/Landing/OpenProjectViewModel.cs rename to Ghost.Editor/ViewModels/Pages/Landing/OpenProjectViewModel.cs index e46c5d7..9d26cb3 100644 --- a/Ghost.App/ViewModels/Pages/Landing/OpenProjectViewModel.cs +++ b/Ghost.Editor/ViewModels/Pages/Landing/OpenProjectViewModel.cs @@ -1,10 +1,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using Ghost.Data.Models; using Ghost.Data.Services; -using Ghost.Editor.Contracts; using Ghost.Editor.Core.AppState; -using Ghost.Editor.Models; -using Ghost.Editor.Services.Contracts; +using Ghost.Editor.Core.Contracts; +using Ghost.Editor.Core.Notifications; using Microsoft.UI.Xaml; using System.Collections.ObjectModel; using Windows.ApplicationModel.DataTransfer; @@ -69,7 +68,7 @@ internal partial class OpenProjectViewModel(ProjectService projectService, INoti var result = await projectService.AddProjectFromDirectoryAsync(rootFolder.Path); if (result.success) { - projects.Add(result.data); + projects.Add(result.value); goto CloseDropPanel; } else diff --git a/Ghost.App/ViewModels/Windows/EngineEditorViewModel.cs b/Ghost.Editor/ViewModels/Windows/EngineEditorViewModel.cs similarity index 100% rename from Ghost.App/ViewModels/Windows/EngineEditorViewModel.cs rename to Ghost.Editor/ViewModels/Windows/EngineEditorViewModel.cs diff --git a/Ghost.App/app.manifest b/Ghost.Editor/app.manifest similarity index 100% rename from Ghost.App/app.manifest rename to Ghost.Editor/app.manifest diff --git a/Ghost.Engine/EngineCore.cs b/Ghost.Engine/EngineCore.cs index f8f74fe..83dbc1c 100644 --- a/Ghost.Engine/EngineCore.cs +++ b/Ghost.Engine/EngineCore.cs @@ -1,13 +1,16 @@ using Ghost.Engine.Models; using Ghost.Engine.Services; +using Ghost.Graphics; +using Ghost.Graphics.Data; namespace Ghost.Engine; -internal class EngineCore : IDisposable, IAsyncDisposable +internal class EngineCore { public async Task StartAsync(LaunchArgument args) { ActivationHandler.Handle(args); + GraphicsPipeline.Initialize(GraphicsAPI.DX12); Logger.LogInfo("Engine started successfully."); @@ -16,16 +19,7 @@ internal class EngineCore : IDisposable, IAsyncDisposable public async Task ShutDownAsync() { + GraphicsPipeline.Shutdown(); await Task.CompletedTask; } - - public void Dispose() - { - ShutDownAsync().GetAwaiter().GetResult(); - } - - public async ValueTask DisposeAsync() - { - await ShutDownAsync(); - } } \ No newline at end of file diff --git a/Ghost.Engine/Ghost.Engine.csproj b/Ghost.Engine/Ghost.Engine.csproj index f3c3142..96716b3 100644 --- a/Ghost.Engine/Ghost.Engine.csproj +++ b/Ghost.Engine/Ghost.Engine.csproj @@ -17,7 +17,14 @@ + + + + ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll + + + diff --git a/Ghost.Engine/Models/LogMessage.cs b/Ghost.Engine/Models/LogMessage.cs index 948b153..4a21bda 100644 --- a/Ghost.Engine/Models/LogMessage.cs +++ b/Ghost.Engine/Models/LogMessage.cs @@ -14,7 +14,7 @@ internal class LogMessage get; set; } - public string Message + public string? Message { get; set; } @@ -29,7 +29,7 @@ internal class LogMessage get; set; } - public LogMessage(LogLevel level, string message, string? stackTrace = null) + public LogMessage(LogLevel level, string? message, string? stackTrace = null) { Level = level; Message = message; diff --git a/Ghost.Engine/Services/Logger.cs b/Ghost.Engine/Services/Logger.cs index d3ef227..6f27b40 100644 --- a/Ghost.Engine/Services/Logger.cs +++ b/Ghost.Engine/Services/Logger.cs @@ -25,7 +25,7 @@ public static class Logger get; set; } - private static void LogInternal(LogLevel level, string message, int skipFrame) + private static void LogInternal(LogLevel level, string? message, int skipFrame) { if (_logs.Count >= _MAX_LOGS) { @@ -59,22 +59,22 @@ public static class Logger OnLogsUpdate?.Invoke(LogChangeType.LogAdded); } - public static void Log(LogLevel level, string message) + public static void Log(LogLevel level, string? message) { LogInternal(level, message, 2); } - public static void LogInfo(string message) + public static void LogInfo(string? message) { LogInternal(LogLevel.Info, message, 3); } - public static void LogWarning(string message) + public static void LogWarning(string? message) { LogInternal(LogLevel.Warning, message, 3); } - public static void LogError(string message) + public static void LogError(string? message) { LogInternal(LogLevel.Error, message, 3); } @@ -84,6 +84,14 @@ public static class Logger LogExceptionInternal(ex); } + public static void Assert(bool condition, string? message = null) + { + if (!condition) + { + LogInternal(LogLevel.Error, message ?? "Assertion failed", 3); + } + } + internal static void Clear() { _logs.Clear(); diff --git a/Ghost.Entities/AssemblyInfo.cs b/Ghost.Entities/AssemblyInfo.cs index 6e543db..0416c34 100644 --- a/Ghost.Entities/AssemblyInfo.cs +++ b/Ghost.Entities/AssemblyInfo.cs @@ -5,5 +5,5 @@ global using WorldID = System.UInt16; using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Ghost.Engine")] -[assembly: InternalsVisibleTo("Ghost.Editor")] +[assembly: InternalsVisibleTo("Ghost.Editor.Core")] [assembly: InternalsVisibleTo("Ghost.UnitTest")] \ No newline at end of file diff --git a/Ghost.Entities/Components/ComponentStorage.cs b/Ghost.Entities/Components/ComponentStorage.cs index 460d711..9742979 100644 --- a/Ghost.Entities/Components/ComponentStorage.cs +++ b/Ghost.Entities/Components/ComponentStorage.cs @@ -1,4 +1,4 @@ -using Ghost.Entities.Utilities; +using Ghost.Core; using Misaki.HighPerformance.Unsafe.Collections; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -463,8 +463,8 @@ internal class ScriptComponentPool : IComponentPool [SkipLocalsInit] internal readonly struct ComponentStorage : IDisposable { - private readonly Dictionary _componentPools = new(); - private readonly Dictionary _componentEntityMasks = new(); + private readonly Dictionary _componentPools = new(); + private readonly Dictionary _componentEntityMasks = new(); private readonly ScriptComponentPool _scriptComponentPool = new(); private readonly World _world; @@ -474,12 +474,12 @@ internal readonly struct ComponentStorage : IDisposable _world = world; } - internal Dictionary ComponentPools => _componentPools; - internal Dictionary ComponentEntityMasks => _componentEntityMasks; + internal Dictionary ComponentPools => _componentPools; + internal Dictionary ComponentEntityMasks => _componentEntityMasks; internal ScriptComponentPool ScriptComponentPool => _scriptComponentPool; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetPool(nint typeHandle, [MaybeNullWhen(false)] out IComponentPool pool) + public bool TryGetPool(TypeHandle typeHandle, [MaybeNullWhen(false)] out IComponentPool pool) { return _componentPools.TryGetValue(typeHandle, out pool); } @@ -514,7 +514,7 @@ internal readonly struct ComponentStorage : IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetMask(nint typeHandle, [MaybeNullWhen(false)] out BitSet bitSet) + public bool TryGetMask(TypeHandle typeHandle, [MaybeNullWhen(false)] out BitSet bitSet) { return _componentEntityMasks.TryGetValue(typeHandle, out bitSet); } @@ -522,7 +522,7 @@ internal readonly struct ComponentStorage : IDisposable [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetMask(Type type, [MaybeNullWhen(false)] out BitSet bitSet) { - return TryGetMask(type.TypeHandle.Value, out bitSet); + return TryGetMask(TypeHandle.Get(type), out bitSet); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -532,7 +532,7 @@ internal readonly struct ComponentStorage : IDisposable return TryGetMask(TypeHandle.Get(), out bitSet); } - public BitSet GetOrCreateMask(nint typeHandle) + public BitSet GetOrCreateMask(TypeHandle typeHandle) { if (!_componentEntityMasks.TryGetValue(typeHandle, out var mask)) { diff --git a/Ghost.Entities/EntityManager.cs b/Ghost.Entities/EntityManager.cs index cf13561..00f796e 100644 --- a/Ghost.Entities/EntityManager.cs +++ b/Ghost.Entities/EntityManager.cs @@ -1,10 +1,11 @@ -using Ghost.Entities.Components; +using Ghost.Core; +using Ghost.Entities.Components; using Ghost.Entities.Query; -using Ghost.Entities.Utilities; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Ghost.Entities; + public readonly struct EntityManager : IDisposable { private readonly List _entities; @@ -181,7 +182,7 @@ public readonly struct EntityManager : IDisposable /// The handle of the component type. /// True if the entity has the component; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool HasComponent(Entity entity, nint typeHandle) + public readonly bool HasComponent(Entity entity, TypeHandle typeHandle) { return _world.ComponentStorage.TryGetMask(typeHandle, out var bitSet) && bitSet.IsSet(entity.ID); } @@ -238,7 +239,7 @@ public readonly struct EntityManager : IDisposable /// An enumerable collection of representing the memory addresses of the components associated /// with the specified entity. The collection will be empty if the entity has no associated components. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly IEnumerable<(IntPtr, IntPtr)> GetComponentsUnsafe(Entity entity) + public readonly IEnumerable<(TypeHandle, IntPtr)> GetComponentsUnsafe(Entity entity) { foreach (var (typeHandle, pool) in _world.ComponentStorage.ComponentPools) { diff --git a/Ghost.Entities/Ghost.Entities.csproj b/Ghost.Entities/Ghost.Entities.csproj index 32e2d87..6999cd3 100644 --- a/Ghost.Entities/Ghost.Entities.csproj +++ b/Ghost.Entities/Ghost.Entities.csproj @@ -90,4 +90,8 @@ + + + + diff --git a/Ghost.Entities/Query/QueryFilter.cs b/Ghost.Entities/Query/QueryFilter.cs index ca4b3c8..c2aaae7 100644 --- a/Ghost.Entities/Query/QueryFilter.cs +++ b/Ghost.Entities/Query/QueryFilter.cs @@ -1,4 +1,5 @@ -using Misaki.HighPerformance.Unsafe.Collections; +using Ghost.Core; +using Misaki.HighPerformance.Unsafe.Collections; namespace Ghost.Entities.Query; @@ -11,30 +12,29 @@ internal enum FilterMode Disabled = 1 << 3, } -internal readonly struct FilterEntry(nint id, FilterMode mode) +internal readonly struct FilterEntry(TypeHandle id, FilterMode mode) { - public readonly nint typeHandle = id; + public readonly TypeHandle typeHandle = id; public readonly FilterMode mode = mode; } internal struct QueryFilter() { - internal List _all = new(6); - internal List _any = new(6); - internal List _absent = new(6); - internal List _disabled = new(6); + internal List _all = new(6); + internal List _any = new(6); + internal List _absent = new(6); + internal List _disabled = new(6); - public readonly void ComputeFilterBitMask(World world, BitSet result) + public readonly BitSet ComputeFilterBitMask(World world) { - BitSet allMask = new(); - BitSet anyMask = new(); - BitSet absentMask = new(); + BitSet? allMask = null; + BitSet? anyMask = null; + BitSet? absentMask = null; var hasAll = false; var hasAny = false; var hasAbsent = false; - // Compute All mask (intersection) foreach (var typeHandle in _all) { var mask = world.ComponentStorage.GetOrCreateMask(typeHandle); @@ -49,7 +49,6 @@ internal struct QueryFilter() allMask &= mask; } - // Compute Any mask (union) foreach (var typeHandle in _any) { var mask = world.ComponentStorage.GetOrCreateMask(typeHandle); @@ -63,7 +62,6 @@ internal struct QueryFilter() anyMask |= mask; } - // Compute Absent mask (union for exclusion) foreach (var typeHandle in _absent) { var mask = world.ComponentStorage.GetOrCreateMask(typeHandle); @@ -77,21 +75,24 @@ internal struct QueryFilter() absentMask |= mask; } + var result = new BitSet(world.EntityManager.EntityCount); result.SetAll(); if (hasAll) { - result &= allMask; + result &= allMask!; } if (hasAny) { - result &= anyMask; + result &= anyMask!; } if (hasAbsent) { - result &= ~absentMask; + result &= ~absentMask!; } + + return result; } } \ No newline at end of file diff --git a/Ghost.Entities/Template/QueryEnumerable.cs b/Ghost.Entities/Template/QueryEnumerable.cs index 53dabd6..56b947b 100644 --- a/Ghost.Entities/Template/QueryEnumerable.cs +++ b/Ghost.Entities/Template/QueryEnumerable.cs @@ -1,8 +1,8 @@  +using Ghost.Core; using Ghost.Entities.Components; using Ghost.Entities.Query; -using Ghost.Entities.Utilities; using Misaki.HighPerformance.Unsafe.Collections; namespace Ghost.Entities; @@ -59,8 +59,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } @@ -232,8 +231,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } @@ -410,8 +408,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } @@ -593,8 +590,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } @@ -781,8 +777,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } @@ -974,8 +969,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } @@ -1172,8 +1166,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } @@ -1375,8 +1368,7 @@ public struct QueryEnumerable _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } diff --git a/Ghost.Entities/Template/QueryEnumerable.tt b/Ghost.Entities/Template/QueryEnumerable.tt index 31dee4a..6c45780 100644 --- a/Ghost.Entities/Template/QueryEnumerable.tt +++ b/Ghost.Entities/Template/QueryEnumerable.tt @@ -5,9 +5,9 @@ <#@ import namespace="System.Linq" #> <#@ include file="Helpers.ttinclude" #> +using Ghost.Core; using Ghost.Entities.Components; using Ghost.Entities.Query; -using Ghost.Entities.Utilities; using Misaki.HighPerformance.Unsafe.Collections; namespace Ghost.Entities; @@ -85,8 +85,7 @@ public struct QueryEnumerable<<#= generics #>> _count = count; _index = -1; - _filterMask = new BitSet(_world.EntityManager.EntityCount); - filters.ComputeFilterBitMask(_world, _filterMask); + _filterMask = filters.ComputeFilterBitMask(_world); Current = default; } diff --git a/Ghost.Entities/World.cs b/Ghost.Entities/World.cs index e136f00..3c3c264 100644 --- a/Ghost.Entities/World.cs +++ b/Ghost.Entities/World.cs @@ -57,6 +57,8 @@ public partial class World : IDisposable, IEquatable public EntityManager EntityManager => _entityManager; public SystemStorage SystemStorage => _systemStorage; + public event Action? ComponentChanged; + private World(WorldID id, int entityCapacity) { _id = id; @@ -84,6 +86,21 @@ public partial class World : IDisposable, IEquatable return Enumerable.Empty(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void NotifyComponentChanged(Entity entity, Type type) + { + //#if GHOST_EDITOR + ComponentChanged?.Invoke(this, entity, type); + //#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void NotifyComponentChanged(Entity entity) + where T : unmanaged, IComponentData + { + NotifyComponentChanged(entity, typeof(T)); + } + public void Dispose() { _entityManager.Dispose(); diff --git a/Ghost.Graphics/AssemblyInfo.cs b/Ghost.Graphics/AssemblyInfo.cs new file mode 100644 index 0000000..fca6460 --- /dev/null +++ b/Ghost.Graphics/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Ghost.Engine")] +[assembly: InternalsVisibleTo("Ghost.Editor")] +[assembly: InternalsVisibleTo("Ghost.Editor.Core")] +[assembly: InternalsVisibleTo("Ghost.UnitTest")] \ No newline at end of file diff --git a/Ghost.Graphics/Class1.cs b/Ghost.Graphics/Class1.cs deleted file mode 100644 index 6132543..0000000 --- a/Ghost.Graphics/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ghost.Graphics; - -public class Class1 -{ - -} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IDebugLayer.cs b/Ghost.Graphics/Contracts/IDebugLayer.cs new file mode 100644 index 0000000..d8ddafb --- /dev/null +++ b/Ghost.Graphics/Contracts/IDebugLayer.cs @@ -0,0 +1,5 @@ +namespace Ghost.Graphics.Contracts; + +internal interface IDebugLayer : IDisposable +{ +} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IGraphicsDevice.cs b/Ghost.Graphics/Contracts/IGraphicsDevice.cs new file mode 100644 index 0000000..6956fcf --- /dev/null +++ b/Ghost.Graphics/Contracts/IGraphicsDevice.cs @@ -0,0 +1,11 @@ +using Ghost.Graphics.Data; + +namespace Ghost.Graphics.Contracts; + +internal interface IGraphicsDevice : IDisposable +{ + public static abstract IGraphicsDevice Create(); + + public IRenderView CreateRenderView(in SwapChainSurface swapChainSurface); + public void OnRender(); +} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IRenderView.cs b/Ghost.Graphics/Contracts/IRenderView.cs new file mode 100644 index 0000000..44a0843 --- /dev/null +++ b/Ghost.Graphics/Contracts/IRenderView.cs @@ -0,0 +1,11 @@ +namespace Ghost.Graphics.Contracts; + +internal interface IRenderView : IDisposable +{ + public void Resize(uint width, uint height); + public void Render(); + + public void WaitNextFrame(); + public void WaitIdle(); + public void Flush(); +} \ No newline at end of file diff --git a/Ghost.Graphics/DX12/DX12DebugLayer.cs b/Ghost.Graphics/DX12/DX12DebugLayer.cs new file mode 100644 index 0000000..a1280a1 --- /dev/null +++ b/Ghost.Graphics/DX12/DX12DebugLayer.cs @@ -0,0 +1,36 @@ +using Ghost.Graphics.Contracts; +using Vortice.Direct3D12; +using Vortice.Direct3D12.Debug; +using Vortice.DXGI; +using Vortice.DXGI.Debug; + +namespace Ghost.Graphics.DX12; + +internal class DX12DebugLayer : IDebugLayer +{ +#if DEBUG + private readonly ID3D12Debug6 _d3d12Debug; + private readonly IDXGIDebug1 _dxgiDebug; +#endif + + public DX12DebugLayer() + { +#if DEBUG + _d3d12Debug = D3D12.D3D12GetDebugInterface(); + _d3d12Debug.EnableDebugLayer(); + + _dxgiDebug = DXGI.DXGIGetDebugInterface1(); + _dxgiDebug.EnableLeakTrackingForThread(); +#endif + } + + public void Dispose() + { +#if DEBUG + _dxgiDebug.ReportLiveObjects(DXGI.DebugAll, ReportLiveObjectFlags.Detail | ReportLiveObjectFlags.IgnoreInternal); + + _d3d12Debug?.Dispose(); + _dxgiDebug?.Dispose(); +#endif + } +} \ No newline at end of file diff --git a/Ghost.Graphics/DX12/DX12GraphicsDevice.cs b/Ghost.Graphics/DX12/DX12GraphicsDevice.cs new file mode 100644 index 0000000..c636605 --- /dev/null +++ b/Ghost.Graphics/DX12/DX12GraphicsDevice.cs @@ -0,0 +1,117 @@ +using Ghost.Graphics.Contracts; +using Ghost.Graphics.Data; +using Vortice.Direct3D; +using Vortice.Direct3D12; +using Vortice.DXGI; + +namespace Ghost.Graphics.DX12; + +internal class DX12GraphicsDevice : IGraphicsDevice +{ + private readonly IDXGIFactory7 _dxgiFactory; + private readonly ID3D12Device14 _device; + private readonly ID3D12CommandQueue _commandQueue; + + private readonly List _renderViews = new(); + +#if DEBUG + private readonly IDebugLayer _debugLayer; +#endif + + public ID3D12Device14 Device => _device; + public IDXGIFactory7 DXGIFactory => _dxgiFactory; + public ID3D12CommandQueue CommandQueue => _commandQueue; + + public static IGraphicsDevice Create() => new DX12GraphicsDevice(); + + private DX12GraphicsDevice() + { +#if DEBUG + _debugLayer = new DX12DebugLayer(); +#endif + + InitializeDevice(out _dxgiFactory, out _device); + InitializeCommandQueue(out _commandQueue); + } + + private void InitializeDevice(out IDXGIFactory7 factory, out ID3D12Device14 device) + { +#if DEBUG + factory = DXGI.CreateDXGIFactory2(true); +#else + factory = DXGI.CreateDXGIFactory2(false); +#endif + + ID3D12Device14? d3d12Device = default; + for (uint adapterIndex = 0; + factory.EnumAdapters1(adapterIndex, out var adapter).Success; + adapterIndex++) + { + var desc = adapter.Description1; + + // Don't select the Basic Render Driver adapter. + if ((desc.Flags & AdapterFlags.Software) != AdapterFlags.None) + { + adapter.Dispose(); + continue; + } + + if (D3D12.D3D12CreateDevice(adapter, FeatureLevel.Level_11_0, out d3d12Device).Success) + { + adapter.Dispose(); + break; + } + } + + if (d3d12Device == null) + { + throw new PlatformNotSupportedException("Cannot create ID3D12Device"); + } + + device = d3d12Device; + } + + private void InitializeCommandQueue(out ID3D12CommandQueue queue) + { + var queueDesc = new CommandQueueDescription + { + Type = CommandListType.Direct, + Priority = (int)CommandQueuePriority.High, + Flags = CommandQueueFlags.None, + }; + + queue = _device.CreateCommandQueue(queueDesc); + } + + public IRenderView CreateRenderView(in SwapChainSurface swapChainSurface) + { + var renderView = new DX12RenderView(this, swapChainSurface); + _renderViews.Add(renderView); + return renderView; + } + + public void OnRender() + { + foreach (var renderView in _renderViews) + { + renderView.Render(); + } + } + + public void Dispose() + { + foreach (var renderView in _renderViews) + { + renderView.Dispose(); + } + _renderViews.Clear(); + + _commandQueue?.Dispose(); + _device?.Dispose(); + _dxgiFactory?.Dispose(); + +#if DEBUG + _debugLayer.Dispose(); +#endif + } +} \ No newline at end of file diff --git a/Ghost.Graphics/DX12/DX12RenderView.cs b/Ghost.Graphics/DX12/DX12RenderView.cs new file mode 100644 index 0000000..ae0414c --- /dev/null +++ b/Ghost.Graphics/DX12/DX12RenderView.cs @@ -0,0 +1,219 @@ +using Ghost.Graphics.Contracts; +using Ghost.Graphics.Data; +using Ghost.Graphics.DX12.Utilities; +using System.Runtime.CompilerServices; +using Vortice.Direct3D12; +using Vortice.DXGI; + +namespace Ghost.Graphics.DX12; + +internal class DX12RenderView : IRenderView +{ + private const int _RENDER_TARGET_VIEW_HEAP_SIZE = 1024; + private const int _DEPTH_STENCIL_VIEW_HEAP_SIZE = 256; + + private readonly DX12GraphicsDevice _graphicsDevice; + + private readonly IDXGISwapChain4 _swapChain; + private readonly ID3D12Resource[] _renderTargets; + private readonly uint[] _renderTargetDescriptorIndexes; + private uint _backBufferIndex; + + private readonly ID3D12CommandAllocator[] _commandAllocators; + private readonly ID3D12GraphicsCommandList7 _commandList; + + private readonly ID3D12Fence1 _fence; + private readonly AutoResetEvent _fenceEvent; + private readonly ulong[] _fenceValues; + + private readonly D3D12DescriptorAllocator _rtvHeap; + + public DX12RenderView(DX12GraphicsDevice pipelineContext, in SwapChainSurface swapChainSurface) + { + _graphicsDevice = pipelineContext; + + _rtvHeap = new(_graphicsDevice.Device, DescriptorHeapType.RenderTargetView, _RENDER_TARGET_VIEW_HEAP_SIZE); + + _fenceEvent = new AutoResetEvent(false); + _renderTargets = new ID3D12Resource[GraphicsPipeline.FRAME_COUNT]; + _fenceValues = new ulong[GraphicsPipeline.FRAME_COUNT]; + _renderTargetDescriptorIndexes = new uint[GraphicsPipeline.FRAME_COUNT]; + + InitializeSwapChain(swapChainSurface, out _swapChain); + InitializeCommandObjects(out _commandAllocators, out _commandList, out _fence); + CreateRenderTargets(); + } + + private void InitializeSwapChain(in SwapChainSurface swapChainSurface, out IDXGISwapChain4 swapChain) + { + var swapChainDesc = new SwapChainDescription1 + { + Width = swapChainSurface.Width, + Height = swapChainSurface.Height, + Format = Format.B8G8R8A8_UNorm, + Stereo = false, + SampleDescription = new SampleDescription(1, 0), + BufferUsage = Usage.RenderTargetOutput, + BufferCount = GraphicsPipeline.FRAME_COUNT, + Scaling = Scaling.Stretch, + SwapEffect = SwapEffect.FlipDiscard, + AlphaMode = AlphaMode.Ignore, + Flags = SwapChainFlags.AllowTearing + }; + + // NOTE: Not going to need it for now, this is for standalone applications. + var swapChainFullscreenDesc = new SwapChainFullscreenDescription + { + Windowed = true, + }; + + switch (swapChainSurface.Type) + { + case SwapChainSurface.TargetType.Composition: + var swapChain1 = _graphicsDevice.DXGIFactory.CreateSwapChainForComposition(_graphicsDevice.CommandQueue, swapChainDesc); + swapChain = swapChain1.QueryInterface(); + + _backBufferIndex = swapChain.CurrentBackBufferIndex; + swapChainSurface.SwapChainPanelNative!.SetSwapChain(swapChain); + break; + case SwapChainSurface.TargetType.Hwnd: + var swapChain2 = _graphicsDevice.DXGIFactory.CreateSwapChainForHwnd( + _graphicsDevice.CommandQueue, + swapChainSurface.Hwnd, + swapChainDesc, + swapChainFullscreenDesc, + null); + swapChain = swapChain2.QueryInterface(); + break; + default: + throw new ArgumentException("Unsupported swap chain surface type."); + } + } + + private void InitializeCommandObjects(out ID3D12CommandAllocator[] commandAllocator, out ID3D12GraphicsCommandList7 commandList, out ID3D12Fence1 fence) + { + commandAllocator = new ID3D12CommandAllocator[GraphicsPipeline.FRAME_COUNT]; + for (var i = 0; i < GraphicsPipeline.FRAME_COUNT; i++) + { + commandAllocator[i] = _graphicsDevice.Device.CreateCommandAllocator(CommandListType.Direct); + } + + commandList = _graphicsDevice.Device.CreateCommandList(CommandListType.Direct, commandAllocator[0], null!); + fence = _graphicsDevice.Device.CreateFence(_fenceValues[_backBufferIndex], FenceFlags.None); + + _commandList.Close(); + _fenceValues[_backBufferIndex]++; + } + + private void CreateRenderTargets() + { + for (var i = 0u; i < GraphicsPipeline.FRAME_COUNT; i++) + { + _renderTargets[i] = _swapChain.GetBuffer(i); + _renderTargets[i].Name = $"RenderTarget_{i}"; + _renderTargetDescriptorIndexes[i] = _rtvHeap.AllocateDescriptor(); + + var rtvHandle = _rtvHeap.GetCpuHandle(_renderTargetDescriptorIndexes[i]); + _graphicsDevice.Device.CreateRenderTargetView(_renderTargets[i], null, rtvHandle); + } + } + + public void Resize(uint width, uint height) + { + WaitIdle(); + + for (var i = 0; i < GraphicsPipeline.FRAME_COUNT; i++) + { + _renderTargets[i].Dispose(); + _rtvHeap.ReleaseDescriptor(_renderTargetDescriptorIndexes[i]); + + _fenceValues[i] = _fenceValues[_backBufferIndex]; + } + + _swapChain.ResizeBuffers(GraphicsPipeline.FRAME_COUNT, width, height, Format.B8G8R8A8_UNorm, SwapChainFlags.AllowTearing).CheckError(); + + CreateRenderTargets(); + _backBufferIndex = _swapChain.CurrentBackBufferIndex; + } + + public void Render() + { + var commandAllocator = _commandAllocators[_backBufferIndex]; + commandAllocator.Reset(); + _commandList.Reset(commandAllocator, null); + + _commandList.Close(); + _graphicsDevice.CommandQueue.ExecuteCommandLists([_commandList]); + + _swapChain.Present(1, PresentFlags.None).CheckError(); + + WaitNextFrame(); + } + + public void WaitNextFrame() + { + var fenceValue = _fenceValues[_backBufferIndex]; + + if (_graphicsDevice.CommandQueue.Signal(_fence, fenceValue).Failure) + { + return; + } + + _backBufferIndex = _swapChain.CurrentBackBufferIndex; + if (_fence.CompletedValue < _fenceValues[_backBufferIndex] + && _fence.SetEventOnCompletion(_fenceValues[_backBufferIndex], _fenceEvent.SafeWaitHandle.DangerousGetHandle()).Success) + { + _fenceEvent.WaitOne(); + } + + _fenceValues[_backBufferIndex]++; + } + + public void WaitIdle() + { + var fenceValue = _fenceValues[_backBufferIndex]; + if (_graphicsDevice.CommandQueue.Signal(_fence, fenceValue).Failure + || _fence.SetEventOnCompletion(fenceValue, _fenceEvent.SafeWaitHandle.DangerousGetHandle()).Failure) + { + return; + } + + _fenceEvent.WaitOne(); + _fenceValues[_backBufferIndex]++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Flush() + { + for (var i = 0; i < GraphicsPipeline.FRAME_COUNT; i++) + { + WaitIdle(); + } + } + + public void Dispose() + { + Flush(); + + foreach (var commandAllocator in _commandAllocators) + { + commandAllocator.Dispose(); + } + + foreach (var renderTarget in _renderTargets) + { + renderTarget.Dispose(); + } + + _swapChain.Dispose(); + _commandList.Dispose(); + + _fence.Dispose(); + _fenceEvent.Dispose(); + + _rtvHeap.Dispose(); + + _backBufferIndex = 0; + _fenceValues.AsSpan().Clear(); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/DX12/Utilities/D3D12DescriptorAllocator.cs b/Ghost.Graphics/DX12/Utilities/D3D12DescriptorAllocator.cs new file mode 100644 index 0000000..cb25fd7 --- /dev/null +++ b/Ghost.Graphics/DX12/Utilities/D3D12DescriptorAllocator.cs @@ -0,0 +1,238 @@ +using System.Diagnostics; +using System.Numerics; +using Vortice.Direct3D12; +using DescriptorIndex = System.UInt32; + +namespace Ghost.Graphics.DX12.Utilities; + +internal class D3D12DescriptorAllocator : IDisposable +{ + private const DescriptorIndex _INVALID_DESCRIPTOR_INDEX = ~0u; + + private readonly ID3D12Device _device; + private readonly Lock _mutex = new(); + + private ID3D12DescriptorHeap? _heap; + private ID3D12DescriptorHeap? _shaderVisibleHeap; + private CpuDescriptorHandle _startCpuHandle = default; + private CpuDescriptorHandle _startCpuHandleShaderVisible = default; + private GpuDescriptorHandle _startGpuHandleShaderVisible = default; + private DescriptorIndex _searchStart; + private bool[] _allocatedDescriptors = []; + + public DescriptorHeapType HeapType + { + get; + } + + public uint NumDescriptors + { + get; private set; + } + + public uint NumAllocatedDescriptors + { + get; private set; + } + + public bool ShaderVisible + { + get; + } + + public uint Stride + { + get; + } + + public ID3D12DescriptorHeap Heap => _heap!; + public ID3D12DescriptorHeap? ShaderVisibleHeap => _shaderVisibleHeap; + + public D3D12DescriptorAllocator(ID3D12Device device, DescriptorHeapType type, uint numDescriptors) + { + _device = device; + HeapType = type; + NumDescriptors = numDescriptors; + ShaderVisible = type == DescriptorHeapType.ConstantBufferViewShaderResourceViewUnorderedAccessView || type == DescriptorHeapType.Sampler; + Stride = device.GetDescriptorHandleIncrementSize(type); + + Debug.Assert(AllocateResources(numDescriptors)); + } + + public DescriptorIndex AllocateDescriptor() => AllocateDescriptors(1); + + public DescriptorIndex AllocateDescriptors(uint count) + { + lock (_mutex) + { + DescriptorIndex foundIndex = 0; + uint freeCount = 0; + var found = false; + + // Find a contiguous range of 'count' indices for which _allocatedDescriptors[index] is false + for (var index = _searchStart; index < NumDescriptors; index++) + { + if (_allocatedDescriptors[index]) + { + freeCount = 0; + } + else + { + freeCount += 1; + } + + if (freeCount >= count) + { + foundIndex = index > 0 ? index - count + 1 : 0; + found = true; + break; + } + } + + if (!found) + { + foundIndex = NumDescriptors; + + if (!Grow(NumDescriptors + count)) + { + Debug.WriteLine("ERROR: Failed to grow a descriptor heap!"); + return _INVALID_DESCRIPTOR_INDEX; + } + } + + for (var index = foundIndex; index < foundIndex + count; index++) + { + _allocatedDescriptors[index] = true; + } + + NumAllocatedDescriptors += count; + _searchStart = foundIndex + count; + return foundIndex; + } + } + + public void ReleaseDescriptor(DescriptorIndex index) => ReleaseDescriptors(index, 1); + + public void ReleaseDescriptors(DescriptorIndex baseIndex, uint count = 1) + { + if (count == 0) + { + return; + } + + lock (_mutex) + { + for (var index = baseIndex; index < baseIndex + count; index++) + { +#if DEBUG + if (!_allocatedDescriptors[index]) + { + Debug.WriteLine("Error: Attempted to release an un-allocated descriptor"); + } +#endif + + _allocatedDescriptors[index] = false; + } + + NumAllocatedDescriptors -= count; + + if (_searchStart > baseIndex) + { + _searchStart = baseIndex; + } + } + } + + public CpuDescriptorHandle GetCpuHandle(DescriptorIndex index) + { + var handle = _startCpuHandle; + return handle.Offset((int)index, Stride); + } + + public CpuDescriptorHandle GetCpuHandleShaderVisible(DescriptorIndex index) + { + var handle = _startCpuHandleShaderVisible; + return handle.Offset((int)index, Stride); + } + + public GpuDescriptorHandle GetGpuHandle(DescriptorIndex index) + { + var handle = _startGpuHandleShaderVisible; + return handle.Offset((int)index, Stride); + } + + public void CopyToShaderVisibleHeap(DescriptorIndex index, uint count = 1) + { + _device.CopyDescriptorsSimple(count, GetCpuHandleShaderVisible(index), GetCpuHandle(index), HeapType); + } + + private bool AllocateResources(uint numDescriptors) + { + NumDescriptors = numDescriptors; + _heap?.Dispose(); + _shaderVisibleHeap?.Dispose(); + + DescriptorHeapDescription heapDesc = new() + { + Type = HeapType, + DescriptorCount = numDescriptors, + Flags = DescriptorHeapFlags.None, + NodeMask = 0 + }; + + var hr = _device.CreateDescriptorHeap(in heapDesc, out _heap); + if (hr.Failure) + { + return false; + } + + _startCpuHandle = _heap!.GetCPUDescriptorHandleForHeapStart(); + Array.Resize(ref _allocatedDescriptors, (int)numDescriptors); + + if (ShaderVisible) + { + heapDesc.Flags = DescriptorHeapFlags.ShaderVisible; + + hr = _device.CreateDescriptorHeap(in heapDesc, out _shaderVisibleHeap); + + if (hr.Failure) + { + return false; + } + + _startCpuHandleShaderVisible = _shaderVisibleHeap!.GetCPUDescriptorHandleForHeapStart(); + _startGpuHandleShaderVisible = _shaderVisibleHeap!.GetGPUDescriptorHandleForHeapStart(); + } + + return true; + } + + private bool Grow(uint minRequiredSize) + { + var oldSize = NumDescriptors; + var newSize = BitOperations.RoundUpToPowerOf2(minRequiredSize); + + var oldHeap = _heap; + + if (!AllocateResources(newSize)) + { + return false; + } + + _device.CopyDescriptorsSimple(oldSize, _startCpuHandle, oldHeap!.GetCPUDescriptorHandleForHeapStart(), HeapType); + + if (_shaderVisibleHeap is not null) + { + _device.CopyDescriptorsSimple(oldSize, _startCpuHandleShaderVisible, oldHeap.GetCPUDescriptorHandleForHeapStart(), HeapType); + } + + return true; + } + + /// + public void Dispose() + { + _heap?.Dispose(); + _shaderVisibleHeap?.Dispose(); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/Data/Camera.cs b/Ghost.Graphics/Data/Camera.cs new file mode 100644 index 0000000..d4ce3ee --- /dev/null +++ b/Ghost.Graphics/Data/Camera.cs @@ -0,0 +1,5 @@ +namespace Ghost.Graphics.Data; + +public class Camera +{ +} \ No newline at end of file diff --git a/Ghost.Graphics/Data/Color32.cs b/Ghost.Graphics/Data/Color32.cs new file mode 100644 index 0000000..f948ba6 --- /dev/null +++ b/Ghost.Graphics/Data/Color32.cs @@ -0,0 +1,27 @@ +using System.Drawing; + +namespace Ghost.Graphics.Data; + +/// +/// Represents a color with 32-bit components, the unmanaged version of ."/> +/// +public struct Color32 +{ + public byte r; + public byte g; + public byte b; + public byte a; + + public Color32(byte r, byte g, byte b, byte a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + public Color32(Color color) + : this(color.R, color.G, color.B, color.A) + { + } +} \ No newline at end of file diff --git a/Ghost.Graphics/Data/GraphicsAPI.cs b/Ghost.Graphics/Data/GraphicsAPI.cs new file mode 100644 index 0000000..a1c711e --- /dev/null +++ b/Ghost.Graphics/Data/GraphicsAPI.cs @@ -0,0 +1,6 @@ +namespace Ghost.Graphics.Data; + +public enum GraphicsAPI +{ + DX12 +} \ No newline at end of file diff --git a/Ghost.Graphics/Data/Mesh.cs b/Ghost.Graphics/Data/Mesh.cs new file mode 100644 index 0000000..275749f --- /dev/null +++ b/Ghost.Graphics/Data/Mesh.cs @@ -0,0 +1,234 @@ +using Misaki.HighPerformance.Unsafe.Collections; +using Misaki.HighPerformance.Unsafe.Helpers; +using System.Numerics; + +namespace Ghost.Graphics.Data; + +public sealed class Mesh(int initialVertexCapacity = 256, int initialIndexCapacity = 512) : IDisposable +{ + private UnsafeList _vertices = new(initialVertexCapacity, Allocator.Persistent); + private UnsafeList _normals = new(initialVertexCapacity, Allocator.Persistent); + private UnsafeList _tangents = new(initialVertexCapacity, Allocator.Persistent); + private UnsafeList _colors = new(initialVertexCapacity, Allocator.Persistent); + private UnsafeList _uvs = new(initialVertexCapacity, Allocator.Persistent); + private UnsafeList _indices = new(initialIndexCapacity, Allocator.Persistent); + + public Span Vertices => _vertices.AsSpan(); + public Span Normals => _normals.AsSpan(); + public Span Tangents => _tangents.AsSpan(); + public Span Colors => _colors.AsSpan(); + public Span UVs => _uvs.AsSpan(); + public Span Indices => _indices.AsSpan(); + + public int VertexCount => _vertices.Count; + + ~Mesh() + { + Dispose(); + } + + /// + /// Adds a vertex to the mesh with the specified attributes. + /// + /// This method adds the vertex attributes to their respective collections, allowing the mesh + /// to be constructed with detailed vertex data. Ensure that all parameters are provided with valid values to + /// avoid incomplete or incorrect mesh data. + /// The position of the vertex in 3D space. + /// The normal vector at the vertex. + /// The tangent vector at the vertex. + /// The color of the vertex. + /// The UV coordinates of the vertex. + public void AddVertex(Vector3 position, Vector3 normal, Vector4 tangent, Color32 color, Vector2 uv) + { + _vertices.Add(position); + _normals.Add(normal); + _tangents.Add(tangent); + _colors.Add(color); + _uvs.Add(uv); + } + + /// + /// Adds a triangle to the mesh by specifying the indices of its three vertices. + /// + /// The index of the first vertex in the triangle. Must be within the range of the current vertex count. + /// The index of the second vertex in the triangle. Must be within the range of the current vertex count. + /// The index of the third vertex in the triangle. Must be within the range of the current vertex count. + /// Thrown if any of the specified indices are out of range for the current vertex count. + public void AddTriangle(int index0, int index1, int index2) + { + if (index0 >= _vertices.Count || index1 >= _vertices.Count || index2 >= _vertices.Count) + { + throw new ArgumentOutOfRangeException("Index out of range for the current vertex count."); + } + + _indices.Add(index0); + _indices.Add(index1); + _indices.Add(index2); + } + + public void TrimExcess() + { + _vertices.Resize(_vertices.Count); + _normals.Resize(_normals.Count); + _tangents.Resize(_tangents.Count); + _colors.Resize(_colors.Count); + _uvs.Resize(_uvs.Count); + _indices.Resize(_indices.Count); + } + + /// + /// Auto-compute smooth per-vertex normals. + /// + /// + /// Call this method before vertices and indices are valid. + /// + public void ComputeNormal() + { + if (!_vertices.IsCreated || _vertices.Count < 3 + || !_indices.IsCreated || _indices.Count < 3) + { + return; + } + + if (!_normals.IsCreated) + { + _normals = new(_vertices.Count, Allocator.Persistent); + } + else + { + _normals.Clear(); + _normals.Resize(_vertices.Count); + } + + for (var i = 0; i < _indices.Count; i += 3) + { + var i0 = _indices[i]; + var i1 = _indices[i + 1]; + var i2 = _indices[i + 2]; + + var v0 = _vertices[i0]; + var v1 = _vertices[i1]; + var v2 = _vertices[i2]; + + var edge1 = v1 - v0; + var edge2 = v2 - v0; + var faceNormal = Vector3.Cross(edge1, edge2); + + _normals[i0] += faceNormal; + _normals[i1] += faceNormal; + _normals[i2] += faceNormal; + } + + for (var i = 0; i < _normals.Count; i++) + { + _normals[i] = Vector3.Normalize(_normals[i]); + } + } + + /// + /// Auto-compute per-vertex tangents. + /// + /// + /// Call this method before vertices, normals, and UVs are valid. + /// + public void ComputeTangents() + { + if (!_vertices.IsCreated || _vertices.Count < 3 + || !_indices.IsCreated || _indices.Count < 3 + || !_uvs.IsCreated || _uvs.Count < _vertices.Count) + { + return; + } + + if (!_normals.IsCreated || _normals.Count != _vertices.Count) + { + ComputeNormal(); + } + + if (!_tangents.IsCreated) + { + _tangents = new(_vertices.Count, Allocator.Persistent); + } + else + { + _tangents.Clear(); + _tangents.Resize(_vertices.Count); + } + + var bitangents = new Vector3[_vertices.Count]; + + for (var i = 0; i < _indices.Count; i += 3) + { + var i0 = _indices[i]; + var i1 = _indices[i + 1]; + var i2 = _indices[i + 2]; + + var v0 = _vertices[i0]; + var v1 = _vertices[i1]; + var v2 = _vertices[i2]; + var uv0 = _uvs[i0]; + var uv1 = _uvs[i1]; + var uv2 = _uvs[i2]; + + var deltaPos1 = v1 - v0; + var deltaPos2 = v2 - v0; + var deltaUV1 = uv1 - uv0; + var deltaUV2 = uv2 - uv0; + + var r = 1.0f / (deltaUV1.X * deltaUV2.Y - deltaUV1.Y * deltaUV2.X); + var tangent = (deltaPos1 * deltaUV2.Y - deltaPos2 * deltaUV1.Y) * r; + var bitangent = (deltaPos2 * deltaUV1.X - deltaPos1 * deltaUV2.X) * r; + + for (var j = 0; j < 3; j++) + { + var idx = _indices[i + j]; + var t = _tangents[idx]; + _tangents[idx] = new Vector4( + t.X + tangent.X, + t.Y + tangent.Y, + t.Z + tangent.Z, + 0.0f // we’ll fill w later + ); + + bitangents[idx] += bitangent; + } + } + + for (var i = 0; i < _vertices.Count; i++) + { + var n = _normals![i]; + var t = _tangents[i]; + var t3 = new Vector3(t.X, t.Y, t.Z); + + var proj = n * Vector3.Dot(n, t3); + t3 = Vector3.Normalize(t3 - proj); + + var b = bitangents[i]; + var w = Vector3.Dot(Vector3.Cross(n, t3), b) < 0.0f ? -1.0f : 1.0f; + + _tangents[i] = new Vector4(t3.X, t3.Y, t3.Z, w); + } + } + + public void Clear() + { + _vertices.Clear(); + _normals.Clear(); + _tangents.Clear(); + _colors.Clear(); + _uvs.Clear(); + _indices.Clear(); + } + + public void Dispose() + { + _vertices.Dispose(); + _normals.Dispose(); + _tangents.Dispose(); + _colors.Dispose(); + _uvs.Dispose(); + _indices.Dispose(); + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/Data/SwapChainSurface.cs b/Ghost.Graphics/Data/SwapChainSurface.cs new file mode 100644 index 0000000..205020f --- /dev/null +++ b/Ghost.Graphics/Data/SwapChainSurface.cs @@ -0,0 +1,53 @@ +namespace Ghost.Graphics.Data; + +internal readonly struct SwapChainSurface +{ + public enum TargetType + { + Composition, + Hwnd + } + + public readonly TargetType Type + { + get; + } + + public readonly Vortice.WinUI.ISwapChainPanelNative? SwapChainPanelNative + { + get; + } + + public readonly IntPtr Hwnd + { + get; + } + + public readonly uint Width + { + get; + } + + public readonly uint Height + { + get; + } + + public SwapChainSurface(Vortice.WinUI.ISwapChainPanelNative swapChainPanelNative, uint width, uint height) + { + Type = TargetType.Composition; + SwapChainPanelNative = swapChainPanelNative; + Hwnd = IntPtr.Zero; + Width = width; + Height = height; + } + + public SwapChainSurface(IntPtr hwnd, uint width, uint height) + { + Type = TargetType.Hwnd; + SwapChainPanelNative = null; + Hwnd = hwnd; + Width = width; + Height = height; + } +} \ No newline at end of file diff --git a/Ghost.Graphics/Ghost.Graphics.csproj b/Ghost.Graphics/Ghost.Graphics.csproj index 6a87d7c..1a17af8 100644 --- a/Ghost.Graphics/Ghost.Graphics.csproj +++ b/Ghost.Graphics/Ghost.Graphics.csproj @@ -5,6 +5,7 @@ enable enable True + preview @@ -17,6 +18,17 @@ + + + + + + + + + + ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll + diff --git a/Ghost.Graphics/GraphicsPipeline.cs b/Ghost.Graphics/GraphicsPipeline.cs new file mode 100644 index 0000000..f7218c2 --- /dev/null +++ b/Ghost.Graphics/GraphicsPipeline.cs @@ -0,0 +1,77 @@ +using Ghost.Graphics.Contracts; +using Ghost.Graphics.Data; + +namespace Ghost.Graphics; + +internal static class GraphicsPipeline +{ + public const int FRAME_COUNT = 2; + + private static IGraphicsDevice? _graphicsDevice; + private static Thread? _renderThread; + + private static bool _isRunning; + + public static IGraphicsDevice GraphicsDevice + { + get + { + if (_graphicsDevice == null) + { + throw new InvalidOperationException("Graphics pipeline is not initialized."); + } + return _graphicsDevice; + } + } + + public static void Initialize(GraphicsAPI api) + { + _graphicsDevice = api switch + { + GraphicsAPI.DX12 => DX12.DX12GraphicsDevice.Create(), + _ => throw new NotSupportedException($"Graphics API {api} is not supported.") + }; + + _renderThread = new Thread(RenderLoop); + } + + private static void RenderLoop() + { + while (_isRunning) + { + GraphicsDevice.OnRender(); + } + } + + public static void Start() + { + if (_isRunning) + { + return; + } + + if (_graphicsDevice == null || _renderThread == null) + { + throw new InvalidOperationException("Graphics pipeline is not initialized."); + } + + _isRunning = true; + _renderThread.Start(); + } + + public static void Stop() + { + _isRunning = false; + _renderThread?.Join(); + } + + public static void Shutdown() + { + Stop(); + + _graphicsDevice?.Dispose(); + + _graphicsDevice = null; + _renderThread = null; + } +} \ No newline at end of file diff --git a/Ghost.Graphics/Utilities/MeshBuilder.cs b/Ghost.Graphics/Utilities/MeshBuilder.cs new file mode 100644 index 0000000..6c27a0d --- /dev/null +++ b/Ghost.Graphics/Utilities/MeshBuilder.cs @@ -0,0 +1,126 @@ +using Ghost.Graphics.Data; +using System.Numerics; + +namespace Ghost.Graphics.Utilities; + +public static class MeshBuilder +{ + /// + /// Creates a unit cube centered at the origin with size 1. + /// + public static Mesh CreateCube(float size = 1.0f, Color32 color = default) + { + var half = size * 0.5f; + var mesh = new Mesh(24, 36); + + var corners = new Vector3[] + { + new(-half, -half, -half), new( half, -half, -half), new( half, half, -half), new(-half, half, -half), + new(-half, -half, half), new( half, -half, half), new( half, half, half), new(-half, half, half) + }; + + int[][] faces = + [ + [0,1,2,3], + [5,4,7,6], + [4,0,3,7], + [1,5,6,2], + [3,2,6,7], + [4,5,1,0] + ]; + + var uvs = new Vector2[] { new(0, 0), new(1, 0), new(1, 1), new(0, 1) }; + + foreach (var face in faces) + { + var baseIndex = mesh.VertexCount; + for (var i = 0; i < 4; i++) + { + mesh.AddVertex(corners[face[i]], Vector3.Zero, Vector4.Zero, color, uvs[i]); + } + + mesh.AddTriangle(baseIndex + 0, baseIndex + 1, baseIndex + 2); + mesh.AddTriangle(baseIndex + 0, baseIndex + 2, baseIndex + 3); + } + + mesh.ComputeNormal(); + mesh.ComputeTangents(); + return mesh; + } + + /// + /// Creates a plane on the XZ axis centered at the origin. + /// + public static Mesh CreatePlane(float width = 1.0f, float depth = 1.0f, Color32 color = default) + { + var hw = width * 0.5f; + var hd = depth * 0.5f; + var mesh = new Mesh(4, 6); + + mesh.AddVertex(new(-hw, 0, -hd), Vector3.Zero, Vector4.Zero, color, new(0, 0)); + mesh.AddVertex(new(hw, 0, -hd), Vector3.Zero, Vector4.Zero, color, new(1, 0)); + mesh.AddVertex(new(hw, 0, hd), Vector3.Zero, Vector4.Zero, color, new(1, 1)); + mesh.AddVertex(new(-hw, 0, hd), Vector3.Zero, Vector4.Zero, color, new(0, 1)); + + mesh.AddTriangle(0, 1, 2); + mesh.AddTriangle(0, 2, 3); + + mesh.ComputeNormal(); + mesh.ComputeTangents(); + return mesh; + } + + /// + /// Creates a UV sphere centered at the origin. + /// + public static Mesh CreateSphere(int latitudeSegments = 16, int longitudeSegments = 24, float radius = 0.5f, Color32 color = default) + { + var mesh = new Mesh((latitudeSegments + 1) * (longitudeSegments + 1), latitudeSegments * longitudeSegments * 6); + + // Vertices + for (var lat = 0; lat <= latitudeSegments; lat++) + { + var theta = (float)lat / latitudeSegments * MathF.PI; + var sinTheta = MathF.Sin(theta); + var cosTheta = MathF.Cos(theta); + + for (var lon = 0; lon <= longitudeSegments; lon++) + { + var phi = (float)lon / longitudeSegments * 2 * MathF.PI; + var sinPhi = MathF.Sin(phi); + var cosPhi = MathF.Cos(phi); + + var x = cosPhi * sinTheta; + var y = cosTheta; + var z = sinPhi * sinTheta; + + mesh.AddVertex( + position: new Vector3(x, y, z) * radius, + normal: Vector3.Zero, + tangent: Vector4.Zero, + color: color, + uv: new Vector2((float)lon / longitudeSegments, (float)lat / latitudeSegments) + ); + } + } + + // Indices + for (var lat = 0; lat < latitudeSegments; lat++) + { + for (var lon = 0; lon < longitudeSegments; lon++) + { + var i0 = lat * (longitudeSegments + 1) + lon; + var i1 = i0 + longitudeSegments + 1; + var i2 = i1 + 1; + var i3 = i0 + 1; + + mesh.AddTriangle(i0, i1, i2); + mesh.AddTriangle(i0, i2, i3); + } + } + + mesh.ComputeNormal(); + mesh.ComputeTangents(); + return mesh; + } +} \ No newline at end of file diff --git a/Ghost.UnitTest/Ghost.UnitTest.csproj b/Ghost.UnitTest/Ghost.UnitTest.csproj index 7922365..4992dfe 100644 --- a/Ghost.UnitTest/Ghost.UnitTest.csproj +++ b/Ghost.UnitTest/Ghost.UnitTest.csproj @@ -44,12 +44,12 @@ - - - + + + - + diff --git a/GhostEngine.sln b/GhostEngine.sln index 43bc32d..9b9ee17 100644 --- a/GhostEngine.sln +++ b/GhostEngine.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.35906.104 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Editor", "Ghost.App\Ghost.Editor.csproj", "{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Editor", "Ghost.Editor\Ghost.Editor.csproj", "{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Engine", "Ghost.Engine\Ghost.Engine.csproj", "{1ED62E09-8F36-4671-896B-16C1C1530202}" EndProject @@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Generator", "Ghost.Ge EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.UnitTest", "Ghost.UnitTest\Ghost.UnitTest.csproj", "{4179873E-8174-4D17-9584-8C223BA71366}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Core", "Ghost.Core\Ghost.Core.csproj", "{337D9110-76FC-453C-9481-757052A216E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Editor.Core", "Ghost.Editor.Core\Ghost.Editor.Core.csproj", "{222A4E83-D902-423A-8E99-8321BBFC604C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -123,6 +127,30 @@ Global {4179873E-8174-4D17-9584-8C223BA71366}.Release|x86.ActiveCfg = Release|x86 {4179873E-8174-4D17-9584-8C223BA71366}.Release|x86.Build.0 = Release|x86 {4179873E-8174-4D17-9584-8C223BA71366}.Release|x86.Deploy.0 = Release|x86 + {337D9110-76FC-453C-9481-757052A216E5}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Debug|ARM64.Build.0 = Debug|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Debug|x64.Build.0 = Debug|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Debug|x86.Build.0 = Debug|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Release|ARM64.ActiveCfg = Release|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Release|ARM64.Build.0 = Release|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Release|x64.ActiveCfg = Release|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Release|x64.Build.0 = Release|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Release|x86.ActiveCfg = Release|Any CPU + {337D9110-76FC-453C-9481-757052A216E5}.Release|x86.Build.0 = Release|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Debug|ARM64.Build.0 = Debug|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Debug|x64.ActiveCfg = Debug|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Debug|x64.Build.0 = Debug|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Debug|x86.ActiveCfg = Debug|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Debug|x86.Build.0 = Debug|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Release|ARM64.ActiveCfg = Release|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Release|ARM64.Build.0 = Release|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Release|x64.ActiveCfg = Release|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Release|x64.Build.0 = Release|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Release|x86.ActiveCfg = Release|Any CPU + {222A4E83-D902-423A-8E99-8321BBFC604C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE