Update editor

This commit is contained in:
2026-02-03 21:49:14 +09:00
parent 9fcf06dbe4
commit 59991f47d5
88 changed files with 1157 additions and 1288 deletions

View File

@@ -1,97 +0,0 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AppState;
internal partial class AppStateMachine : IDisposable, IAsyncDisposable
{
private Dictionary<StateKey, Lazy<IAppState>> _states = new();
private IAppState? _current;
private bool _disposed;
public void RegisterState(StateKey key, Func<IAppState> stateFactory)
{
_states[key] = new(stateFactory);
}
public async Task<Result> TransitionToAsync(StateKey stateKey, object? parameter = null)
{
var previous = _current;
if (!_states.TryGetValue(stateKey, out var next))
{
return Result.Failure($"State '{stateKey}' not found.");
}
Result result;
if (previous != null)
{
result = await previous.OnExitingAsync();
if (result.IsFailure)
{
return result;
}
}
result = await next.Value.OnEnteringAsync(parameter);
if (result.IsFailure)
{
if (previous != null)
{
await previous.OnEnteredAsync(parameter);
}
return result;
}
if (previous != null)
{
result = await previous.OnExitedAsync();
if (result.IsFailure)
{
await next.Value.OnExitedAsync();
await previous.OnEnteredAsync(parameter);
return result;
}
}
result = await next.Value.OnEnteredAsync(parameter);
if (result.IsFailure)
{
await next.Value.OnExitedAsync();
if (previous != null)
{
await previous.OnEnteredAsync(parameter);
}
return result;
}
_current = next.Value;
return Result.Success();
}
public void Dispose()
{
DisposeAsync().AsTask().Wait();
}
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_states.Clear();
if (_current != null)
{
await _current.OnExitingAsync();
await _current.OnExitedAsync();
}
_current = null;
_disposed = true;
}
}

View File

@@ -1,28 +0,0 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AppState;
internal interface IAppState
{
/// <summary>
/// Called when exiting the state.
/// </summary>
public ValueTask<Result> OnExitingAsync();
/// <summary>
/// Called when entering the state, right after OnEnteringAsync.
/// <paramref name="parameter">can be used to pass data into the state, such as a project to load.</summary>
/// </summary>
public ValueTask<Result> OnEnteringAsync(object? parameter);
/// <summary>
/// Called when exiting the state, specifically for pose transitions.
/// </summary>
public ValueTask<Result> OnExitedAsync();
/// <summary>
/// Called when entered the state, specifically after the state has been fully initialized and is ready for interaction.
/// </summary>
/// <param name="parameter">can be used to pass data into the state, such as a project to load.</param>
public ValueTask<Result> OnEnteredAsync(object? parameter);
}

View File

@@ -1,8 +0,0 @@
namespace Ghost.Editor.Core.AppState;
internal enum StateKey
{
None,
Landing,
EngineEditor,
}

View File

@@ -208,7 +208,7 @@ public static partial class AssetDatabase
/// <summary>
/// Move an asset to a new location by path.
/// </summary>
/// <param name="oldPath">Current path of the asset.</param>
/// <param name="oldPath">CurrentApplication path of the asset.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default)

View File

@@ -1,5 +1,4 @@
using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent;
using System.Text.Json;
@@ -26,7 +25,7 @@ public static partial class AssetDatabase
return Result<string>.Failure("AssetsDirectory not initialized");
}
var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "ImportedAssets");
var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, EditorApplication.CACHES_FOLDER_NAME, "ImportedAssets");
if (!Directory.Exists(cacheDir))
{
Directory.CreateDirectory(cacheDir);

View File

@@ -1,5 +1,4 @@
using Ghost.Core;
using Ghost.Data.Services;
using Microsoft.Data.Sqlite;
using System.Text.Json;
@@ -19,7 +18,7 @@ public static partial class AssetDatabase
throw new InvalidOperationException("AssetsDirectory is not set. Initialize() must be called first.");
}
var dbPath = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "AssetDatabase.db");
var dbPath = Path.Combine(AssetsDirectory.Parent!.FullName, EditorApplication.CACHES_FOLDER_NAME, "AssetDatabase.db");
var cacheDir = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(cacheDir))
{
@@ -301,7 +300,7 @@ public static partial class AssetDatabase
var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_');
await using var cmd = s_dbConnection.CreateCommand();
// Extract just the filename from the path for matching
// SQLite doesn't have a built-in path manipulation, so we search in the full path
// and filter by checking if the pattern matches the filename part
@@ -319,12 +318,12 @@ public static partial class AssetDatabase
// Extract filename and check if it matches the pattern
var fileName = Path.GetFileName(path);
// Convert pattern to regex for proper matching
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(namePattern)
.Replace("\\*", ".*")
.Replace("\\?", ".") + "$";
if (System.Text.RegularExpressions.Regex.IsMatch(fileName, regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
if (Guid.TryParse(guidStr, out var guid))
@@ -377,10 +376,10 @@ public static partial class AssetDatabase
}
// Remove orphaned entries
foreach (var guid in orphanedGuids)
{
await RemoveAssetFromDatabaseAsync(guid, token);
}
foreach (var guid in orphanedGuids)
{
await RemoveAssetFromDatabaseAsync(guid, token);
}
}
catch
{

View File

@@ -1,5 +1,4 @@
using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -92,12 +91,7 @@ public static partial class AssetDatabase
s_initialized = true;
}
if (ProjectService.CurrentProject.Metadata == null)
{
throw new InvalidOperationException("Project metadata is not initialized. Ensure that the project is loaded before accessing the AssetDatabase.");
}
AssetsDirectory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER));
AssetsDirectory = new DirectoryInfo(Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME));
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
{

View File

@@ -1,7 +1,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
namespace Ghost.Editor.Core.Contracts;
public interface IInspectable
{

View File

@@ -0,0 +1,32 @@
namespace Ghost.Editor.Core.Contracts;
public class InspectorSelectionChangedEventArgs : EventArgs
{
public object? Source
{
get;
}
public IInspectable? Selected
{
get;
}
public InspectorSelectionChangedEventArgs(object? source, IInspectable? selected)
{
Source = source;
Selected = selected;
}
}
public interface IInspectorService
{
IInspectable? Selected
{
get;
}
event EventHandler<InspectorSelectionChangedEventArgs> OnSelectionChanged;
void SetSelected(IInspectable? inspectable, object? source);
}

View File

@@ -1,6 +1,7 @@
using CommunityToolkit.WinUI.Behaviors;
using Ghost.Editor.Core.Notifications;
namespace Ghost.Editor.Core.Notifications;
namespace Ghost.Editor.Core.Contracts;
public interface INotificationService
{

View File

@@ -0,0 +1,12 @@
namespace Ghost.Editor.Core.Contracts;
public enum IconSize
{
Small,
Large
}
public interface IPreviewService
{
string GetIconPath(string path, bool isDirectory, IconSize size);
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Editor.Core.Progress;
namespace Ghost.Editor.Core.Contracts;
public interface IProgressService
{

View File

@@ -0,0 +1,39 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core;
public static class EditorApplication
{
public const string ASSETS_FOLDER_NAME = "Assets";
public const string CACHES_FOLDER_NAME = "Caches";
private static IServiceProvider? s_serviceProvider;
private static string s_currentProjectPath = string.Empty;
private static string s_currentProjectName = string.Empty;
internal static Application CurrentApplication => Application.Current;
internal static string CurrentProjectPath => s_currentProjectPath;
internal static string CurrentProjectName => s_currentProjectName;
internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName)
{
s_serviceProvider = serviceProvider;
s_currentProjectPath = projectPath;
s_currentProjectName = projectName;
}
public static T GetService<T>()
where T : class
{
if (s_serviceProvider?.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
internal static void Shutdown()
{
}
}

View File

@@ -0,0 +1,28 @@
namespace Ghost.Editor.Core;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class EditorInjectionAttribute : Attribute
{
public enum ServiceLifetime
{
Singleton,
Transient,
Scoped
}
public ServiceLifetime Lifetime
{
get;
}
public Type? ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type? implementationType = null)
{
Lifetime = lifetime;
ImplementationType = implementationType;
}
}

View File

@@ -21,7 +21,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Data\Ghost.Data.csproj" />
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
</ItemGroup>

View File

@@ -1,12 +0,0 @@
namespace Ghost.Editor.Core.Inspector;
internal interface IInspectorService
{
public IInspectable? SelectedInspectable
{
get;
set;
}
public event Action? OnSelectionChanged;
}

View File

@@ -1,19 +0,0 @@
namespace Ghost.Editor.Core.Inspector;
public class InspectorService : IInspectorService
{
public IInspectable? SelectedInspectable
{
get => field;
set
{
if (field != value)
{
field = value;
OnSelectionChanged?.Invoke();
}
}
}
public event Action? OnSelectionChanged;
}

View File

@@ -1,5 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;

View File

@@ -0,0 +1,21 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
public class InspectorService : IInspectorService
{
private IInspectable? _selected;
public IInspectable? Selected => _selected;
public event EventHandler<InspectorSelectionChangedEventArgs>? OnSelectionChanged;
public void SetSelected(IInspectable? inspectable, object? source)
{
if (_selected != inspectable)
{
_selected = inspectable;
OnSelectionChanged?.Invoke(this, new InspectorSelectionChangedEventArgs(source, inspectable));
}
}
}

View File

@@ -1,7 +1,9 @@
using CommunityToolkit.WinUI.Behaviors;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Notifications;
namespace Ghost.Editor.Core.Services;
public class NotificationService : INotificationService
{

View File

@@ -0,0 +1,35 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
internal class PreviewService : IPreviewService
{
public string GetIconPath(string path, bool isDirectory, IconSize size)
{
string iconPath;
if (isDirectory)
{
iconPath = "ms-appx:///Assets/EditorIcons/folder-{0}.png";
}
else
{
// TODO: Generate preview icons dynamically for known file types like images, meshes, materials, etc.
var ext = Path.GetExtension(path);
iconPath = ext switch
{
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".tiff" or ".svg" => "ms-appx:///Assets/EditorIcons/image-{0}.png",
_ => "ms-appx:///Assets/EditorIcons/document-{0}.png",
};
}
var sizeIndex = size switch
{
IconSize.Small => "0",
IconSize.Large => "1",
_ => "0"
};
iconPath = string.Format(iconPath, sizeIndex);
return iconPath;
}
}

View File

@@ -1,9 +1,10 @@
using CommunityToolkit.WinUI;
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.CompilerServices;
namespace Ghost.Editor.Core.Progress;
namespace Ghost.Editor.Core.Services;
public class ProgressService : IProgressService
{

View File

@@ -1,26 +0,0 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core.Utilities;
public static class EditorApplication
{
private static IServiceProvider? _serviceProvider;
public static Application Current => Application.Current;
internal static void Initialize(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public static T GetService<T>()
where T : class
{
if (_serviceProvider?.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}
return service;
}
}

View File

@@ -1,12 +1,10 @@
using Ghost.Data.Models;
namespace Ghost.Editor.Core.Utilities;
internal static class FileExtensions
{
public const string META_FILE_EXTENSION = ".gmeta";
public const string PROJECT_FILE_EXTENSION = "." + ProjectMetadata.PROJECT_FILE_EXTENSION_NAME;
public const string PROJECT_FILE_EXTENSION = ".gproj";
public const string TEMPLATE_FILE_EXTENSION = ".gtmpl";
public const string SCENE_FILE_EXTENSION = ".gscene";
public const string ASSET_FILE_EXTENSION = ".gasset";

View File

@@ -29,6 +29,12 @@ public static class TypeCache
s_types = loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
}
internal static void Init()
{
// Intentionally left blank.
// This method exists to force the static constructor to run.
}
public static Type[] GetTypes()
{
return s_types;