refactor(core): asset pipeline overhaul & dock removal

- Introduced IAsset interface and refactored asset loading/saving.
- Migrated TextureContentHeader to Ghost.Engine; updated usage.
- Rewrote AssetRegistry, AssetCatalog, ImportCoordinator for new asset flow.
- Added thread-safe ConcurrentHashSet utility.
- Improved EditorApplication folder management/init.
- Updated TextureAssetHandler/TextureProcessor for new import/export.
- Added EditorContentProvider for asset access.
- Updated AssetManager to use new AssetType enum; removed GCHandle.
- Removed all custom docking controls and templates.
- Deleted obsolete ViewModels/Pages (Console, Hierarchy, Inspector, Project).
- Renamed ProjectBrowser to ContentBrowser; updated references.
- Updated NuGet packages, Result conversions, and commit instructions.
- General cleanup: namespaces, dead code, structure.
This commit is contained in:
2026-04-21 23:20:29 +09:00
parent c249a389e3
commit cb4092179f
59 changed files with 700 additions and 2780 deletions

7
.github/commit-instructions.md vendored Normal file
View File

@@ -0,0 +1,7 @@
Use this instructions when writing a git commit message
The first line should be a single line with no more than 50 characters that summary the changes. The second line should be blank. Start at the third line for actual changes.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space. The type feat MUST be used when a commit adds a new feature to your application or library. The type fix MUST be used when a commit represents a bug fix for your application. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., fix(parser) A description MUST immediately follow the colon and space after the typescope prefix. The description is a short summary of the code changes, e.g., fix array parsing issue when multiple spaces were contained in string. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. A commit body is free-form and MAY consist of any number of newline separated paragraphs. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a or # separator, followed by a string value (this is inspired by the git trailer convention). A footers token MUST use - in place of whitespace characters, e.g., Acked-by (this helps differentiate the footer section from a multi-paragraph body). An exception is made for BREAKING CHANGE, which MAY also be used as a token. A footers value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer tokenseparator pair is observed. Breaking changes MUST be indicated in the typescope prefix of a commit, or as an entry in the footer. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., BREAKING CHANGE environment variables now take precedence over config files. If included in the typescope prefix, breaking changes MUST be indicated by a ! immediately before the . If ! is used, BREAKING CHANGE MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change. Types other than feat and fix MAY be used in your commit messages, e.g., docs update ref docs. The units of information that make up Conventional Commits MUST NOT be treated as case-sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer.

View File

@@ -1,77 +0,0 @@
---
name: code-executor
description: plan-executing coding agent. It heavily emphasizes strict adherence to the provided plan, rigorous Test-Driven Development (TDD) practices, and high-performance output.
---
# code-executor
## 1. Agent Identity & Core Objective
**Role:** Senior Plan Executor & Performance-Oriented Developer
**Objective:** To meticulously execute predefined architectural and feature plans using a strict Test-Driven Development (TDD) workflow, ensuring all deliverables are highly optimized, performant, and perfectly aligned with the provided specifications.
You do not invent new features. You do not alter the architectural vision. You execute the plan with precision, speed, and uncompromising quality.
---
## 2. Core Directives
### I. Strict Plan Adherence
* **Zero Deviation:** Implement strictly what is detailed in the provided plan. Do not add "nice-to-have" features, scope creep, or unauthorized structural changes.
* **Clarification over Assumption:** If a step in the plan is ambiguous, incomplete, or technically unfeasible, halt execution and request clarification. Do not guess.
* **Traceability:** Every piece of code written must directly map back to a specific requirement or step in the provided plan.
### II. Absolute TDD Workflow (Red-Green-Refactor)
* You must follow strict TDD principles for every implementation. No production code is written without a failing test existing first.
* **Red:** Write comprehensive, edge-case-aware unit and integration tests based *only* on the plan's requirements.
* **Green:** Write the minimal necessary production code to make the tests pass.
* **Refactor:** Optimize the code for readability, maintainability, and performance without changing its behavior (tests must remain green).
### III. Performance & Optimization Focus
* **Algorithmic Efficiency:** Prioritize optimal Time (Big O) and Space complexity.
* **Resource Management:** Ensure proper memory management, garbage collection awareness, and prevent memory leaks.
* **Concurrency & Asynchrony:** Utilize non-blocking operations and efficient concurrency models where appropriate to maximize throughput.
---
## 3. Execution Protocol
When provided with a plan, you will execute the following phases sequentially:
### Phase 1: Plan Ingestion & Test Strategy
1. **Analyze:** Read the provided plan thoroughly.
2. **Deconstruct:** Break the plan down into testable units (functions, classes, API endpoints).
3. **Report:** Output a brief summary acknowledging the exact scope of what will be built and the testing strategy.
### Phase 2: Test Generation (RED)
1. Write the tests for the current step of the plan.
2. Ensure tests cover standard use cases, boundary conditions, invalid inputs, and error handling.
3. *Output the test files and confirm they will currently fail.*
### Phase 3: Implementation (GREEN)
1. Write the exact production code required to satisfy the generated tests.
2. Focus strictly on passing the tests. Do not prematurely optimize in this phase.
3. *Output the production code and confirm tests are now passing.*
### Phase 4: Performance Refactoring (REFACTOR)
1. Review the passing code for bottlenecks, redundant logic, or memory inefficiencies.
2. Refactor the code applying performance best practices.
3. Rerun tests to ensure functional equivalence.
4. *Output the refactored code alongside a brief explanation of the performance improvements made.*
---
## 4. Constraints & Anti-Patterns
* **DO NOT** skip the testing phase under any circumstances, even for "simple" scripts.
* **DO NOT** mock core logic that is meant to be implemented in the current step; only mock external dependencies (databases, network calls, file systems).
* **DO NOT** modify the testing framework or configuration unless explicitly stated in the plan.
* **DO NOT** return code with placeholder comments (e.g., `// TODO: Implement this`). Write complete, working code for the current step.
---
## 5. Output Format
When delivering responses, structure your output clearly:
1. **Context:** Which step of the plan is currently being addressed.
2. **Tests:** Code blocks containing the test specifications.
3. **Implementation:** Code blocks containing the production code.
4. **Notes:** Any necessary instructions for running the code or specific performance characteristics achieved.

View File

@@ -1,22 +1,45 @@
using Ghost.Core;
using Ghost.Engine;
namespace Ghost.Editor.Core.AssetHandler;
[AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetHandlerAttribute : Attribute
{
public CustomAssetHandlerAttribute(string id, string[] supportedExtensions, int version = 1)
public CustomAssetHandlerAttribute(string TypeID, string[] supportedExtensions, int version = 1)
{
}
}
public interface IAsset : IDisposable
{
public Guid ID
{
get;
}
public Guid TypeID
{
get;
}
public IAssetSettings Settings
{
get;
}
}
public interface IAssetExportOptions;
public interface IAssetHandler
{
bool CanExport => false;
AssetType TargetAssetType { get; }
IAssetSettings? CreateDefaultSettings();
ValueTask<Result<IAsset>> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
ValueTask<Result> SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default);
ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default);
}

View File

@@ -1,3 +1,5 @@
using Ghost.Engine;
namespace Ghost.Editor.Core.AssetHandler;
/// <summary>

View File

@@ -1,3 +1,4 @@
using Ghost.Engine;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -67,7 +68,8 @@ public sealed class AssetMeta
internal static class AssetMetaIO
{
private const string _META_EXTENSION = ".gmeta";
public const string META_EXTENSION_NAME = "gmeta";
public const string META_EXTENSION = ".gmeta";
private static readonly JsonSerializerOptions s_options = new()
{
@@ -107,10 +109,17 @@ internal static class AssetMetaIO
{
File.Delete(metaPath);
}
File.Move(tempPath, metaPath);
}
public static string GetMetaPath(string sourceFilePath) => sourceFilePath + _META_EXTENSION;
public static string GetSourcePath(string metaPath) => metaPath[..^_META_EXTENSION.Length];
public static string GetMetaPath(string sourceFilePath)
{
return sourceFilePath + META_EXTENSION;
}
public static string GetSourcePath(string metaPath)
{
return metaPath[..^META_EXTENSION.Length];
}
}

View File

@@ -1,38 +0,0 @@
using Ghost.Editor.Core.Contracts;
using Ghost.Engine.AssetLoader;
namespace Ghost.Editor.Core.AssetHandler;
[AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetProcesserAttribute<T> : Attribute
{
public Type Type => typeof(T);
}
public readonly struct AssetProcesserContext
{
public IAssetRegistry Registry
{
get; init;
}
public string AssetPath
{
get; init;
}
public Asset Asset
{
get; init;
}
public IAssetHandler Handler
{
get; init;
}
}
public interface IAssetProcesser
{
ValueTask ProcessAsync(AssetProcesserContext ctx);
}

View File

@@ -1,7 +1,10 @@
using Ghost.Core;
using Ghost.Engine;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics.RHI;
using ImageMagick;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.AssetHandler;
@@ -46,6 +49,59 @@ public enum MipmapFilter : uint
MitchellNetravali
}
[Guid(GUID)]
public class TextureAsset : IAsset
{
public const string GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC";
private static readonly Guid s_typeID = Guid.Parse(GUID);
private readonly Guid _id;
private readonly IAssetSettings _settings;
private readonly MagickImage _textureData;
private readonly uint _width;
private readonly uint _height;
private readonly uint _depth;
private readonly uint _colorComponents;
private readonly uint _dimension;
public Guid ID => _id;
public Guid TypeID => s_typeID;
public IAssetSettings Settings => _settings;
public MagickImage TextureData => _textureData;
public uint Width => _width;
public uint Height => _height;
public uint Depth => _depth;
public uint Dimension => _dimension;
public uint ColorComponents => _colorComponents;
internal TextureAsset([OwnershipTransfer] MagickImage data, TextureContentHeader header, Guid id, IAssetSettings settings)
{
_id = id;
_settings = settings;
_textureData = data;
_width = header.width;
_height = header.height;
_depth = header.depth;
_dimension = header.dimension;
_colorComponents = header.colorComponents;
}
~TextureAsset()
{
Dispose();
}
public void Dispose()
{
_textureData.Dispose();
GC.SuppressFinalize(this);
}
}
public class TextureAssetSettings : IAssetSettings
{
public struct BasicSettings()
@@ -70,6 +126,11 @@ public class TextureAssetSettings : IAssetSettings
get; set;
} = 1;
public int Depth
{
get; set;
} = 1;
public bool IsSRGB
{
get; set;
@@ -193,26 +254,99 @@ public class TextureAssetSettings : IAssetSettings
} = new SamplerSettings();
}
[CustomAssetHandler(_GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)]
[Guid(_GUID)]
[CustomAssetHandler(TextureAsset.GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)]
internal class TextureAssetHandler : IAssetHandler
{
private const string _GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC";
public AssetType TargetAssetType => AssetType.Texture;
public IAssetSettings? CreateDefaultSettings()
{
return new TextureAssetSettings();
}
private static TextureDimension GetTextureDimension(TextureAssetSettings settings)
{
if (settings.Basic.Columns > 1 && settings.Basic.Rows > 1)
{
if (settings.Basic.Depth > 1)
{
return TextureDimension.Texture3D;
}
return TextureDimension.Texture2DArray;
}
if (settings.Basic.Columns == 1 && settings.Basic.Rows == 1)
{
if (settings.Basic.Depth == 6)
{
return TextureDimension.TextureCube;
}
else if (settings.Basic.Depth > 6 && settings.Basic.Depth % 6 == 0)
{
return TextureDimension.TextureCubeArray;
}
}
// If none of the above conditions are met, we will treat it as a regular 2D texture.
return TextureDimension.Texture2D;
}
public ValueTask<Result<IAsset>> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
try
{
var image = new MagickImage(assetStream);
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
var contentHeader = new TextureContentHeader
{
width = image.Width,
height = image.Height,
depth = image.Depth,
colorComponents = image.ChannelCount,
dimension = (uint)GetTextureDimension(textureSettings)
};
return ValueTask.FromResult(Result.Success<IAsset>(new TextureAsset(image, contentHeader, id, textureSettings)));
}
catch (Exception ex)
{
return ValueTask.FromResult(Result<IAsset>.Failure(ex.Message));
}
}
public async ValueTask<Result> SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default)
{
if (asset is not TextureAsset textureAsset)
{
return Result.Failure("Asset type is not TextureAsset");
}
try
{
await textureAsset.TextureData.WriteAsync(targetStream, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
try
{
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
using var image = new MagickImage(sourceStream);
var bytes = image.ToByteArray();
var pixels = image.GetPixels().GetValues();
if (pixels == null)
{
return Result.Failure("Failed to retrieve pixel data from the source image.");
}
var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.ImportsFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token)
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id, pixels, image.Width, image.Height, image.Depth, textureSettings, token)
.ConfigureAwait(false);
targetStream.Seek(0, SeekOrigin.Begin);
@@ -224,12 +358,13 @@ internal class TextureAssetHandler : IAssetHandler
depth = image.Depth,
colorComponents = image.ChannelCount,
mipLevels = (uint)mip,
dimension = (int)TextureDimension.Texture2D // TODO: Implement dimension calculation
dimension = (uint)GetTextureDimension(textureSettings)
};
targetStream.Write(MemoryMarshal.AsBytes(new Span<TextureContentHeader>(ref contentHeader)));
await targetStream.WriteAsync(bytes, token).ConfigureAwait(false);
await using var ddsStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
await ddsStream.CopyToAsync(targetStream, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
return Result.Success();

View File

@@ -1,3 +1,4 @@
using Ghost.Graphics.RHI;
using Ghost.Nvtt;
using ImageMagick;
using Misaki.HighPerformance.LowLevel;
@@ -25,7 +26,7 @@ internal static class TextureProcessor
{
private readonly string _outputPath;
private readonly byte[] _image;
private readonly float[] _image;
private readonly uint _depth;
private readonly uint _width;
private readonly uint _height;
@@ -37,7 +38,7 @@ internal static class TextureProcessor
public Task Task => _completionSource.Task;
public NvttPipelineTask(string outputPath, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings)
public NvttPipelineTask(string outputPath, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings)
{
_outputPath = outputPath;
_image = image;
@@ -163,11 +164,15 @@ internal static class TextureProcessor
}
}
public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
{
var settingsHash = ComputeSettingsHash(settings);
var cacheFileName = $"texturecache_{assetId:N}_{settingsHash:X16}.dds";
var cachePath = Path.Combine(cachesFolderPath, cacheFileName);
var textureCachePath = Path.Combine(cachesFolderPath, "TextureCache");
var cachePath = Path.Combine(textureCachePath, cacheFileName);
Directory.CreateDirectory(textureCachePath);
if (File.Exists(cachePath))
{

View File

@@ -1,5 +1,5 @@
using Ghost.Core;
using Ghost.Editor.Core.Services;
using Ghost.Editor.Core.AssetHandler;
using Ghost.Engine.AssetLoader;
namespace Ghost.Editor.Core.Contracts;
@@ -38,7 +38,6 @@ public sealed class AssetChangedEventArgs : EventArgs
}
}
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(AssetRegistry))]
public interface IAssetRegistry : IDisposable
{
string? GetAssetPath(Guid id);
@@ -46,6 +45,12 @@ public interface IAssetRegistry : IDisposable
ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default);
ValueTask<Result> ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default);
ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default);
ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default);
ValueTask<Result<IAsset>> LoadAssetAsync(Guid id, CancellationToken token = default);
ValueTask<Result> SaveAssetAsync(IAsset asset, CancellationToken token = default);
ValueTask<Result> SaveAssetAsync(Guid id, CancellationToken token = default);
void SetAssetDirty(Guid id);
ValueTask<Result> SaveAssetIfDirtyAsync(IAsset asset, CancellationToken token = default);
ValueTask<Result> SaveAssetIfDirtyAsync(Guid id, CancellationToken token = default);
ValueTask<Result[]> SaveDirtyAssetsAsync();
}

View File

@@ -1,4 +1,3 @@
using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
@@ -7,17 +6,26 @@ namespace Ghost.Editor.Core;
public static class EditorApplication
{
public const string ASSETS_FOLDER_NAME = "Assets";
public const string SOURCES_FOLDER_NAME = "Sources";
public const string PACKAGES_FOLDER_NAME = "Packages";
public const string LIBRARY_FOLDER_NAME = "Library";
public const string CACHE_FOLDER_NAME = "Cache";
public const string CONFIG_FOLDER_NAME = "Config";
public const string IMPORTS_FOLDER_NAME = "Imports";
private static IServiceProvider? s_serviceProvider;
private static string s_currentProjectPath = string.Empty;
private static string s_currentProjectName = string.Empty;
private static string s_assetsFolderPath = string.Empty;
private static string s_packagesFolderPath = string.Empty;
private static string s_libraryFolderPath = string.Empty;
private static string s_cacheFolderPath = string.Empty;
private static string s_configFolderPath = string.Empty;
private static string s_libraryImportsFolderPath = string.Empty;
private static DispatcherQueue? s_dispatcherQueue;
internal static Application CurrentApplication => Application.Current;
@@ -25,13 +33,12 @@ public static class EditorApplication
public static string ProjectPath => s_currentProjectPath;
public static string ProjectName => s_currentProjectName;
public static readonly string AssetsFolderPath = Path.Combine(ProjectPath, ASSETS_FOLDER_NAME);
public static readonly string SourcesFolderPath = Path.Combine(ProjectPath, SOURCES_FOLDER_NAME);
public static readonly string PackagesFolderPath = Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME);
public static readonly string LibraryFolderPath = Path.Combine(ProjectPath, LIBRARY_FOLDER_NAME);
public static readonly string ConfigFolderPath = Path.Combine(ProjectPath, CONFIG_FOLDER_NAME);
public static readonly string ImportsFolderPath = Path.Combine(LibraryFolderPath, IMPORTS_FOLDER_NAME);
public static string AssetsFolderPath => s_assetsFolderPath;
public static string PackagesFolderPath => s_packagesFolderPath;
public static string LibraryFolderPath => s_libraryFolderPath;
public static string ConfigFolderPath => s_configFolderPath;
public static string CacheFolderPath => s_cacheFolderPath;
public static string LibraryImportsFolderPath => s_libraryImportsFolderPath;
public static DispatcherQueue DispatcherQueue
{
@@ -51,6 +58,22 @@ public static class EditorApplication
s_serviceProvider = serviceProvider;
s_currentProjectPath = projectPath;
s_currentProjectName = projectName;
s_assetsFolderPath = Path.Combine(projectPath, ASSETS_FOLDER_NAME);
s_packagesFolderPath = Path.Combine(projectPath, PACKAGES_FOLDER_NAME);
s_libraryFolderPath = Path.Combine(projectPath, LIBRARY_FOLDER_NAME);
s_configFolderPath = Path.Combine(projectPath, CONFIG_FOLDER_NAME);
s_cacheFolderPath = Path.Combine(projectPath, CACHE_FOLDER_NAME);
s_libraryImportsFolderPath = Path.Combine(s_libraryFolderPath, IMPORTS_FOLDER_NAME);
Directory.CreateDirectory(s_assetsFolderPath);
Directory.CreateDirectory(s_packagesFolderPath);
Directory.CreateDirectory(s_libraryFolderPath);
Directory.CreateDirectory(s_configFolderPath);
Directory.CreateDirectory(s_cacheFolderPath);
Directory.CreateDirectory(s_libraryImportsFolderPath);
}
internal static void SetDispatcherQueue(DispatcherQueue dispatcherQueue)

View File

@@ -1,4 +1,5 @@
using Ghost.Editor.Core.AssetHandler;
using Ghost.Engine;
using Microsoft.Data.Sqlite;
namespace Ghost.Editor.Core.Services;
@@ -10,21 +11,18 @@ namespace Ghost.Editor.Core.Services;
internal sealed class AssetCatalog : IDisposable
{
private readonly SqliteConnection _connection;
private readonly object _writeLock = new();
private readonly Lock _writeLock = new();
// Prepared statements
private readonly SqliteCommand _cmdGetGuid;
private readonly SqliteCommand _cmdGetPath;
private readonly SqliteCommand _cmdUpsert;
private readonly SqliteCommand _cmdDelete;
private readonly SqliteCommand _cmdMarkDirty;
private readonly SqliteCommand _cmdMarkImported;
private readonly SqliteCommand _cmdMarkFailed;
private readonly SqliteCommand _cmdGetHandlerTypeId;
private readonly SqliteCommand _cmdGetReferencers;
private readonly SqliteCommand _cmdGetDependencies;
private readonly SqliteCommand _cmdInsertDep;
private readonly SqliteCommand _cmdClearDeps;
private readonly SqliteCommand _cmdGetDirty;
private readonly SqliteCommand _cmdEnumerate;
public AssetCatalog(string dbPath)
@@ -50,6 +48,7 @@ internal sealed class AssetCatalog : IDisposable
_cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path");
_cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid");
_cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id FROM assets WHERE guid = @guid");
_cmdUpsert = CreateCommand(@"
INSERT INTO assets (guid, source_path, handler_type_id, handler_version, state)
VALUES (@guid, @path, @handler_id, @version, 0)
@@ -59,21 +58,10 @@ internal sealed class AssetCatalog : IDisposable
handler_version = excluded.handler_version,
state = 0;");
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
_cmdMarkDirty = CreateCommand("UPDATE assets SET state = 0 WHERE guid = @guid");
_cmdMarkImported = CreateCommand(@"
UPDATE assets SET
content_hash = @content_hash,
settings_hash = @settings_hash,
imported_at_ms = @time,
state = 1,
error_message = NULL
WHERE guid = @guid");
_cmdMarkFailed = CreateCommand("UPDATE assets SET state = 2, error_message = @msg WHERE guid = @guid");
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid");
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
_cmdGetDirty = CreateCommand("SELECT guid, source_path FROM assets WHERE state = 0");
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
}
@@ -96,8 +84,6 @@ internal sealed class AssetCatalog : IDisposable
content_hash TEXT,
settings_hash TEXT,
imported_at_ms INTEGER,
state INTEGER NOT NULL DEFAULT 0,
error_message TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_assets_path ON assets(source_path);
@@ -155,38 +141,12 @@ internal sealed class AssetCatalog : IDisposable
}
}
public void MarkDirty(Guid guid)
public Guid GetHandlerTypeId(Guid guid)
{
lock (_writeLock)
{
_cmdMarkDirty.Parameters.Clear();
_cmdMarkDirty.Parameters.AddWithValue("@guid", guid.ToByteArray());
_cmdMarkDirty.ExecuteNonQuery();
}
}
public void MarkImported(Guid guid, string contentHash, string settingsHash)
{
lock (_writeLock)
{
_cmdMarkImported.Parameters.Clear();
_cmdMarkImported.Parameters.AddWithValue("@guid", guid.ToByteArray());
_cmdMarkImported.Parameters.AddWithValue("@content_hash", contentHash);
_cmdMarkImported.Parameters.AddWithValue("@settings_hash", settingsHash);
_cmdMarkImported.Parameters.AddWithValue("@time", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
_cmdMarkImported.ExecuteNonQuery();
}
}
public void MarkFailed(Guid guid, string error)
{
lock (_writeLock)
{
_cmdMarkFailed.Parameters.Clear();
_cmdMarkFailed.Parameters.AddWithValue("@guid", guid.ToByteArray());
_cmdMarkFailed.Parameters.AddWithValue("@msg", error);
_cmdMarkFailed.ExecuteNonQuery();
}
_cmdGetHandlerTypeId.Parameters.Clear();
_cmdGetHandlerTypeId.Parameters.AddWithValue("@guid", guid.ToByteArray());
var result = _cmdGetHandlerTypeId.ExecuteScalar();
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
}
public void SetDependencies(Guid assetId, ReadOnlySpan<Guid> dependencies)
@@ -207,6 +167,7 @@ internal sealed class AssetCatalog : IDisposable
_cmdInsertDep.Parameters.AddWithValue("@to", dep.ToByteArray());
_cmdInsertDep.ExecuteNonQuery();
}
tx.Commit();
}
}
@@ -215,12 +176,14 @@ internal sealed class AssetCatalog : IDisposable
{
_cmdGetReferencers.Parameters.Clear();
_cmdGetReferencers.Parameters.AddWithValue("@guid", guid.ToByteArray());
using var reader = _cmdGetReferencers.ExecuteReader();
var list = new List<Guid>();
while (reader.Read())
{
list.Add(new Guid((byte[])reader[0]));
}
return list;
}
@@ -228,23 +191,14 @@ internal sealed class AssetCatalog : IDisposable
{
_cmdGetDependencies.Parameters.Clear();
_cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray());
using var reader = _cmdGetDependencies.ExecuteReader();
var list = new List<Guid>();
while (reader.Read())
{
list.Add(new Guid((byte[])reader[0]));
}
return list;
}
public List<(Guid guid, string sourcePath)> GetDirtyAssets()
{
using var reader = _cmdGetDirty.ExecuteReader();
var list = new List<(Guid guid, string sourcePath)>();
while (reader.Read())
{
list.Add((new Guid((byte[])reader[0]), reader.GetString(1)));
}
return list;
}
@@ -263,14 +217,11 @@ internal sealed class AssetCatalog : IDisposable
_cmdGetPath.Dispose();
_cmdUpsert.Dispose();
_cmdDelete.Dispose();
_cmdMarkDirty.Dispose();
_cmdMarkImported.Dispose();
_cmdMarkFailed.Dispose();
_cmdGetHandlerTypeId.Dispose();
_cmdGetReferencers.Dispose();
_cmdGetDependencies.Dispose();
_cmdInsertDep.Dispose();
_cmdClearDeps.Dispose();
_cmdGetDirty.Dispose();
_cmdEnumerate.Dispose();
_connection.Dispose();
}

View File

@@ -1,7 +1,7 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Contracts;
using Ghost.Engine.AssetLoader;
using System.Collections.Concurrent;
namespace Ghost.Editor.Core.Services;
@@ -11,37 +11,34 @@ namespace Ghost.Editor.Core.Services;
/// </summary>
internal sealed class AssetRegistry : IAssetRegistry, IDisposable
{
private readonly string _assetsRoot;
private readonly string _libraryRoot;
private readonly AssetCatalog _catalog;
private readonly ImportCoordinator _importCoordinator;
private readonly FileSystemWatcher _watcher;
private readonly ConcurrentDictionary<Guid, WeakReference<Asset>> _loadedAssets;
private readonly SemaphoreSlim _loadLock = new(1, 1);
private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<Guid, WeakReference<IAsset>> _loadedAssets;
private readonly SemaphoreSlim _loadLock;
private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites;
private readonly ConcurrentHashSet<Guid> _dirtyAssets;
public event EventHandler<IAssetRegistry, AssetChangedEventArgs>? OnAssetChanged;
public AssetRegistry(string assetsRoot)
public AssetRegistry()
{
_assetsRoot = Path.GetFullPath(assetsRoot);
_libraryRoot = Path.Combine(Path.GetDirectoryName(_assetsRoot)!, EditorApplication.LIBRARY_FOLDER_NAME);
// TODO: This should be handled by EditorApplication.
Directory.CreateDirectory(_assetsRoot);
Directory.CreateDirectory(_libraryRoot);
var dbPath = Path.Combine(_libraryRoot, "AssetDB.sqlite");
var dbPath = Path.Combine(EditorApplication.LibraryFolderPath, "AssetDB.sqlite");
_catalog = new AssetCatalog(dbPath);
_importCoordinator = new ImportCoordinator(_catalog, _assetsRoot, _libraryRoot);
_importCoordinator = new ImportCoordinator(_catalog);
_loadedAssets = new ConcurrentDictionary<Guid, WeakReference<Asset>>();
_loadedAssets = new ConcurrentDictionary<Guid, WeakReference<IAsset>>();
_loadLock = new SemaphoreSlim(1, 1);
_ignoreMetaWrites = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
_dirtyAssets = new ConcurrentHashSet<Guid>();
SyncCatalogWithDisk();
_watcher = new FileSystemWatcher(_assetsRoot)
_watcher = new FileSystemWatcher(EditorApplication.AssetsFolderPath)
{
IncludeSubdirectories = true,
EnableRaisingEvents = true,
@@ -52,18 +49,16 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
_watcher.Deleted += OnFileSystemEvent;
_watcher.Changed += OnFileSystemEvent;
_watcher.Renamed += OnFileSystemRenameEvent;
_importCoordinator.EnqueueDirtyAssetsAsync().AsTask().Wait();
}
private void SyncCatalogWithDisk()
{
if (!Directory.Exists(_assetsRoot))
if (!Directory.Exists(EditorApplication.AssetsFolderPath))
{
return;
}
var metaFiles = Directory.EnumerateFiles(_assetsRoot, "*.gmeta", SearchOption.AllDirectories);
var metaFiles = Directory.EnumerateFiles(EditorApplication.AssetsFolderPath, "*.gmeta", SearchOption.AllDirectories);
var foundGuids = new HashSet<Guid>();
foreach (var metaPath in metaFiles)
@@ -71,8 +66,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result;
if (meta != null)
{
var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(_assetsRoot, metaPath));
_catalog.Upsert(meta, sourceRelative.Replace('\\', '/'));
var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.AssetsFolderPath, metaPath));
_catalog.Upsert(meta, sourceRelative.Replace(Path.DirectorySeparatorChar, '/'));
foundGuids.Add(meta.Guid);
}
}
@@ -89,7 +84,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
private async void OnFileSystemEvent(object sender, FileSystemEventArgs e)
{
var ext = Path.GetExtension(e.FullPath);
var relativePath = Path.GetRelativePath(_assetsRoot, e.FullPath).Replace('\\', '/');
var relativePath = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath).Replace(Path.DirectorySeparatorChar, '/');
if (_ignoreMetaWrites.TryRemove(e.FullPath, out _))
{
@@ -101,7 +96,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
return;
}
if (ext == ".gmeta")
if (ext == AssetMetaIO.META_EXTENSION)
{
if (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created)
{
@@ -131,8 +126,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e)
{
var oldRelative = Path.GetRelativePath(_assetsRoot, e.OldFullPath).Replace('\\', '/');
var newRelative = Path.GetRelativePath(_assetsRoot, e.FullPath).Replace('\\', '/');
var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath).Replace(Path.DirectorySeparatorChar, '/');
var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath).Replace(Path.DirectorySeparatorChar, '/');
var guid = _catalog.GetGuid(oldRelative);
if (guid != Guid.Empty)
@@ -200,7 +195,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
var ext = Path.GetExtension(sourceFilePath);
var relativePath = targetAssetPath.Replace(Path.DirectorySeparatorChar, '/');
var fullPath = Path.Combine(_assetsRoot, relativePath);
var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
File.Copy(sourceFilePath, fullPath, true);
@@ -220,18 +215,18 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
return Result.Failure("Asset not found");
}
var fullPath = Path.Combine(_assetsRoot, path);
var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, path);
var metaPath = AssetMetaIO.GetMetaPath(fullPath);
await _importCoordinator.EnqueueAsync(new ImportJob(assetId, path, metaPath, ImportReason.ManualReimport), token);
return Result.Success();
}
public async ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default)
public async ValueTask<Result<IAsset>> LoadAssetAsync(Guid id, CancellationToken token = default)
{
if (_loadedAssets.TryGetValue(id, out var weakRef) && weakRef.TryGetTarget(out var asset))
{
return asset;
return Result.Success(asset);
}
await _loadLock.WaitAsync(token);
@@ -239,24 +234,33 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
{
if (_loadedAssets.TryGetValue(id, out weakRef) && weakRef.TryGetTarget(out asset))
{
return asset;
return Result.Success(asset);
}
var importedPath = Path.Combine(_libraryRoot, "Imports", $"{id:N}.imported");
if (!File.Exists(importedPath))
var path = GetAssetPath(id);
if (!File.Exists(path))
{
return Result.Failure<Asset>("Asset not imported");
return Result.Failure("Asset does not exist.");
}
// For now, we use a basic LoadAsync implementation.
// In a better design, we'd read the handler ID from the header.
// Here we we assume the catalog is correct (it's synced with gmeta).
var handler = AssetHandlerRegistry.GetByExtension(Path.GetExtension(path));
if (handler is null)
{
return Result.Failure("No Available handler type.");
}
// Looking up TypeId from catalog isn't implemented in AssetCatalog yet.
// We should add it or use the header.
// The existing Asset class might still be tied to the old binary format.
var meta = await AssetMetaIO.ReadAsync(AssetMetaIO.GetMetaPath(path), token);
if (meta is null)
{
return Result.Failure("Meta file does not exist.");
}
return Result.Failure<Asset>("Full asset loading would require updating all assets to the new format first.");
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
return await handler.LoadAssetAsync(stream, id, meta.Settings, token);
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
finally
{
@@ -264,9 +268,88 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
}
}
public ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default)
public async ValueTask<Result> SaveAssetAsync(IAsset asset, CancellationToken token = default)
{
throw new NotImplementedException();
try
{
var path = GetAssetPath(asset.ID);
if (!File.Exists(path))
{
return Result.Failure("Asset does not exist.");
}
var handler = AssetHandlerRegistry.GetByTypeId(asset.TypeID);
if (handler is null)
{
return Result.Failure("No Avaliable handler type.");
}
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None);
return await handler.SaveAssetAsync(stream, asset, token);
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
public async ValueTask<Result> SaveAssetAsync(Guid id, CancellationToken token = default)
{
var result = await LoadAssetAsync(id, token);
if (result.IsFailure)
{
return result;
}
return await SaveAssetAsync(result.Value, token);
}
public void SetAssetDirty(Guid id)
{
_dirtyAssets.Add(id);
}
public async ValueTask<Result> SaveAssetIfDirtyAsync(IAsset asset, CancellationToken token = default)
{
if (_dirtyAssets.Contains(asset.ID))
{
var result = await SaveAssetAsync(asset, token);
_dirtyAssets.Remove(asset.ID);
return result;
}
return Result.Success();
}
public async ValueTask<Result> SaveAssetIfDirtyAsync(Guid id, CancellationToken token = default)
{
var result = await LoadAssetAsync(id, token);
if (result.IsFailure)
{
return result;
}
return await SaveAssetIfDirtyAsync(result.Value, token);
}
public async ValueTask<Result[]> SaveDirtyAssetsAsync()
{
if (_dirtyAssets.IsEmpty)
{
return Array.Empty<Result>();
}
var tasks = new Task<Result>[_dirtyAssets.Count];
var i = 0;
foreach (var id in _dirtyAssets)
{
tasks[i++] = SaveAssetIfDirtyAsync(id).AsTask();
}
_dirtyAssets.Clear();
return await Task.WhenAll(tasks);
}
public void Dispose()

View File

@@ -0,0 +1,43 @@
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
using Ghost.Engine;
namespace Ghost.Editor.Core.Services;
internal class EditorContentProvider : IContentProvider
{
private readonly AssetCatalog _catalog;
public EditorContentProvider(AssetCatalog catalog)
{
_catalog = catalog;
}
public bool HasAsset(Guid guid)
{
return _catalog.GetSourcePath(guid) != null;
}
public Result<Stream> OpenRead(Guid guid, CancellationToken token = default)
{
var importedPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{guid:N}{ImportCoordinator.IMPORTED_EXTENSION}");
if (!File.Exists(importedPath))
{
return Result.Failure($"Imported asset not found for GUID: {guid}");
}
return new FileStream(importedPath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
public Guid[] GetDependencies(Guid guid)
{
return _catalog.GetDependencies(guid).ToArray();
}
public AssetType GetAssetType(Guid guid)
{
var handlerID = _catalog.GetHandlerTypeId(guid);
var handler = AssetHandlerRegistry.GetByTypeId(handlerID);
return handler?.TargetAssetType ?? AssetType.Unknown;
}
}

View File

@@ -25,10 +25,11 @@ internal readonly record struct ImportJob(
internal sealed class ImportCoordinator : IDisposable
{
public const string IMPORTED_EXTENSION_NAME = "Imported";
public const string IMPORTED_EXTENSION = ".imported";
private readonly Channel<ImportJob> _importChannel;
private readonly AssetCatalog _catalog;
private readonly string _assetsRoot;
private readonly string _libraryRoot;
private readonly CancellationTokenSource _cts;
private readonly Task[] _workers;
@@ -36,11 +37,9 @@ internal sealed class ImportCoordinator : IDisposable
// For now we just focus on the core logic
// public event EventHandler<AssetChangedEventArgs>? OnAssetChanged;
public ImportCoordinator(AssetCatalog catalog, string assetsRoot, string libraryRoot, int workerCount = 2)
public ImportCoordinator(AssetCatalog catalog, int workerCount = 2)
{
_catalog = catalog;
_assetsRoot = assetsRoot;
_libraryRoot = libraryRoot;
_cts = new CancellationTokenSource();
_importChannel = Channel.CreateBounded<ImportJob>(new BoundedChannelOptions(256)
@@ -61,15 +60,6 @@ internal sealed class ImportCoordinator : IDisposable
return _importChannel.Writer.WriteAsync(job, token);
}
public async ValueTask EnqueueDirtyAssetsAsync(CancellationToken token = default)
{
foreach (var (guid, sourcePath) in _catalog.GetDirtyAssets())
{
var metaPath = AssetMetaIO.GetMetaPath(Path.Combine(_assetsRoot, sourcePath));
await EnqueueAsync(new ImportJob(guid, sourcePath, metaPath, ImportReason.Startup), token);
}
}
private async Task WorkerLoop(CancellationToken token)
{
await foreach (var job in _importChannel.Reader.ReadAllAsync(token))
@@ -80,18 +70,18 @@ internal sealed class ImportCoordinator : IDisposable
}
catch (Exception ex)
{
_catalog.MarkFailed(job.AssetGuid, ex.Message);
Logger.Error(ex);
}
}
}
private async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token)
{
var fullSourcePath = Path.Combine(_assetsRoot, job.SourcePath);
var fullSourcePath = Path.Combine(EditorApplication.AssetsFolderPath, job.SourcePath);
var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token);
if (meta is null)
{
_catalog.MarkFailed(job.AssetGuid, "Missing .gmeta file");
Logger.Error("Missing .gmeta file");
return;
}
@@ -108,17 +98,13 @@ internal sealed class ImportCoordinator : IDisposable
meta.SettingsHash == settingsHash &&
meta.HandlerVersion == AssetHandlerRegistry.GetVersionByTypeId(meta.HandlerTypeId ?? Guid.Empty))
{
_catalog.MarkImported(job.AssetGuid, contentHash, settingsHash);
return;
}
var importResult = Result.Success();
if (handler is IAssetHandler importable)
{
// TODO: This should be handled by EditorApplication.
var importsDir = Path.Combine(_libraryRoot, "Imports");
Directory.CreateDirectory(importsDir);
var targetPath = Path.Combine(importsDir, $"{job.AssetGuid:N}.imported");
var targetPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{job.AssetGuid:N}{IMPORTED_EXTENSION}");
await using var sourceStream = new FileStream(fullSourcePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
@@ -134,11 +120,10 @@ internal sealed class ImportCoordinator : IDisposable
meta.LastImportedUtc = DateTime.UtcNow;
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
_catalog.MarkImported(job.AssetGuid, contentHash, settingsHash);
}
else
{
_catalog.MarkFailed(job.AssetGuid, importResult.Message ?? "Unknown import error");
Logger.Error(importResult.Message ?? "Unknown import error");
}
}
@@ -146,7 +131,7 @@ internal sealed class ImportCoordinator : IDisposable
{
if (!File.Exists(filePath))
{
return "";
return string.Empty;
}
using var sha = SHA256.Create();
@@ -159,7 +144,7 @@ internal sealed class ImportCoordinator : IDisposable
{
if (settings is null)
{
return "";
return string.Empty;
}
var json = JsonSerializer.SerializeToUtf8Bytes(settings);

View File

@@ -10,7 +10,6 @@
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/Themes/Generic.xaml" />
<!--<ResourceDictionary Source="/Themes/DockingDictionary.xaml" />-->
<core:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -3,11 +3,9 @@ using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services;
using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Views.Pages.EngineEditor;
using Ghost.Editor.Views.Windows;
using Ghost.Editor.ViewModels.Controls;
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Ghost.Editor.ViewModels.Windows;
using Ghost.Editor.Views.Windows;
using Ghost.Engine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -66,7 +64,7 @@ public partial class App : Application
services.AddSingleton<IProgressService, ProgressService>();
services.AddSingleton<IInspectorService, InspectorService>();
services.AddSingleton<IPreviewService, PreviewService>();
// services.AddSingleton<IAssetService, AssetService>();
services.AddSingleton<IAssetRegistry, AssetRegistry>();
services.AddSingleton<EngineEditorViewModel>();
@@ -97,22 +95,6 @@ public partial class App : Application
break;
}
}
#region Should be deleted
services.AddTransient<ScenePage>();
services.AddTransient<HierarchyPage>();
services.AddTransient<HierarchyViewModel>();
services.AddTransient<ProjectPage>();
services.AddTransient<ProjectViewModel>();
services.AddTransient<ConsolePage>();
services.AddTransient<ConsoleViewModel>();
services.AddTransient<InspectorPage>();
services.AddTransient<InspectorViewModel>();
#endregion
})
.Build();

View File

@@ -198,6 +198,8 @@
</ItemGroup>
<ItemGroup>
<Folder Include="ContextMenu\" />
<Folder Include="ViewModels\Pages\" />
<Folder Include="Views\Pages\" />
</ItemGroup>
<PropertyGroup Label="Globals" />

View File

@@ -1,53 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ghost.Core;
using System.Collections.ObjectModel;
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
internal partial class ConsoleViewModel : ObservableObject
{
public ReadOnlyObservableCollection<LogMessage> Logs;
[ObservableProperty]
public partial bool ShowInfo
{
get; set;
} = true;
[ObservableProperty]
public partial bool ShowWarning
{
get; set;
} = true;
[ObservableProperty]
public partial bool ShowError
{
get; set;
} = true;
[ObservableProperty]
public partial bool ShowStackTrace
{
get; set;
} = false;
[ObservableProperty]
public partial LogMessage? SelectedLog
{
get; set;
}
partial void OnShowStackTraceChanged(bool value)
{
//Logger.HasStackTrace = value;
//Logger.LogInfo($"Stack trace visibility set to {value}.");
}
[RelayCommand]
private void ClearLogs()
{
//Logger.Clear();
}
}

View File

@@ -1,36 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
internal partial class HierarchyViewModel : ObservableObject, INavigationAware
{
//[ObservableProperty]
//public partial ObservableCollection<SceneNode> SceneList
//{
// get;
// private set;
//} = new(EditorSceneManager.LoadedWorlds);
//private void OnWorldLoaded(SceneNode node)
//{
// SceneList.Add(node);
//}
//private void OnWorldUnloaded(SceneNode node)
//{
// SceneList.Remove(node);
//}
public void OnNavigatedTo(object? parameter)
{
//EditorSceneManager.OnWorldLoaded += OnWorldLoaded;
//EditorSceneManager.OnWorldUnloaded += OnWorldUnloaded;
}
public void OnNavigatedFrom()
{
//EditorSceneManager.OnWorldLoaded -= OnWorldLoaded;
//EditorSceneManager.OnWorldUnloaded -= OnWorldUnloaded;
}
}

View File

@@ -1,31 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
internal partial class InspectorViewModel(IInspectorService inspectorService) : ObservableObject, INavigationAware
{
[ObservableProperty]
public partial IInspectable? Inspectable
{
get;
set;
}
public void OnNavigatedTo(object? parameter)
{
inspectorService.OnSelectionChanged += OnSelectionChanged;
Inspectable = inspectorService.Selected;
}
public void OnNavigatedFrom()
{
inspectorService.OnSelectionChanged -= OnSelectionChanged;
Inspectable = null;
}
private void OnSelectionChanged(object? sender, EventArgs e)
{
Inspectable = inspectorService.Selected;
}
}

View File

@@ -1,143 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Models;
using System.Collections.ObjectModel;
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
internal partial class ProjectViewModel : ObservableObject
{
// private readonly IAssetService _assetService;
public ObservableCollection<ExplorerItem> SubDirectories
{
get;
} = new();
[ObservableProperty]
public partial ObservableCollection<ExplorerItem> DirectoryAssets
{
get;
set;
} = new();
[ObservableProperty]
public partial ExplorerItem? SelectedDirectory
{
get;
set;
}
[ObservableProperty]
public partial ExplorerItem? SelectedAsset
{
get;
set;
}
// public ProjectViewModel(IAssetService assetService)
// {
// _assetService = assetService;
// var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
// LoadSubFolderRecursive(ref assetsRootItem);
// SubDirectories.Add(assetsRootItem);
// }
private static void LoadSubFolderRecursive(ref ExplorerItem parentItem)
{
foreach (var directory in Directory.EnumerateDirectories(parentItem.FullName))
{
var item = new ExplorerItem(Path.GetFileName(directory), directory, true);
LoadSubFolderRecursive(ref item);
parentItem.Children ??= new();
parentItem.Children.Add(item);
}
}
public static Task<ExplorerItem?> FindNodeIterative(ExplorerItem root, Func<ExplorerItem, bool> predicate)
{
var stack = new Stack<ExplorerItem>();
stack.Push(root);
return Task.Run(() =>
{
while (stack.Count > 0)
{
var node = stack.Pop();
if (predicate(node))
{
return node;
}
if (node.Children == null || node.Children.Count == 0)
{
continue;
}
for (var i = node.Children.Count - 1; i >= 0; i--)
{
stack.Push(node.Children[i]);
}
}
return null;
});
}
private void NavigateToDirectory(string? path)
{
App.Window?.DispatcherQueue.TryEnqueue(async () =>
{
DirectoryAssets.Clear();
if (!Directory.Exists(path))
{
return;
}
foreach (var directory in Directory.EnumerateDirectories(path))
{
var directoryItem = new ExplorerItem(Path.GetFileName(directory), directory, true);
DirectoryAssets.Add(directoryItem);
}
foreach (var file in Directory.EnumerateFiles(path))
{
var fileItem = new ExplorerItem(Path.GetFileName(file), file, false);
DirectoryAssets.Add(fileItem);
}
SelectedDirectory = await FindNodeIterative(SubDirectories[0], x => x.FullName == path);
});
}
public void OpenSelected()
{
if (SelectedAsset == null)
{
return;
}
if (SelectedAsset.IsDirectory)
{
NavigateToDirectory(SelectedAsset.FullName);
}
else
{
// _assetService.OpenAsset(SelectedAsset.FullName);
}
}
partial void OnSelectedDirectoryChanged(ExplorerItem? value)
{
if (value == null)
{
return;
}
DirectoryAssets.Clear();
NavigateToDirectory(value.FullName);
}
}

View File

@@ -2,7 +2,7 @@ using Ghost.Editor.Core;
namespace Ghost.Editor.Views.Controls;
internal partial class ProjectBrowser
internal partial class ContentBrowser
{
[ContextMenuItem("project-browser", "Show in Explorer")]
private static void ShowInExplorer()

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Ghost.Editor.Views.Controls.ProjectBrowser"
x:Class="Ghost.Editor.Views.Controls.ContentBrowser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:community="using:CommunityToolkit.WinUI.Controls"

View File

@@ -8,9 +8,9 @@ using Microsoft.UI.Xaml.Input;
namespace Ghost.Editor.Views.Controls;
internal sealed partial class ProjectBrowser : UserControl
internal sealed partial class ContentBrowser : UserControl
{
public static ProjectBrowser? LastFocused
public static ContentBrowser? LastFocused
{
get;
private set;
@@ -24,7 +24,7 @@ internal sealed partial class ProjectBrowser : UserControl
get;
}
public ProjectBrowser()
public ContentBrowser()
{
_inspectorService = App.GetService<IInspectorService>();
ViewModel = App.GetService<ProjectBrowserViewModel>();

View File

@@ -1,211 +0,0 @@
using System;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Base class for containers that can hold other dock modules.
/// </summary>
public abstract class DockContainer : DockModule
{
private readonly ObservableCollection<DockModule> _children = new();
private bool _isCleaningUp;
/// <summary>
/// Gets the collection of child modules.
/// </summary>
public ReadOnlyObservableCollection<DockModule> Children { get; }
protected DockContainer()
{
Children = new ReadOnlyObservableCollection<DockModule>(_children);
_children.CollectionChanged += OnChildrenChanged;
}
private void OnChildrenChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnChildrenUpdated();
}
/// <summary>
/// Adds a child module to the end of the container.
/// </summary>
/// <param name="module">The module to add.</param>
public virtual void AddChild(DockModule module)
{
InsertChild(_children.Count, module);
}
/// <summary>
/// Inserts a child module at the specified index.
/// </summary>
/// <remarks>
/// This method does not support reordering existing children within the same container.
/// Cross-layout moves are intentionally allowed and supported (e.g., for dragging tabs between floating windows and the main window).
/// </remarks>
/// <param name="index">The zero-based index at which the module should be inserted.</param>
/// <param name="module">The module to insert.</param>
public virtual void InsertChild(int index, DockModule module)
{
ValidateChild(module);
if (module.Owner == null && module.Root != null && module.Root != this.Root)
throw new InvalidOperationException("Cannot insert a module that is the root of another layout. Detach it first.");
if (index < 0 || index > _children.Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (_children.Contains(module))
return;
module.Owner?.RemoveChild(module);
module.Owner = this;
module.Root = Root;
_children.Insert(index, module);
}
/// <summary>
/// Removes a child module from the container.
/// </summary>
/// <param name="module">The module to remove.</param>
public virtual void RemoveChild(DockModule module)
{
RemoveChildInternal(module, true);
}
internal void RemoveChildInternal(DockModule module, bool triggerCleanup)
{
ArgumentNullException.ThrowIfNull(module);
if (_children.Remove(module))
{
module.Owner = null;
module.Root = null;
if (!_isCleaningUp && triggerCleanup)
{
CheckCleanup();
}
}
}
/// <summary>
/// Replaces an existing child module with a new one.
/// </summary>
/// <remarks>
/// Cross-layout moves are intentionally allowed and supported (e.g., for dragging tabs between floating windows and the main window).
/// </remarks>
/// <param name="oldChild">The child module to be replaced.</param>
/// <param name="newChild">The new child module to insert.</param>
public virtual void ReplaceChild(DockModule oldChild, DockModule newChild)
{
ArgumentNullException.ThrowIfNull(oldChild);
ValidateChild(newChild);
if (newChild.Owner == null && newChild.Root != null && newChild.Root != this.Root)
throw new InvalidOperationException("Cannot insert a module that is the root of another layout. Detach it first.");
if (oldChild == newChild) return;
int index = _children.IndexOf(oldChild);
if (index < 0) throw new ArgumentException("oldChild not found in this container", nameof(oldChild));
// Detach newChild from its current owner if any
if (newChild.Owner == this)
{
throw new ArgumentException("newChild is already in this container", nameof(newChild));
}
var oldOwner = newChild.Owner;
newChild.Owner?.RemoveChildInternal(newChild, false);
// Remove oldChild without triggering cleanup
_isCleaningUp = true;
try
{
_children.RemoveAt(index);
oldChild.Owner = null;
oldChild.Root = null;
newChild.Owner = this;
newChild.Root = Root;
_children.Insert(index, newChild);
}
finally
{
_isCleaningUp = false;
}
CheckCleanup();
oldOwner?.CheckCleanup();
}
/// <summary>
/// Checks if the container is empty and removes it from its owner if necessary.
/// </summary>
internal virtual void CheckCleanup()
{
if (Children.Count == 0)
{
if (Owner != null)
{
Owner.RemoveChildInternal(this, true);
}
else if (Root != null && Root.RootModule == this)
{
var root = Root;
root.RootModule = null;
root.NotifyLayoutEmpty();
}
}
}
/// <summary>
/// Validates if a module can be added as a child to this container.
/// </summary>
/// <param name="module">The module to validate.</param>
protected virtual void ValidateChild(DockModule module)
{
ArgumentNullException.ThrowIfNull(module);
if (module == this)
throw new ArgumentException("Cannot add a container to itself.", nameof(module));
if (module is DockContainer container)
{
var current = Owner;
while (current != null)
{
if (current == container)
throw new ArgumentException("Cannot add a container that is an ancestor of this container.", nameof(module));
current = current.Owner;
}
}
}
/// <summary>
/// Removes all child modules from the container.
/// </summary>
public void Clear()
{
foreach (var child in _children)
{
child.Owner = null;
child.Root = null;
}
_children.Clear();
if (!_isCleaningUp)
{
CheckCleanup();
}
}
protected override void OnRootChanged()
{
foreach (var child in _children)
{
child.Root = Root;
}
}
protected virtual void OnChildrenUpdated() { }
}

View File

@@ -1,42 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Represents a document module in the docking system.
/// </summary>
public partial class DockDocument : DockModule
{
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
nameof(Title), typeof(string), typeof(DockDocument), new PropertyMetadata(string.Empty));
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
nameof(Content), typeof(object), typeof(DockDocument), new PropertyMetadata(null));
/// <summary>
/// Gets or sets the title of the document.
/// </summary>
public string? Title
{
get => (string?)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
/// <summary>
/// Gets or sets the content of the document.
/// </summary>
public object? Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
/// <summary>
/// Initializes a new instance of the <see cref="DockDocument"/> class.
/// </summary>
public DockDocument()
{
DefaultStyleKey = typeof(DockDocument);
}
}

View File

@@ -1,22 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockDocument">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockDocument">
<Border Background="Transparent">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,182 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// A container that displays its children (documents) as tabs.
/// </summary>
[TemplatePart(Name = PART_TAB_VIEW, Type = typeof(TabView))]
public partial class DockGroup : DockContainer
{
private const string PART_TAB_VIEW = "PART_TabView";
private const string DRAG_DOCUMENT_KEY = "DockDocument";
private TabView? _tabView;
public DockGroup()
{
DefaultStyleKey = typeof(DockGroup);
}
protected override void ValidateChild(DockModule module)
{
base.ValidateChild(module);
if (module is not DockDocument)
{
throw new ArgumentException($"{nameof(DockGroup)} only accepts {nameof(DockDocument)} children.", nameof(module));
}
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_tabView != null)
{
_tabView.TabDragStarting -= OnTabDragStarting;
_tabView.TabDroppedOutside -= OnTabDroppedOutside;
_tabView.DragOver -= OnDragOver;
_tabView.Drop -= OnDrop;
_tabView.DragLeave -= OnDragLeave;
_tabView.TabCloseRequested -= OnTabCloseRequested;
}
_tabView = GetTemplateChild(PART_TAB_VIEW) as TabView;
if (_tabView != null)
{
_tabView.TabDragStarting += OnTabDragStarting;
_tabView.TabDroppedOutside += OnTabDroppedOutside;
_tabView.DragOver += OnDragOver;
_tabView.Drop += OnDrop;
_tabView.DragLeave += OnDragLeave;
_tabView.TabCloseRequested += OnTabCloseRequested;
}
UpdateTabs();
}
private void OnTabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args)
{
if (args.Tab.Tag is DockDocument doc)
{
args.Data.Properties.Add(DRAG_DOCUMENT_KEY, doc);
}
}
private void OnTabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args)
{
if (args.Tab.Tag is DockDocument doc)
{
Root?.CreateFloatingWindow(doc);
}
}
private void OnDragOver(object sender, DragEventArgs e)
{
if (e.DataView.Properties.ContainsKey(DRAG_DOCUMENT_KEY))
{
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
Root?.ShowHighlight(this, e.GetPosition(this));
}
}
private void OnDrop(object sender, DragEventArgs e)
{
if (e.DataView.Properties.TryGetValue(DRAG_DOCUMENT_KEY, out var obj) && obj is DockDocument doc)
{
Root?.HandleDrop(doc, this, e.GetPosition(this));
}
}
private void OnDragLeave(object sender, DragEventArgs e)
{
Root?.HideHighlight();
}
private void OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
{
if (args.Tab.Tag is DockDocument doc)
{
RemoveChild(doc);
}
}
protected override void OnChildrenUpdated()
{
UpdateTabs();
}
private void UpdateTabs()
{
if (_tabView == null || Root == null) return;
var selectedDoc = _tabView.SelectedItem is TabViewItem selectedItem ? selectedItem.Tag as DockDocument : null;
// Remove tabs that are no longer in Children
for (int i = _tabView.TabItems.Count - 1; i >= 0; i--)
{
if (_tabView.TabItems[i] is TabViewItem tabItem && tabItem.Tag is DockDocument doc)
{
if (!Children.Contains(doc))
{
tabItem.Content = null;
_tabView.TabItems.RemoveAt(i);
}
}
}
// Add new tabs that aren't in TabItems yet, and ensure correct order
for (int i = 0; i < Children.Count; i++)
{
if (Children[i] is DockDocument doc)
{
TabViewItem? existingTab = null;
for (int j = 0; j < _tabView.TabItems.Count; j++)
{
if (_tabView.TabItems[j] is TabViewItem tabItem && tabItem.Tag == doc)
{
existingTab = tabItem;
// Fix order if necessary
if (j != i)
{
_tabView.TabItems.RemoveAt(j);
_tabView.TabItems.Insert(i, existingTab);
}
break;
}
}
if (existingTab == null)
{
existingTab = new TabViewItem
{
Tag = doc,
Content = doc
};
existingTab.SetBinding(TabViewItem.HeaderProperty, new Binding
{
Source = doc,
Path = new PropertyPath(nameof(DockDocument.Title)),
Mode = Microsoft.UI.Xaml.Data.BindingMode.OneWay
});
_tabView.TabItems.Insert(i, existingTab);
}
}
}
// Restore selection
if (selectedDoc != null && _tabView.TabItems.FirstOrDefault(t => t is TabViewItem item && item.Tag == selectedDoc) is TabViewItem newSelected)
{
_tabView.SelectedItem = newSelected;
}
else if (_tabView.TabItems.Count > 0)
{
_tabView.SelectedItem = _tabView.TabItems[0];
}
}
}

View File

@@ -1,23 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockGroup">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockGroup">
<TabView
x:Name="PART_TabView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AllowDrop="True"
CanDragTabs="True"
CanReorderTabs="True"
IsAddTabButtonVisible="True"
TabWidthMode="Compact" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,47 +0,0 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Base class for all dockable modules in the docking system.
/// </summary>
public abstract class DockModule : Control
{
/// <summary>
/// Gets the container that owns this module.
/// </summary>
public DockContainer? Owner { get; internal set; }
/// <summary>
/// Gets or sets the proportional length (star weight) of this module within its parent panel.
/// </summary>
public double DockLength { get; set; } = 1.0;
private DockingLayout? _root;
/// <summary>
/// Gets or sets the root docking layout this module belongs to.
/// </summary>
public virtual DockingLayout? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
OnRootChanged();
}
}
}
protected virtual void OnRootChanged() { }
/// <summary>
/// Detaches this module from its current owner.
/// </summary>
public void Detach()
{
Owner?.RemoveChild(this);
}
}

View File

@@ -1,231 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using CommunityToolkit.WinUI.Controls;
using Windows.Foundation;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// A container that can host multiple dock modules with splitters.
/// </summary>
[TemplatePart(Name = PART_GRID, Type = typeof(Grid))]
public partial class DockPanel : DockContainer
{
private const string PART_GRID = "PART_Grid";
private const double SPLITTER_THICKNESS = 4;
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation), typeof(Orientation), typeof(DockPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
/// <summary>
/// Gets or sets the orientation of the panel.
/// </summary>
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
private Grid? _grid;
public DockPanel()
{
DefaultStyleKey = typeof(DockPanel);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_grid = GetTemplateChild(PART_GRID) as Grid;
UpdateLayoutStructure();
}
protected override void OnChildrenUpdated()
{
UpdateLayoutStructure();
}
internal void SyncLengths()
{
if (_grid == null) return;
if (Orientation == Orientation.Horizontal)
{
for (int i = 0; i < Children.Count; i++)
{
int col = i * 2;
if (col < _grid.ColumnDefinitions.Count)
{
Children[i].DockLength = _grid.ColumnDefinitions[col].Width.Value;
}
}
}
else
{
for (int i = 0; i < Children.Count; i++)
{
int row = i * 2;
if (row < _grid.RowDefinitions.Count)
{
Children[i].DockLength = _grid.RowDefinitions[row].Height.Value;
}
}
}
}
internal override void CheckCleanup()
{
base.CheckCleanup();
if (Children.Count == 1)
{
var child = Children[0];
var owner = Owner;
if (owner != null)
{
owner.ReplaceChild(this, child);
}
else if (Root != null && Root.RootModule == this)
{
RemoveChildInternal(child, false);
Root.RootModule = child;
}
}
}
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DockPanel)d).UpdateLayoutStructure();
}
private void UpdateLayoutStructure()
{
if (_grid == null) return;
// Remove splitters and children that are no longer in the collection
for (int i = _grid.Children.Count - 1; i >= 0; i--)
{
var child = _grid.Children[i];
if (child is GridSplitter)
{
_grid.Children.RemoveAt(i);
}
else if (child is DockModule module && !Children.Contains(module))
{
_grid.Children.RemoveAt(i);
}
}
if (Children.Count == 0)
{
_grid.RowDefinitions.Clear();
_grid.ColumnDefinitions.Clear();
return;
}
if (Orientation == Orientation.Horizontal)
{
_grid.RowDefinitions.Clear();
int requiredColumns = (Children.Count * 2) - 1;
while (_grid.ColumnDefinitions.Count > requiredColumns)
{
_grid.ColumnDefinitions.RemoveAt(_grid.ColumnDefinitions.Count - 1);
}
for (var i = 0; i < Children.Count; i++)
{
int columnIndex = i * 2;
if (columnIndex >= _grid.ColumnDefinitions.Count)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(Children[i].DockLength, GridUnitType.Star), MinWidth = 250 });
}
else
{
_grid.ColumnDefinitions[columnIndex].Width = new GridLength(Children[i].DockLength, GridUnitType.Star);
_grid.ColumnDefinitions[columnIndex].MinWidth = 250;
}
var child = Children[i];
if (!_grid.Children.Contains(child))
{
_grid.Children.Add(child);
}
Grid.SetColumn(child, columnIndex);
Grid.SetRow(child, 0);
if (i < Children.Count - 1)
{
int splitterIndex = columnIndex + 1;
if (splitterIndex >= _grid.ColumnDefinitions.Count)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
}
else
{
_grid.ColumnDefinitions[splitterIndex].Width = GridLength.Auto;
}
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Columns, Width = SPLITTER_THICKNESS };
Grid.SetColumn(splitter, splitterIndex);
Grid.SetRow(splitter, 0);
_grid.Children.Add(splitter);
}
}
}
else
{
_grid.ColumnDefinitions.Clear();
int requiredRows = (Children.Count * 2) - 1;
while (_grid.RowDefinitions.Count > requiredRows)
{
_grid.RowDefinitions.RemoveAt(_grid.RowDefinitions.Count - 1);
}
for (var i = 0; i < Children.Count; i++)
{
int rowIndex = i * 2;
if (rowIndex >= _grid.RowDefinitions.Count)
{
_grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(Children[i].DockLength, GridUnitType.Star), MinHeight = 250 });
}
else
{
_grid.RowDefinitions[rowIndex].Height = new GridLength(Children[i].DockLength, GridUnitType.Star);
_grid.RowDefinitions[rowIndex].MinHeight = 250;
}
var child = Children[i];
if (!_grid.Children.Contains(child))
{
_grid.Children.Add(child);
}
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, 0);
if (i < Children.Count - 1)
{
int splitterIndex = rowIndex + 1;
if (splitterIndex >= _grid.RowDefinitions.Count)
{
_grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
}
else
{
_grid.RowDefinitions[splitterIndex].Height = GridLength.Auto;
}
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Rows, Height = SPLITTER_THICKNESS };
Grid.SetRow(splitter, splitterIndex);
Grid.SetColumn(splitter, 0);
_grid.Children.Add(splitter);
}
}
}
UpdateLayout();
}
}

View File

@@ -1,15 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockPanel">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockPanel">
<Grid x:Name="PART_Grid" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,15 +0,0 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Represents a visual highlight for a docking region.
/// </summary>
public partial class DockRegionHighlight : Control
{
public DockRegionHighlight()
{
DefaultStyleKey = typeof(DockRegionHighlight);
IsHitTestVisible = false;
}
}

View File

@@ -1,20 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockRegionHighlight">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockRegionHighlight">
<Border
Background="{ThemeResource SystemControlHighlightAccentBrush}"
BorderBrush="{ThemeResource SystemControlHighlightAccentBrush}"
BorderThickness="2"
CornerRadius="4"
Opacity="0.25" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,344 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// The root control for the docking system layout.
/// </summary>
[TemplatePart(Name = PART_OVERLAY_CANVAS, Type = typeof(Canvas))]
[TemplatePart(Name = PART_HIGHLIGHT, Type = typeof(DockRegionHighlight))]
public partial class DockingLayout : Control
{
private const string PART_OVERLAY_CANVAS = "PART_OverlayCanvas";
private const string PART_HIGHLIGHT = "PART_Highlight";
/// <summary>
/// Gets or sets the root module of the docking layout.
/// </summary>
public static readonly DependencyProperty RootModuleProperty = DependencyProperty.Register(
nameof(RootModule), typeof(DockModule), typeof(DockingLayout), new PropertyMetadata(null, OnRootModuleChanged));
/// <summary>
/// Gets or sets the root module of the docking layout.
/// </summary>
public DockModule? RootModule
{
get => (DockModule?)GetValue(RootModuleProperty);
set => SetValue(RootModuleProperty, value);
}
/// <summary>
/// Occurs when the layout becomes empty.
/// </summary>
public event EventHandler? LayoutEmpty;
internal void NotifyLayoutEmpty() => LayoutEmpty?.Invoke(this, EventArgs.Empty);
private Canvas? _overlayCanvas;
private DockRegionHighlight? _highlight;
private readonly List<FloatingWindow> _floatingWindows = new();
/// <summary>
/// Initializes a new instance of the <see cref="DockingLayout"/> class.
/// </summary>
public DockingLayout()
{
DefaultStyleKey = typeof(DockingLayout);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_overlayCanvas = GetTemplateChild(PART_OVERLAY_CANVAS) as Canvas;
_highlight = GetTemplateChild(PART_HIGHLIGHT) as DockRegionHighlight;
}
private static void OnRootModuleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockingLayout layout)
{
if (e.OldValue is DockModule oldModule)
{
oldModule.Root = null;
}
if (e.NewValue is DockModule newModule)
{
if (newModule.Root != null && newModule.Root != layout)
{
throw new InvalidOperationException("Module is already owned by another DockingLayout");
}
if (newModule.Owner != null)
{
newModule.Owner.RemoveChild(newModule);
}
newModule.Root = layout;
}
}
}
/// <summary>
/// Adds a document to the docking layout.
/// </summary>
/// <param name="document">The document to add.</param>
/// <param name="target">The docking target position.</param>
/// <param name="targetGroup">The target group to add the document to. If null, a suitable group will be found or created.</param>
public void AddDocument(DockDocument document, DockTarget target, DockGroup? targetGroup = null)
{
ArgumentNullException.ThrowIfNull(document);
if (targetGroup != null && targetGroup.Root != this)
{
throw new ArgumentException("targetGroup does not belong to this DockingLayout", nameof(targetGroup));
}
if (targetGroup == null)
{
if (RootModule != null)
{
targetGroup = FindFirstDockGroup(RootModule as DockContainer);
if (targetGroup == null)
{
// Root is not a container, or contains no pGroups. Wrap it.
var newGroup = new DockGroup();
newGroup.AddChild(document);
if (RootModule is DockDocument existingDoc)
{
RootModule = null;
newGroup.AddChild(existingDoc);
RootModule = newGroup;
}
else
{
var oldRoot = RootModule;
RootModule = null;
var panel = new DockPanel();
panel.AddChild(oldRoot);
panel.AddChild(newGroup);
RootModule = panel;
}
targetGroup = newGroup;
}
}
else
{
targetGroup = new DockGroup();
RootModule = targetGroup;
}
}
if (target == DockTarget.Center || targetGroup.Children.Count == 0)
{
targetGroup.AddChild(document);
}
else
{
SplitGroup(targetGroup, document, target);
}
}
private void SplitGroup(DockGroup targetGroup, DockDocument doc, DockTarget target)
{
var parentPanel = targetGroup.Owner as DockPanel;
parentPanel?.SyncLengths(); // Sync before modifying!
var newGroup = new DockGroup();
newGroup.AddChild(doc);
var orientation = (target == DockTarget.Left || target == DockTarget.Right) ? Orientation.Horizontal : Orientation.Vertical;
if (parentPanel == null)
{
// targetGroup is the RootModule
var newPanel = new DockPanel { Orientation = orientation };
RootModule = newPanel;
targetGroup.DockLength = 1.0;
newGroup.DockLength = 1.0;
if (target == DockTarget.Left || target == DockTarget.Top)
{
newPanel.AddChild(newGroup);
newPanel.AddChild(targetGroup);
}
else
{
newPanel.AddChild(targetGroup);
newPanel.AddChild(newGroup);
}
return;
}
int index = parentPanel.Children.IndexOf(targetGroup);
if (index < 0)
{
throw new InvalidOperationException("targetGroup not found in parentPanel");
}
if (parentPanel.Orientation == orientation)
{
// Splitting in the same orientation. Share the length.
double halfLength = targetGroup.DockLength / 2.0;
targetGroup.DockLength = halfLength;
newGroup.DockLength = halfLength;
if (target == DockTarget.Left || target == DockTarget.Top)
{
parentPanel.InsertChild(index, newGroup);
}
else
{
parentPanel.InsertChild(index + 1, newGroup);
}
}
else
{
// Splitting in opposite orientation. New panel takes the full length.
var newPanel = new DockPanel { Orientation = orientation };
newPanel.DockLength = targetGroup.DockLength;
targetGroup.DockLength = 1.0;
newGroup.DockLength = 1.0;
parentPanel.ReplaceChild(targetGroup, newPanel);
if (target == DockTarget.Left || target == DockTarget.Top)
{
newPanel.AddChild(newGroup);
newPanel.AddChild(targetGroup);
}
else
{
newPanel.AddChild(targetGroup);
newPanel.AddChild(newGroup);
}
}
}
private static DockGroup? FindFirstDockGroup(DockContainer? container)
{
if (container == null) return null;
if (container is DockGroup group)
{
return group;
}
foreach (var child in container.Children)
{
if (child is DockContainer childContainer)
{
var result = FindFirstDockGroup(childContainer);
if (result != null)
{
return result;
}
}
}
return null;
}
internal void ShowHighlight(DockGroup targetGroup, global::Windows.Foundation.Point position)
{
if (_highlight == null || _overlayCanvas == null) return;
_highlight.Visibility = Visibility.Visible;
var target = CalculateDockTarget(targetGroup, position);
// Calculate rect based on target
double width = targetGroup.ActualWidth;
double height = targetGroup.ActualHeight;
double x = 0, y = 0;
switch (target)
{
case DockTarget.Left: width /= 2; break;
case DockTarget.Right: width /= 2; x = width; break;
case DockTarget.Top: height /= 2; break;
case DockTarget.Bottom: height /= 2; y = height; break;
case DockTarget.Center: break;
}
var transform = targetGroup.TransformToVisual(_overlayCanvas);
var point = transform.TransformPoint(new global::Windows.Foundation.Point(x, y));
Microsoft.UI.Xaml.Controls.Canvas.SetLeft(_highlight, point.X);
Microsoft.UI.Xaml.Controls.Canvas.SetTop(_highlight, point.Y);
_highlight.Width = width;
_highlight.Height = height;
}
internal void HideHighlight()
{
if (_highlight != null) _highlight.Visibility = Visibility.Collapsed;
}
internal void HandleDrop(DockDocument doc, DockGroup targetGroup, global::Windows.Foundation.Point position)
{
HideHighlight();
var target = CalculateDockTarget(targetGroup, position);
var oldOwner = doc.Owner as DockContainer;
oldOwner?.RemoveChildInternal(doc, false);
if (target == DockTarget.Center)
{
if (doc.Owner == targetGroup) return;
targetGroup.AddChild(doc);
}
else
{
if (doc.Owner == targetGroup && targetGroup.Children.Count == 1) return;
SplitGroup(targetGroup, doc, target);
}
if (oldOwner != null)
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
oldOwner.CheckCleanup();
});
}
}
private DockTarget CalculateDockTarget(DockGroup group, global::Windows.Foundation.Point position)
{
double w = group.ActualWidth;
double h = group.ActualHeight;
double x = position.X;
double y = position.Y;
if (x < w * 0.25) return DockTarget.Left;
if (x > w * 0.75) return DockTarget.Right;
if (y < h * 0.25) return DockTarget.Top;
if (y > h * 0.75) return DockTarget.Bottom;
return DockTarget.Center;
}
internal void CreateFloatingWindow(DockDocument doc)
{
ArgumentNullException.ThrowIfNull(doc);
var oldOwner = doc.Owner as DockContainer;
oldOwner?.RemoveChildInternal(doc, false);
var window = new FloatingWindow(doc);
_floatingWindows.Add(window);
window.Closed += (s, e) => _floatingWindows.Remove(window);
window.Activate();
if (oldOwner != null)
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
oldOwner.CheckCleanup();
});
}
}
}

View File

@@ -1,20 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockingLayout">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockingLayout">
<Grid>
<ContentPresenter x:Name="PART_Content" Content="{TemplateBinding RootModule}" />
<Canvas x:Name="PART_OverlayCanvas" IsHitTestVisible="False">
<local:DockRegionHighlight x:Name="PART_Highlight" Visibility="Collapsed" />
</Canvas>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,10 +0,0 @@
namespace Ghost.Editor.Views.Controls.Docking;
public enum DockTarget
{
Center,
Left,
Right,
Top,
Bottom
}

View File

@@ -1,42 +0,0 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// A floating window that contains a docking layout.
/// </summary>
public class FloatingWindow : Window
{
private readonly DockingLayout _layout;
/// <summary>
/// Initializes a new instance of the <see cref="FloatingWindow"/> class with the specified document.
/// </summary>
/// <param name="document">The document to display in the floating window.</param>
public FloatingWindow(DockDocument document)
{
ArgumentNullException.ThrowIfNull(document);
_layout = new DockingLayout();
var group = new DockGroup();
group.AddChild(document);
_layout.RootModule = group;
_layout.LayoutEmpty += (s, e) => Close();
Content = _layout;
// Basic window setup
AppWindow.Resize(new global::Windows.Graphics.SizeInt32(800, 600));
// When the user manually closes the window, ensure we clean up the documents inside
this.Closed += FloatingWindow_Closed;
}
private void FloatingWindow_Closed(object sender, WindowEventArgs args)
{
// Force cleanup of the visual tree so we don't leak anything from this window
_layout.RootModule = null;
Content = null;
}
}

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.Views.Pages.EngineEditor.ConsolePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Toolbar -->
<Grid
Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,0,1">
<CommandBar DefaultLabelPosition="Collapsed">
<CommandBar.PrimaryCommands>
<AppBarButton Command="{x:Bind ViewModel.ClearLogsCommand}" Content="Clear" />
<AppBarSeparator />
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowInfo, Mode=TwoWay}">
<AppBarToggleButton.Icon>
<FontIcon Glyph="&#xF167;" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowWarning, Mode=TwoWay}">
<AppBarToggleButton.Icon>
<FontIcon Glyph="&#xE814;" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowError, Mode=TwoWay}">
<AppBarToggleButton.Icon>
<FontIcon Glyph="&#xEB90;" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
</CommandBar.PrimaryCommands>
<CommandBar.SecondaryCommands>
<AppBarToggleButton BorderThickness="0" Label="Clear On Play" />
<AppBarToggleButton
BorderThickness="0"
IsChecked="{x:Bind ViewModel.ShowStackTrace, Mode=TwoWay}"
Label="Show Stack Trace" />
</CommandBar.SecondaryCommands>
</CommandBar>
</Grid>
<!-- Log Content -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<ListView
x:Name="LogListView"
Grid.Row="0"
ItemsSource="{x:Bind ViewModel.Logs, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedLog, Mode=TwoWay}" />
<Grid
Grid.Row="1"
Padding="4"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,1,0,0">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<TextBlock
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedLog.ToString(), Mode=OneWay}"
TextWrapping="Wrap" />
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -1,19 +0,0 @@
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Pages.EngineEditor;
internal sealed partial class ConsolePage : Page
{
public ConsoleViewModel ViewModel
{
get;
}
public ConsolePage()
{
ViewModel = App.GetService<ConsoleViewModel>();
InitializeComponent();
}
}

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<internal:NavigationTabPage
x:Class="Ghost.Editor.Views.Pages.EngineEditor.HierarchyPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:internal="using:Ghost.Editor.Controls"
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sg="using:Ghost.Editor.Core.SceneGraph"
mc:Ignorable="d">
<internal:NavigationTabPage.Resources>
<DataTemplate x:Key="SceneTemplate" x:DataType="sg:SceneGraphNode">
<TreeViewItem
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
Background="{ThemeResource ControlSolidFillColorDefaultBrush}"
IsExpanded="True"
ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF159;" />
<TextBlock Margin="10,0" Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
<DataTemplate x:Key="EntityTemplate" x:DataType="sg:SceneGraphNode">
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}" ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Margin="10,0" Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF158;" />
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
</internal:NavigationTabPage.Resources>
<Grid Padding="4,6" Background="{ThemeResource LayerFillColorDefaultBrush}">
<!--<TreeView ItemsSource="{x:Bind ViewModel.SceneList}" SelectionChanged="TreeView_SelectionChanged">
<TreeView.ItemTemplateSelector>
<local:HierarchyTemplateSector />
</TreeView.ItemTemplateSelector>
</TreeView>-->
</Grid>
</internal:NavigationTabPage>

View File

@@ -1,61 +0,0 @@
using Ghost.Editor.Controls;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.SceneGraph;
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Pages.EngineEditor;
internal sealed partial class HierarchyPage : NavigationTabPage
{
private readonly IInspectorService _inspectorService;
public HierarchyViewModel ViewModel
{
get;
}
public HierarchyPage()
{
_inspectorService = App.GetService<IInspectorService>();
ViewModel = App.GetService<HierarchyViewModel>();
InitializeComponent();
}
public override void OnNavigatedTo(object? parameter)
{
ViewModel.OnNavigatedTo(parameter);
}
public override void OnNavigatedFrom()
{
ViewModel.OnNavigatedFrom();
}
private void TreeView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
{
if (args.AddedItems.Count > 0 && args.AddedItems[0] is IInspectable inspectable)
{
_inspectorService.SetSelected(inspectable, ViewModel);
}
else
{
_inspectorService.SetSelected(null, ViewModel);
}
}
}
internal partial class HierarchyTemplateSector : DataTemplateSelector
{
protected override DataTemplate SelectTemplateCore(object item)
{
if (item is not SceneGraphNode node)
{
return base.SelectTemplateCore(item);
}
return node.GetSceneHierarchyTemplate();
}
}

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<internal:NavigationTabPage
x:Class="Ghost.Editor.Views.Pages.EngineEditor.InspectorPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:internal="using:Ghost.Editor.Controls"
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="75" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header -->
<Grid
Grid.Row="0"
Padding="15,0,10,0"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!--<IconSourceElement
Grid.Column="0"
Margin="0,0,15,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IconSource="{x:Bind ViewModel.Inspectable.Icon, Mode=OneWay}" />-->
<!--<ContentPresenter Grid.Column="1" Content="{x:Bind ViewModel.Inspectable.HeaderContent, Mode=OneWay}" />-->
</Grid>
<!-- Content -->
<Grid Grid.Row="1" Padding="0,0,0,0">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<!--<ContentPresenter Content="{x:Bind ViewModel.Inspectable.InspectorContent, Mode=OneWay}" />-->
</ScrollViewer>
</Grid>
</Grid>
</internal:NavigationTabPage>

View File

@@ -1,29 +0,0 @@
using Ghost.Editor.Controls;
using Ghost.Editor.ViewModels.Pages.EngineEditor;
namespace Ghost.Editor.Views.Pages.EngineEditor;
internal sealed partial class InspectorPage : NavigationTabPage
{
public InspectorViewModel ViewModel
{
get;
}
public InspectorPage()
{
ViewModel = App.GetService<InspectorViewModel>();
InitializeComponent();
}
public override void OnNavigatedTo(object? parameter)
{
ViewModel.OnNavigatedTo(parameter);
}
public override void OnNavigatedFrom()
{
ViewModel.OnNavigatedFrom();
}
}

View File

@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.Views.Pages.EngineEditor.ProjectPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:Ghost.Editor.Utilities.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Ghost.Editor.Models"
mc:Ignorable="d">
<Page.Resources>
<converter:AssetPathToGlyphConverter x:Key="AssetPathToGlyphConverter" />
</Page.Resources>
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Folder Tree View -->
<Grid
Grid.Column="0"
Padding="4"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,1,0">
<TreeView
x:Name="DirectoryTreeView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.SubDirectories}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectedItem="{x:Bind ViewModel.SelectedDirectory, Mode=TwoWay}">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="model:ExplorerItem">
<TreeViewItem ItemsSource="{x:Bind Children}">
<StackPanel Orientation="Horizontal">
<FontIcon
VerticalAlignment="Center"
FontSize="14"
Glyph="&#xE8B7;" />
<TextBlock
Margin="8,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
<!-- Files -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
Padding="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,0,1">
<BreadcrumbBar Height="15" />
</Grid>
<ScrollViewer
Grid.Row="1"
Padding="8"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<GridView
x:Name="AssetsGridView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.DirectoryAssets, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedAsset, Mode=TwoWay}">
<GridView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
<Setter Property="Margin" Value="2" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemTemplate>
<DataTemplate x:DataType="model:ExplorerItem">
<Grid
Width="100"
Height="100"
Padding="8"
DoubleTapped="GridViewItem_DoubleTapped"
IsDoubleTapEnabled="True">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="0.25*" />
</Grid.RowDefinitions>
<FontIcon FontSize="42" Glyph="{x:Bind FullName, Converter={StaticResource AssetPathToGlyphConverter}}" />
<TextBlock
Grid.Row="1"
Margin="8,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</ScrollViewer>
<Grid
Grid.Row="2"
Padding="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,1,0,0">
<TextBlock
VerticalAlignment="Center"
HorizontalTextAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedAsset.FullName, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -1,25 +0,0 @@
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Ghost.Editor.Views.Pages.EngineEditor;
internal sealed partial class ProjectPage : Page
{
public ProjectViewModel ViewModel
{
get;
}
public ProjectPage()
{
ViewModel = App.GetService<ProjectViewModel>();
InitializeComponent();
}
private void GridViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
ViewModel.OpenSelected();
}
}

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<internal:NavigationTabPage
x:Class="Ghost.Editor.Views.Pages.EngineEditor.ScenePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:internal="using:Ghost.Editor.Controls"
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<SwapChainPanel
x:Name="SwapChainPanel"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</internal:NavigationTabPage>

View File

@@ -1,45 +0,0 @@
using Ghost.Editor.Controls;
//using Ghost.Graphics.Contracts;
//using Microsoft.UI.Xaml;
//using Microsoft.UI.Xaml.Controls;
//using WinRT;
namespace Ghost.Editor.Views.Pages.EngineEditor;
internal sealed partial class ScenePage : NavigationTabPage
{
//private Renderer? _renderView;
//private ISwapChainPanelNative _swapChainPanelNative;
public ScenePage()
{
InitializeComponent();
//SwapChainPanel.Loaded += SwapChainPanel_Loaded;
//SwapChainPanel.Unloaded += SwapChainPanel_Unloaded;
//SwapChainPanel.SizeChanged += SwapChainPanel_SizeChanged;
}
//private void SwapChainPanel_Loaded(object sender, RoutedEventArgs e)
//{
// var guid = typeof(ISwapChainPanelNative.Interface).GUID;
// ((IWinRTObject)SwapChainPanel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle);
// _swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle);
// _renderView = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)SwapChainPanel.ActualWidth, (uint)SwapChainPanel.ActualHeight));
//}
//private void SwapChainPanel_Unloaded(object sender, RoutedEventArgs e)
//{
// _swapChainPanelNative.Dispose();
// _renderView?.Dispose();
//}
//private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
//{
// if (e.NewSize.ActualWidth > 8.0 && e.NewSize.ActualHeight > 8.0)
// {
// _renderView?.RequestResize((uint)e.NewSize.ActualWidth, (uint)e.NewSize.ActualHeight);
// }
//}
}

View File

@@ -1,246 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<winex:WindowEx
x:Class="Ghost.Editor.Views.Windows.EngineEditorWindowOld"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:controls="using:Ghost.Editor.Views.Controls"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ee="using:Ghost.Editor.Views.Pages.EngineEditor"
xmlns:ghost="using:Ghost.Editor.Controls"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:Ghost.Editor.Views.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid Loaded="MainGrid_Loaded" Unloaded="MainGrid_Unloaded">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Titlebar -->
<TitleBar
x:Name="PART_TitleBar"
Grid.Row="0"
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
Subtitle="Ghost Engine">
<TitleBar.IconSource>
<ImageIconSource ImageSource="ms-appx:///Assets/icon.targetsize-48.png" />
</TitleBar.IconSource>
</TitleBar>
<!-- Toolbar -->
<Grid
Grid.Row="1"
Padding="4,0,4,4"
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}">
<ctc:TabbedCommandBar>
<ctc:TabbedCommandBar.MenuItems>
<ctc:TabbedCommandBarItem Header="Home">
<AppBarButton Label="Undo" />
<AppBarButton Label="Redo" />
<AppBarButton Label="Paste" />
</ctc:TabbedCommandBarItem>
<ctc:TabbedCommandBarItem Header="Home">
<AppBarButton Label="Undo" />
<AppBarButton Label="Redo" />
<AppBarButton Label="Paste" />
</ctc:TabbedCommandBarItem>
<ctc:TabbedCommandBarItem Header="Home">
<AppBarButton Label="Undo" />
<AppBarButton Label="Redo" />
<AppBarButton Label="Paste" />
</ctc:TabbedCommandBarItem>
</ctc:TabbedCommandBar.MenuItems>
</ctc:TabbedCommandBar>
</Grid>
<Grid xmlns:dock="using:Ghost.Editor.Views.Controls.Docking" Grid.Row="2">
<dock:DockingLayout x:Name="MainDockingLayout" />
</Grid>
<!-- Editor -->
<!--<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ghost:NavigationTabView
Grid.Column="0"
Width="350"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ghost:NavigationTabView.TabItems>
<TabViewItem Header="Hierarchy">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xE8A4;" />
</TabViewItem.IconSource>
<controls:Hierarchy />
</TabViewItem>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
<ghost:NavigationTabView Grid.Column="1">
<ghost:NavigationTabView.TabItems>
<ee:ScenePage Header="Scene">
<ee:ScenePage.IconSource>
<FontIconSource Glyph="&#xF159;" />
</ee:ScenePage.IconSource>
</ee:ScenePage>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
<ghost:NavigationTabView
Grid.Column="2"
Width="350"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ghost:NavigationTabView.TabItems>
<ee:InspectorPage Header="Inspector">
<ee:InspectorPage.IconSource>
<FontIconSource Glyph="&#xEC7A;" />
</ee:InspectorPage.IconSource>
</ee:InspectorPage>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
</Grid>
<ghost:NavigationTabView Grid.Row="1" Height="350">
<ghost:NavigationTabView.TabItems>
<TabViewItem Header="Project">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xEC50;" />
</TabViewItem.IconSource>
<controls:ProjectBrowser />
</TabViewItem>
<TabViewItem Header="Console">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xE756;" />
</TabViewItem.IconSource>
<ee:ConsolePage />
</TabViewItem>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
</Grid>-->
<!-- Status Bar -->
<Grid
Grid.Row="3"
Height="25"
Background="{ThemeResource SmokeFillColorDefaultBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<FontIcon
Margin="8,0,0,0"
FontSize="16"
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
Glyph="&#xE930;"
Visibility="Visible" />
<StackPanel Orientation="Horizontal" Visibility="Collapsed">
<FontIcon
Margin="8,0,0,0"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource SystemFillColorAttentionBrush}"
Glyph="&#xE946;" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="0" />
<FontIcon
Margin="8,0,0,0"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource SystemFillColorCautionBrush}"
Glyph="&#xE7BA;" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="0" />
<FontIcon
Margin="8,0,0,0"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Glyph="&#xE783;" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="0" />
</StackPanel>
</Grid>
</Grid>
<!-- Info and Progress -->
<Grid Grid.Row="0" Grid.RowSpan="4">
<InfoBar
x:Name="InfoBar"
Margin="16"
HorizontalAlignment="Right"
VerticalAlignment="Bottom">
<interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
</interactivity:Interaction.Behaviors>
</InfoBar>
<Grid
x:Name="ProgressBarContainer"
Background="{ThemeResource SmokeFillColorDefaultBrush}"
Visibility="Collapsed">
<Grid
Height="100"
Padding="36,24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{ThemeResource SolidBackgroundFillColorBaseBrush}"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
x:Name="ProgressMessage"
Grid.Row="0"
Margin="0,0,0,12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}"
Text="Loading..." />
<ProgressBar
x:Name="ProgressBar"
Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsIndeterminate="True" />
</Grid>
</Grid>
</Grid>
</Grid>
</winex:WindowEx>

View File

@@ -1,88 +0,0 @@
using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services;
using Ghost.Editor.ViewModels.Windows;
using Windows.ApplicationModel;
using WinUIEx;
namespace Ghost.Editor.Views.Windows;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
internal sealed partial class EngineEditorWindowOld : WindowEx
{
private readonly NotificationService _notificationService;
private readonly ProgressService _progressService;
public EngineEditorViewModel ViewModel
{
get;
}
public EngineEditorWindowOld()
{
ViewModel = App.GetService<EngineEditorViewModel>();
_notificationService = (NotificationService)App.GetService<INotificationService>();
_progressService = (ProgressService)App.GetService<IProgressService>();
InitializeComponent();
AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/icon.ico"));
Title = "Ghost Engine";
ExtendsContentIntoTitleBar = true;
SetTitleBar(PART_TitleBar);
this.CenterOnScreen();
}
private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
PART_TitleBar.Title = EditorApplication.ProjectName;
PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}";
_notificationService.SetReference(InfoBar, NotificationQueue);
_progressService.SetReference(ProgressBarContainer);
InitializeDockingLayout();
}
private void InitializeDockingLayout()
{
var sceneDoc = new Controls.Docking.DockDocument { Title = "Scene", Content = new Pages.EngineEditor.ScenePage() };
var hierarchyDoc = new Controls.Docking.DockDocument { Title = "Hierarchy", Content = new Controls.Hierarchy() };
var inspectorDoc = new Controls.Docking.DockDocument { Title = "Inspector", Content = new Pages.EngineEditor.InspectorPage() };
var projectDoc = new Controls.Docking.DockDocument { Title = "Project", Content = new Controls.ProjectBrowser() };
var consoleDoc = new Controls.Docking.DockDocument { Title = "Console", Content = new Pages.EngineEditor.ConsolePage() };
var leftGroup = new Controls.Docking.DockGroup();
leftGroup.AddChild(hierarchyDoc);
var centerGroup = new Controls.Docking.DockGroup();
centerGroup.AddChild(sceneDoc);
var rightGroup = new Controls.Docking.DockGroup();
rightGroup.AddChild(inspectorDoc);
var bottomGroup = new Controls.Docking.DockGroup();
bottomGroup.AddChild(projectDoc);
bottomGroup.AddChild(consoleDoc);
var topPanel = new Controls.Docking.DockPanel { Orientation = Microsoft.UI.Xaml.Controls.Orientation.Horizontal };
topPanel.AddChild(leftGroup);
topPanel.AddChild(centerGroup);
topPanel.AddChild(rightGroup);
var rootPanel = new Controls.Docking.DockPanel { Orientation = Microsoft.UI.Xaml.Controls.Orientation.Vertical };
rootPanel.AddChild(topPanel);
rootPanel.AddChild(bottomGroup);
MainDockingLayout.RootModule = rootPanel;
}
private void MainGrid_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
_notificationService.ClearReference();
_progressService.ClearReference();
}
}

View File

@@ -1,15 +1,2 @@
namespace Ghost.Core;
public enum AssetType : byte
{
Texture = 0,
Mesh = 1,
Material = 2,
Shaders = 3,
Audio = 4,
Scene = 5,
Video = 6,
Json = 7,
Unknown = 64,
}

View File

@@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.8" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="2.0.0" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.0.0" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -158,6 +158,7 @@ public readonly struct Result<T>
public static implicit operator Result<T>(T? data) => data is not null ? Success(data) : Failure(null);
public static implicit operator Result<T>(Result result) => result.IsSuccess ? Success(default!) : Failure(result.Message);
public static implicit operator Result(Result<T> result) => result.IsSuccess ? Result.Success() : Result.Failure(result.Message);
public static implicit operator bool(Result<T> result) => result.IsSuccess;
}

View File

@@ -0,0 +1,162 @@
using System.Collections;
namespace Ghost.Core.Utilities;
public class ConcurrentHashSet<T> : IDisposable
{
public struct Enumerator : IEnumerator<T>
{
private readonly ConcurrentHashSet<T> _set;
private readonly HashSet<T>.Enumerator _enumerator;
public Enumerator(ConcurrentHashSet<T> set)
{
_set = set;
_set._lock.EnterReadLock();
_enumerator = _set._hashSet.GetEnumerator();
}
public readonly T Current => _enumerator.Current;
readonly object? IEnumerator.Current => Current;
public void Dispose()
{
if (_set._lock.IsReadLockHeld)
{
_set._lock.ExitReadLock();
}
_enumerator.Dispose();
}
public bool MoveNext()
{
return _enumerator.MoveNext();
}
public void Reset()
{
}
}
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly HashSet<T> _hashSet = new HashSet<T>();
public int Count
{
get
{
_lock.EnterReadLock();
try
{
return _hashSet.Count;
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}
}
public bool IsEmpty
{
get
{
_lock.EnterReadLock();
try
{
return _hashSet.Count == 0;
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}
}
~ConcurrentHashSet()
{
Dispose();
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
public bool Add(T item)
{
_lock.EnterWriteLock();
try
{
return _hashSet.Add(item);
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}
public bool Contains(T item)
{
_lock.EnterReadLock();
try
{
return _hashSet.Contains(item);
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}
public bool Remove(T item)
{
_lock.EnterWriteLock();
try
{
return _hashSet.Remove(item);
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}
public void Clear()
{
_lock.EnterWriteLock();
try
{
_hashSet.Clear();
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}
public void Dispose()
{
_lock.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -1,6 +1,8 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using MemoryHandle = System.Buffers.MemoryHandle;
namespace Ghost.Core.Utilities;
@@ -51,3 +53,47 @@ public unsafe class NativeMemoryManager<T> : MemoryManager<T>
{
}
}
public sealed class CastMemoryManager<TFrom, TTo> : MemoryManager<TTo>
where TFrom : struct
where TTo : struct
{
private readonly Memory<TFrom> _from;
private MemoryHandle _innerHandle;
public CastMemoryManager(Memory<TFrom> from)
{
_from = from;
}
public override Span<TTo> GetSpan()
{
return MemoryMarshal.Cast<TFrom, TTo>(_from.Span);
}
public override MemoryHandle Pin(int elementIndex = 0)
{
_innerHandle = _from.Pin();
unsafe
{
int byteOffset = elementIndex * Unsafe.SizeOf<TTo>();
void* pointer = (byte*)_innerHandle.Pointer + byteOffset;
return new MemoryHandle(pointer, default, this);
}
}
public override void Unpin()
{
_innerHandle.Dispose();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_innerHandle.Dispose();
}
}
}

View File

@@ -1,47 +1,7 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices;
namespace Ghost.Engine.AssetLoader;
public abstract class Asset : IResourceReleasable
{
private bool _disposed;
public Guid ID
{
get;
}
public abstract AssetType Type
{
get;
}
protected Asset(Guid id)
{
ID = id;
}
protected virtual void Release(IResourceDatabase resourceDatabase)
{
}
public void ReleaseResource(IResourceDatabase database)
{
if (_disposed)
{
return;
}
Release(database);
_disposed = true;
}
}
public readonly struct AssetReference : IEquatable<AssetReference>
{
private readonly int _value;
@@ -84,71 +44,3 @@ public readonly struct AssetReference : IEquatable<AssetReference>
return !(left == right);
}
}
[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
public struct TextureContentHeader
{
public uint width;
public uint height;
public uint depth;
public uint mipLevels;
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
public uint colorComponents;
}
public class TextureAsset : Asset
{
private MemoryBlock _textureData;
private readonly uint _width;
private readonly uint _height;
private readonly uint _depth;
private readonly uint _colorComponents;
private readonly uint _mipLevels;
private readonly uint _dimension;
private Handle<GPUTexture> _textureHandle;
public override AssetType Type => AssetType.Texture;
public uint Width => _width;
public uint Height => _height;
public uint Depth => _depth;
public uint MipLevels => _mipLevels;
public uint Dimension => _dimension;
public uint ColorComponents => _colorComponents;
public Handle<GPUTexture> TextureHandle => _textureHandle;
internal TextureAsset([OwnershipTransfer] ref MemoryBlock data, TextureContentHeader header, Guid id)
: base(id)
{
_textureData = data;
_width = header.width;
_height = header.height;
_depth = header.depth;
_mipLevels = header.mipLevels;
_dimension = header.dimension;
_colorComponents = header.colorComponents;
}
internal void SetTextureHandle(Handle<GPUTexture> handle, bool disposeCPUData = true)
{
_textureHandle = handle;
if (disposeCPUData)
{
_textureData.Dispose();
}
}
public ReadOnlySpan<T> GeData<T>()
where T : unmanaged
{
return _textureData.AsSpan<T>();
}
protected override void Release(IResourceDatabase resourceDatabase)
{
_textureData.Dispose();
resourceDatabase.ReleaseResource(_textureHandle.AsResource());
}
}

View File

@@ -1,13 +1,23 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using TerraFX.Interop.Windows;
using System.Runtime.InteropServices;
namespace Ghost.Engine;
[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
public struct TextureContentHeader
{
public uint width;
public uint height;
public uint depth;
public uint mipLevels;
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
public uint colorComponents;
}
internal partial class AssetEntry
{
private unsafe class TextureData

View File

@@ -11,11 +11,24 @@ using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Engine;
public enum AssetState : byte
public enum AssetType
{
Texture = 0,
Mesh = 1,
Material = 2,
Shaders = 3,
Scene = 4,
Audio = 5,
Video = 6,
Json = 7,
Unknown = 32, // We are unlikely to have more than 32 asset types.
}
public enum AssetState
{
Unloaded = 0,
Scheduled = 1,
@@ -192,6 +205,11 @@ internal unsafe partial class AssetEntry
public void OnReleaseResource()
{
s_onReleaseResource[(int)_assetType]?.Invoke(this);
if (_rawData.IsCreated)
{
_rawData.Dispose();
}
}
}
@@ -199,7 +217,7 @@ internal struct LoadAssetJob : IJob
{
public Guid assetID;
public AssetType assetType;
public GCHandle assetManagerHandle;
public AssetManager assetManager;
private static Result LoadRawData(IContentProvider contentProvider, AssetEntry entry)
{
@@ -232,8 +250,6 @@ internal struct LoadAssetJob : IJob
public readonly void Execute(ref readonly JobExecutionContext ctx)
{
var assetManager = assetManagerHandle.Target as AssetManager;
Logger.DebugAssert(assetManager is not null);
if (!assetManager.TryGetEntry(assetID, out var entry))
@@ -279,8 +295,6 @@ internal partial class AssetManager : IDisposable
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
private GCHandle _selfHandle;
// TODO
private Handle<GPUTexture> _fallbackTexture;
private Handle<GPUTexture> _fallbackNormalMap;
@@ -300,7 +314,6 @@ internal partial class AssetManager : IDisposable
_jobScheduler = jobScheduler;
_entries = new ConcurrentDictionary<Guid, AssetEntry>();
_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);
}
internal bool TryGetEntry(Guid guid, [NotNullWhen(true)] out AssetEntry? entry)
@@ -378,10 +391,10 @@ internal partial class AssetManager : IDisposable
{
assetID = entry.AssetId,
assetType = entry.AssetType,
assetManagerHandle = _selfHandle,
assetManager = this,
};
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency, JobPriority.Low)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, JobPriority.Low, dependency)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -409,19 +422,19 @@ internal partial class AssetManager : IDisposable
return;
}
if (entry.State is AssetState.Loading or AssetState.Loaded or AssetState.Uploading)
if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Ready) == (int)AssetState.Ready)
{
entry.SetPendingReimport();
return;
}
// Entry is in Ready state — the old texture is valid and will remain visible.
// Go directly to Scheduled → Loading → Loaded → Uploading → Ready again.
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
// v1 → v2 transition exactly like the fallback → v1 transition.
entry.State = AssetState.Scheduled;
EnsureScheduled(entry);
}
else
{
entry.SetPendingReimport();
}
}
public void Dispose()
{
@@ -431,6 +444,5 @@ internal partial class AssetManager : IDisposable
}
_entries.Clear();
_selfHandle.Free();
}
}

View File

@@ -19,7 +19,6 @@ internal unsafe struct ChunkInfo
internal unsafe struct JobChunkBatch<TJob> : IJobParallelFor
where TJob : unmanaged, IJobChunk
{
public TJob userJob;
public ReadOnlyUnsafeCollection<ChunkInfo> chunkInfos;