Update ContextFlyout

This commit is contained in:
2026-02-05 13:52:53 +09:00
parent eadd13931f
commit 9bbccfc8f8
20 changed files with 258 additions and 105 deletions

View File

@@ -20,3 +20,6 @@ public class CollectionPool<TCollection, TItem>
} }
public class ListPool<T> : CollectionPool<List<T>, T>; public class ListPool<T> : CollectionPool<List<T>, T>;
public class HashSetPool<T> : CollectionPool<HashSet<T>, T>;
public class DictionaryPool<TKey, TValue> : CollectionPool<Dictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>
where TKey : notnull;

View File

@@ -2,7 +2,7 @@ using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase public static partial class AssetService
{ {
/// <summary> /// <summary>
/// Create a new asset at the specified path. /// Create a new asset at the specified path.

View File

@@ -3,7 +3,7 @@ using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase public static partial class AssetService
{ {
private static readonly Dictionary<Type, AssetImporter> s_importerInstances = new(); private static readonly Dictionary<Type, AssetImporter> s_importerInstances = new();

View File

@@ -4,7 +4,7 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase public static partial class AssetService
{ {
// Asset cache - stores loaded assets by GUID // Asset cache - stores loaded assets by GUID
private static readonly ConcurrentDictionary<Guid, Asset> s_assetCache = new(); private static readonly ConcurrentDictionary<Guid, Asset> s_assetCache = new();

View File

@@ -3,7 +3,7 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase public static partial class AssetService
{ {
/// <summary> /// <summary>
/// Get the relative path from the assets directory. /// Get the relative path from the assets directory.
@@ -96,7 +96,7 @@ public static partial class AssetDatabase
/// <returns>The loaded asset.</returns> /// <returns>The loaded asset.</returns>
public static Result<T> LoadAsset<T>(Guid guid) where T : Asset public static Result<T> LoadAsset<T>(Guid guid) where T : Asset
{ {
// Implemented in AssetDatabase.Loader.cs // Implemented in AssetService.Loader.cs
return LoadAssetInternal<T>(guid); return LoadAssetInternal<T>(guid);
} }

View File

@@ -7,7 +7,7 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase public static partial class AssetService
{ {
private static readonly Dictionary<string, Type> s_importerTypeLookup = new(); private static readonly Dictionary<string, Type> s_importerTypeLookup = new();

View File

@@ -5,7 +5,7 @@ using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase public static partial class AssetService
{ {
private static readonly Dictionary<string, Action<string>> s_assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, Action<string>> s_assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase);

View File

@@ -4,7 +4,7 @@ using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle; namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase public static partial class AssetService
{ {
private static SqliteConnection? s_dbConnection; private static SqliteConnection? s_dbConnection;

View File

@@ -33,7 +33,7 @@ internal readonly record struct AssetCommand(
/// Handles asset registration, lookup, importing, and dependency management. /// Handles asset registration, lookup, importing, and dependency management.
/// Uses SQLite for persistent storage and efficient querying. /// Uses SQLite for persistent storage and efficient querying.
/// </summary> /// </summary>
public static partial class AssetDatabase public static partial class AssetService
{ {
private static FileSystemWatcher? s_watcher; private static FileSystemWatcher? s_watcher;
private static readonly Lock s_dbLock = new(); private static readonly Lock s_dbLock = new();
@@ -47,7 +47,6 @@ public static partial class AssetDatabase
// Command buffer pattern - Channel for file system event commands // Command buffer pattern - Channel for file system event commands
private static Channel<AssetCommand>? s_commandChannel; private static Channel<AssetCommand>? s_commandChannel;
private static Timer? s_commandProcessorTimer; private static Timer? s_commandProcessorTimer;
private static readonly Lock s_commandLock = new();
private static readonly ConcurrentQueue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh private static readonly ConcurrentQueue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh
private static bool s_autoRefreshEnabled = true; private static bool s_autoRefreshEnabled = true;
@@ -56,7 +55,7 @@ public static partial class AssetDatabase
private static bool s_initialized = false; private static bool s_initialized = false;
private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100); private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100);
private static ManualResetEventSlim s_resetEventSlim = new(false); private static readonly ManualResetEventSlim s_resetEventSlim = new(false);
private static readonly JsonSerializerOptions s_defaultJsonOptions = new() private static readonly JsonSerializerOptions s_defaultJsonOptions = new()
{ {
@@ -78,7 +77,6 @@ public static partial class AssetDatabase
/// Initialize the asset database. /// Initialize the asset database.
/// Must be called after project is loaded. /// Must be called after project is loaded.
/// </summary> /// </summary>
internal static async Task Initialize(CancellationToken token = default) internal static async Task Initialize(CancellationToken token = default)
{ {
lock (s_initializationLock) lock (s_initializationLock)

View File

@@ -39,7 +39,7 @@ public abstract class AssetImporter
{ {
foreach (var dependencyGuid in dependencies) foreach (var dependencyGuid in dependencies)
{ {
var path = AssetDatabase.GuidToPath(dependencyGuid); var path = AssetService.GuidToPath(dependencyGuid);
if (path.IsFailure) if (path.IsFailure)
{ {
return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}")); return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}"));

View File

@@ -134,7 +134,7 @@ internal class TextureImporter : AssetImporter<TextureImporterSettings>
}; };
// Save the imported asset data // Save the imported asset data
var saveResult = AssetDatabase.SaveImportedAsset(meta.Guid, textureAsset); var saveResult = AssetService.SaveImportedAsset(meta.Guid, textureAsset);
if (saveResult.IsFailure) if (saveResult.IsFailure)
{ {
return Result.Failure($"Failed to save texture asset: {saveResult.Message}"); return Result.Failure($"Failed to save texture asset: {saveResult.Message}");

View File

@@ -0,0 +1,5 @@
namespace Ghost.Editor.Core.Contracts;
public interface IAssetService
{
}

View File

@@ -8,6 +8,35 @@ namespace Ghost.Editor.Core.Controls;
public sealed partial class ContextFlyout : MenuFlyout public sealed partial class ContextFlyout : MenuFlyout
{ {
private class MenuNode
{
public required string Name
{
get; init;
}
public MethodInfo? Method
{
get; set;
}
public List<MenuNode> Children
{
get;
} = new();
public int RawGroup
{
get; set;
} = int.MaxValue;
// The calculated group used for sorting (min of children for folders)
public int EffectiveGroup
{
get; set;
}
}
private bool _isPopulated; private bool _isPopulated;
public string Tag public string Tag
@@ -20,6 +49,89 @@ public sealed partial class ContextFlyout : MenuFlyout
Opening += ContextFlyout_Opening; Opening += ContextFlyout_Opening;
} }
// Recursively sorts nodes and calculates folder groups
private static void PrepareNodes(List<MenuNode> nodes)
{
if (nodes.Count == 0)
{
return;
}
foreach (var node in nodes)
{
if (node.Children.Count > 0)
{
// Go deep first
PrepareNodes(node.Children);
// A folder's group is determined by its highest priority child (lowest group number).
// This ensures a "File" folder (containing Group 0 items) sits at the top
// alongside other Group 0 leaf items.
node.EffectiveGroup = node.Children.Min(c => c.EffectiveGroup);
}
else
{
node.EffectiveGroup = node.RawGroup;
}
}
// Sort by Group, then by Name
nodes.Sort((a, b) =>
{
var groupCompare = a.EffectiveGroup.CompareTo(b.EffectiveGroup);
return groupCompare != 0
? groupCompare
: string.CompareOrdinal(a.Name, b.Name);
});
}
// Recursively builds the UI elements
private static void BuildNodes(List<MenuNode> nodes, IList<MenuFlyoutItemBase> targetCollection)
{
if (nodes.Count == 0)
{
return;
}
int currentGroup = nodes[0].EffectiveGroup;
foreach (var node in nodes)
{
if (node.EffectiveGroup != currentGroup)
{
targetCollection.Add(new MenuFlyoutSeparator());
currentGroup = node.EffectiveGroup;
}
if (node.Children.Count > 0)
{
var subItem = new MenuFlyoutSubItem
{
Text = node.Name
};
// Recursively render children into the subitem
BuildNodes(node.Children, subItem.Items);
targetCollection.Add(subItem);
}
else
{
var menuItem = new MenuFlyoutItem
{
Text = node.Name
};
var methodToInvoke = node.Method;
menuItem.Click += (_, _) =>
{
methodToInvoke?.Invoke(null, null);
};
targetCollection.Add(menuItem);
}
}
}
private void PopulateContextMenu() private void PopulateContextMenu()
{ {
var methods = TypeCache.GetMethodsWithAttribute<ContextMenuItemAttribute>(); var methods = TypeCache.GetMethodsWithAttribute<ContextMenuItemAttribute>();
@@ -28,58 +140,66 @@ public sealed partial class ContextFlyout : MenuFlyout
return; return;
} }
var list = new List<(ContextMenuItemAttribute attr, MethodInfo method)>(); // 1. Build the Tree
var rootNodes = new List<MenuNode>();
foreach (var method in methods) foreach (var method in methods)
{ {
var attributes = method.GetCustomAttributes(typeof(ContextMenuItemAttribute), false); var attr = method.GetCustomAttribute<ContextMenuItemAttribute>();
var attribute = (ContextMenuItemAttribute)attributes[0]; if (attr == null)
if (!string.Equals(attribute.Tag, Tag, StringComparison.OrdinalIgnoreCase))
{ {
continue; continue;
} }
list.Add((attribute, method)); // Filter tags
} if (!string.Equals(attr.Tag, Tag, StringComparison.OrdinalIgnoreCase))
var span = CollectionsMarshal.AsSpan(list);
span.Sort((a, b) =>
{
var result = a.attr.Group.CompareTo(b.attr.Group);
if (result == 0)
{ {
result = string.CompareOrdinal(a.attr.Name, b.attr.Name); continue;
} }
return result; var nameSpan = attr.Name.AsSpan();
}); var pathParts = nameSpan.Split('/');
// itemContainer may be a main thread only collection (for example, ObservableCollection), we run it on the UI thread var currentLevel = rootNodes;
var i = 0; MenuNode? currentNode = null;
var group = 0;
while (i < list.Count) foreach (var range in pathParts)
{
var (attr, method) = list[i];
if (attr.Group != group)
{ {
Items.Add(new MenuFlyoutSeparator()); var part = nameSpan[range.Start..range.End];
group = attr.Group;
MenuNode? foundNode = null;
// Try to find existing node in the current level
foreach (var node in currentLevel)
{
if (part.Equals(node.Name.AsSpan(), StringComparison.Ordinal))
{
foundNode = node;
break;
}
}
if (foundNode == null)
{
foundNode = new MenuNode { Name = part.ToString() };
currentLevel.Add(foundNode);
}
currentNode = foundNode;
// If this is the last part, it's the executable item
if (range.End.Value == nameSpan.Length)
{
currentNode.Method = method;
currentNode.RawGroup = attr.Group;
}
currentLevel = currentNode.Children;
} }
// TODO: Group items with / in the name into submenus
var menuItem = new MenuFlyoutItem
{
Text = attr.Name
};
menuItem.Click += (_, _) =>
{
method.Invoke(null, null);
};
Items.Add(menuItem);
i++;
} }
PrepareNodes(rootNodes);
BuildNodes(rootNodes, Items);
} }
private async void ContextFlyout_Opening(object? sender, object e) private async void ContextFlyout_Opening(object? sender, object e)

View File

@@ -63,6 +63,7 @@ public partial class App : Application
services.AddSingleton<IProgressService, ProgressService>(); services.AddSingleton<IProgressService, ProgressService>();
services.AddSingleton<IInspectorService, InspectorService>(); services.AddSingleton<IInspectorService, InspectorService>();
services.AddSingleton<IPreviewService, PreviewService>(); services.AddSingleton<IPreviewService, PreviewService>();
services.AddSingleton<IAssetService, AssetService>();
services.AddSingleton<EngineEditorViewModel>(); services.AddSingleton<EngineEditorViewModel>();

View File

@@ -20,4 +20,34 @@ internal partial class ProjectBrowser
Verb = "open" Verb = "open"
}); });
} }
[ContextMenuItem("project-browser", "Create/Folder")]
private static void CreateFolder()
{
// TODO: Use AssetService
var viewModel = LastFocused?.ViewModel;
if (viewModel is null)
{
return;
}
var currentDir = viewModel.CurrentDirectoryPath;
if (!Directory.Exists(currentDir))
{
return;
}
var newFolderPath = Path.Combine(currentDir, "New Folder");
var folderIndex = 1;
while (Directory.Exists(newFolderPath))
{
newFolderPath = Path.Combine(currentDir, $"New Folder ({folderIndex})");
folderIndex++;
}
Directory.CreateDirectory(newFolderPath);
// Refresh the view model to show the new folder
viewModel.NavigateToDirectory(currentDir);
}
} }

View File

@@ -150,7 +150,8 @@
Padding="8" Padding="8"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
RowSpacing="4"> RowSpacing="4"
ToolTipService.ToolTip="{x:Bind Name}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -191,17 +192,7 @@
MinRowSpacing="4" /> MinRowSpacing="4" />
</ItemsView.Layout> </ItemsView.Layout>
<ItemsView.ContextFlyout> <ItemsView.ContextFlyout>
<ghost:ContextFlyout Tag="project-browser"> <ghost:ContextFlyout Tag="project-browser" />
<MenuFlyoutSubItem Text="Create">
<MenuFlyoutItem Text="Folder" />
<MenuFlyoutItem Text="Script" />
<MenuFlyoutSubItem Text="Rendering">
<MenuFlyoutItem Text="Material" />
<MenuFlyoutItem Text="Volume Profile" />
</MenuFlyoutSubItem>
</MenuFlyoutSubItem>
<MenuFlyoutSeparator />
</ghost:ContextFlyout>
</ItemsView.ContextFlyout> </ItemsView.ContextFlyout>
</ItemsView> </ItemsView>

View File

@@ -54,6 +54,11 @@ internal sealed partial class ProjectBrowser : UserControl
private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e) private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
{ {
_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged; _inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
if (LastFocused == this)
{
LastFocused = null;
}
} }
private void _inspectorService_OnSelectionChanged(object? sender, InspectorSelectionChangedEventArgs e) private void _inspectorService_OnSelectionChanged(object? sender, InspectorSelectionChangedEventArgs e)

View File

@@ -108,7 +108,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject
} }
else else
{ {
AssetDatabase.OpenAsset(SelectedItem.FullName); AssetService.OpenAsset(SelectedItem.FullName);
return (null, 1); return (null, 1);
} }
} }

View File

@@ -124,7 +124,7 @@ internal partial class ProjectViewModel : ObservableObject
} }
else else
{ {
AssetDatabase.OpenAsset(SelectedAsset.FullName); AssetService.OpenAsset(SelectedAsset.FullName);
} }
} }

View File

@@ -5,11 +5,11 @@ using Ghost.Core;
namespace Ghost.UnitTest; namespace Ghost.UnitTest;
/// <summary> /// <summary>
/// Comprehensive integration tests for AssetDatabase. /// Comprehensive integration tests for AssetService.
/// Tests database operations, file system watchers, searching, importing, and race conditions. /// Tests database operations, file system watchers, searching, importing, and race conditions.
/// </summary> /// </summary>
[TestClass] [TestClass]
[DoNotParallelize] // AssetDatabase is a singleton, tests must run sequentially [DoNotParallelize] // AssetService is a singleton, tests must run sequentially
public class AssetDatabaseIntegrationTest public class AssetDatabaseIntegrationTest
{ {
private string _tempPath = string.Empty; private string _tempPath = string.Empty;
@@ -49,8 +49,8 @@ public class AssetDatabaseIntegrationTest
var projectMetadataInfo = new Data.Models.ProjectMetadataInfo(projectPath, metadata); var projectMetadataInfo = new Data.Models.ProjectMetadataInfo(projectPath, metadata);
ProjectService.CurrentProject = projectMetadataInfo; ProjectService.CurrentProject = projectMetadataInfo;
// Initialize AssetDatabase // Initialize AssetService
await AssetDatabase.Initialize(TestContext.CancellationToken); await AssetService.Initialize(TestContext.CancellationToken);
// Give the file system watcher time to start // Give the file system watcher time to start
await Task.Delay(100, TestContext.CancellationToken); await Task.Delay(100, TestContext.CancellationToken);
@@ -59,10 +59,10 @@ public class AssetDatabaseIntegrationTest
[TestCleanup] [TestCleanup]
public void Cleanup() public void Cleanup()
{ {
// Shutdown AssetDatabase to release file watchers // Shutdown AssetService to release file watchers
try try
{ {
AssetDatabase.Shutdown(); AssetService.Shutdown();
} }
catch catch
{ {
@@ -91,7 +91,7 @@ public class AssetDatabaseIntegrationTest
private async Task WaitForFileSystemEvents(int delayMs = 300) private async Task WaitForFileSystemEvents(int delayMs = 300)
{ {
await Task.Delay(delayMs, TestContext.CancellationToken); await Task.Delay(delayMs, TestContext.CancellationToken);
AssetDatabase.FlushPendingCommands(); AssetService.FlushPendingCommands();
// Give a bit more time after flush for any final processing // Give a bit more time after flush for any final processing
await Task.Delay(50, TestContext.CancellationToken); await Task.Delay(50, TestContext.CancellationToken);
@@ -145,15 +145,15 @@ public class AssetDatabaseIntegrationTest
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
// Test wildcard search: player* // Test wildcard search: player*
var results = await AssetDatabase.FindAssetsByNameAsync("player*", TestContext.CancellationToken); var results = await AssetService.FindAssetsByNameAsync("player*", TestContext.CancellationToken);
Assert.HasCount(3, results, "Should find 3 files matching 'player*'"); Assert.HasCount(3, results, "Should find 3 files matching 'player*'");
// Test single character wildcard: player? // Test single character wildcard: player?
results = await AssetDatabase.FindAssetsByNameAsync("player?.txt", TestContext.CancellationToken); results = await AssetService.FindAssetsByNameAsync("player?.txt", TestContext.CancellationToken);
Assert.HasCount(2, results, "Should find 2 files matching 'player?.txt'"); Assert.HasCount(2, results, "Should find 2 files matching 'player?.txt'");
// Test exact match // Test exact match
results = await AssetDatabase.FindAssetsByNameAsync("enemy.txt", TestContext.CancellationToken); results = await AssetService.FindAssetsByNameAsync("enemy.txt", TestContext.CancellationToken);
Assert.HasCount(1, results, "Should find 1 file matching 'enemy.txt'"); Assert.HasCount(1, results, "Should find 1 file matching 'enemy.txt'");
CheckInternalErrors(); CheckInternalErrors();
@@ -168,7 +168,7 @@ public class AssetDatabaseIntegrationTest
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
// Get the GUID before rename // Get the GUID before rename
var guidResult = AssetDatabase.PathToGuid(originalPath); var guidResult = AssetService.PathToGuid(originalPath);
Assert.IsTrue(guidResult.IsSuccess, "Should be able to get GUID before rename"); Assert.IsTrue(guidResult.IsSuccess, "Should be able to get GUID before rename");
var guid = guidResult.Value; var guid = guidResult.Value;
@@ -182,7 +182,7 @@ public class AssetDatabaseIntegrationTest
Assert.IsTrue(File.Exists(newMetaPath), "Meta file should be moved with the asset"); Assert.IsTrue(File.Exists(newMetaPath), "Meta file should be moved with the asset");
// Verify GUID is preserved // Verify GUID is preserved
var newGuidResult = AssetDatabase.PathToGuid(newPath); var newGuidResult = AssetService.PathToGuid(newPath);
Assert.IsTrue(newGuidResult.IsSuccess, "Should be able to get GUID after rename"); Assert.IsTrue(newGuidResult.IsSuccess, "Should be able to get GUID after rename");
Assert.AreEqual(guid, newGuidResult.Value, "GUID should be preserved after rename"); Assert.AreEqual(guid, newGuidResult.Value, "GUID should be preserved after rename");
@@ -197,7 +197,7 @@ public class AssetDatabaseIntegrationTest
await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken); await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
var guidResult = AssetDatabase.PathToGuid(filePath); var guidResult = AssetService.PathToGuid(filePath);
Assert.IsTrue(guidResult.IsSuccess); Assert.IsTrue(guidResult.IsSuccess);
var guid = guidResult.Value; var guid = guidResult.Value;
@@ -211,7 +211,7 @@ public class AssetDatabaseIntegrationTest
Assert.IsFalse(File.Exists(metaPath), "Meta file should be deleted with asset"); Assert.IsFalse(File.Exists(metaPath), "Meta file should be deleted with asset");
// Asset should be removed from database // Asset should be removed from database
var pathResult = AssetDatabase.GuidToPath(guid); var pathResult = AssetService.GuidToPath(guid);
Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database"); Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database");
CheckInternalErrors(); CheckInternalErrors();
@@ -223,7 +223,7 @@ public class AssetDatabaseIntegrationTest
var filePath = Path.Combine(_testAssetsDir, "apiCreated.txt"); var filePath = Path.Combine(_testAssetsDir, "apiCreated.txt");
// Create via API // Create via API
var result = await AssetDatabase.CreateAssetAsync(filePath, TestContext.CancellationToken); var result = await AssetService.CreateAssetAsync(filePath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, "Should create asset successfully"); Assert.IsTrue(result.IsSuccess, "Should create asset successfully");
// File and meta should exist // File and meta should exist
@@ -231,7 +231,7 @@ public class AssetDatabaseIntegrationTest
Assert.IsTrue(File.Exists(filePath + ".gmeta"), "Meta file should exist"); Assert.IsTrue(File.Exists(filePath + ".gmeta"), "Meta file should exist");
// Should be in database // Should be in database
var guidResult = AssetDatabase.PathToGuid(filePath); var guidResult = AssetService.PathToGuid(filePath);
Assert.IsTrue(guidResult.IsSuccess, "Asset should be in database"); Assert.IsTrue(guidResult.IsSuccess, "Asset should be in database");
CheckInternalErrors(); CheckInternalErrors();
@@ -245,7 +245,7 @@ public class AssetDatabaseIntegrationTest
await File.WriteAllTextAsync(sourcePath, "data", TestContext.CancellationToken); await File.WriteAllTextAsync(sourcePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
var guid = AssetDatabase.PathToGuid(sourcePath).Value; var guid = AssetService.PathToGuid(sourcePath).Value;
// Create subdirectory // Create subdirectory
var subDir = Path.Combine(_testAssetsDir, "SubFolder"); var subDir = Path.Combine(_testAssetsDir, "SubFolder");
@@ -254,7 +254,7 @@ public class AssetDatabaseIntegrationTest
var destPath = Path.Combine(subDir, "source.txt"); var destPath = Path.Combine(subDir, "source.txt");
// Move via API // Move via API
var result = await AssetDatabase.MoveAssetAsync(sourcePath, destPath, TestContext.CancellationToken); var result = await AssetService.MoveAssetAsync(sourcePath, destPath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, $"Should move asset successfully. Error: {result.Message}"); Assert.IsTrue(result.IsSuccess, $"Should move asset successfully. Error: {result.Message}");
// Old file should not exist // Old file should not exist
@@ -266,7 +266,7 @@ public class AssetDatabaseIntegrationTest
Assert.IsTrue(File.Exists(destPath + ".gmeta"), "Destination meta should exist"); Assert.IsTrue(File.Exists(destPath + ".gmeta"), "Destination meta should exist");
// GUID should be preserved // GUID should be preserved
var newGuid = AssetDatabase.PathToGuid(destPath).Value; var newGuid = AssetService.PathToGuid(destPath).Value;
Assert.AreEqual(guid, newGuid, "GUID should be preserved"); Assert.AreEqual(guid, newGuid, "GUID should be preserved");
CheckInternalErrors(); CheckInternalErrors();
@@ -280,11 +280,11 @@ public class AssetDatabaseIntegrationTest
await File.WriteAllTextAsync(sourcePath, "data", TestContext.CancellationToken); await File.WriteAllTextAsync(sourcePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
var sourceGuid = AssetDatabase.PathToGuid(sourcePath).Value; var sourceGuid = AssetService.PathToGuid(sourcePath).Value;
var destPath = Path.Combine(_testAssetsDir, "copied.txt"); var destPath = Path.Combine(_testAssetsDir, "copied.txt");
// Copy via API // Copy via API
var result = await AssetDatabase.CopyAssetAsync(sourcePath, destPath, TestContext.CancellationToken); var result = await AssetService.CopyAssetAsync(sourcePath, destPath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, "Should copy asset successfully"); Assert.IsTrue(result.IsSuccess, "Should copy asset successfully");
// Both files should exist // Both files should exist
@@ -292,7 +292,7 @@ public class AssetDatabaseIntegrationTest
Assert.IsTrue(File.Exists(destPath), "Destination file should exist"); Assert.IsTrue(File.Exists(destPath), "Destination file should exist");
// Both should have different GUIDs // Both should have different GUIDs
var destGuid = AssetDatabase.PathToGuid(destPath).Value; var destGuid = AssetService.PathToGuid(destPath).Value;
Assert.AreNotEqual(sourceGuid, destGuid, "Copied asset should have different GUID"); Assert.AreNotEqual(sourceGuid, destGuid, "Copied asset should have different GUID");
CheckInternalErrors(); CheckInternalErrors();
@@ -306,10 +306,10 @@ public class AssetDatabaseIntegrationTest
await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken); await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
var guid = AssetDatabase.PathToGuid(filePath).Value; var guid = AssetService.PathToGuid(filePath).Value;
// Delete via API // Delete via API
var result = await AssetDatabase.DeleteAssetAsync(filePath, TestContext.CancellationToken); var result = await AssetService.DeleteAssetAsync(filePath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, "Should delete asset successfully"); Assert.IsTrue(result.IsSuccess, "Should delete asset successfully");
// File and meta should not exist // File and meta should not exist
@@ -317,7 +317,7 @@ public class AssetDatabaseIntegrationTest
Assert.IsFalse(File.Exists(filePath + ".gmeta"), "Meta should be deleted"); Assert.IsFalse(File.Exists(filePath + ".gmeta"), "Meta should be deleted");
// Should be removed from database // Should be removed from database
var pathResult = AssetDatabase.GuidToPath(guid); var pathResult = AssetService.GuidToPath(guid);
Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database"); Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database");
CheckInternalErrors(); CheckInternalErrors();
@@ -374,18 +374,18 @@ public class AssetDatabaseIntegrationTest
await File.WriteAllTextAsync(file3, "data", TestContext.CancellationToken); await File.WriteAllTextAsync(file3, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
var guid1 = AssetDatabase.PathToGuid(file1).Value; var guid1 = AssetService.PathToGuid(file1).Value;
var guid2 = AssetDatabase.PathToGuid(file2).Value; var guid2 = AssetService.PathToGuid(file2).Value;
// Add tags // Add tags
await AssetDatabase.SetAssetTagsAsync(guid1, new List<string> { "Test", "Player" }, TestContext.CancellationToken); await AssetService.SetAssetTagsAsync(guid1, new List<string> { "Test", "Player" }, TestContext.CancellationToken);
await AssetDatabase.SetAssetTagsAsync(guid2, new List<string> { "Test", "Enemy" }, TestContext.CancellationToken); await AssetService.SetAssetTagsAsync(guid2, new List<string> { "Test", "Enemy" }, TestContext.CancellationToken);
// Search by tag // Search by tag
var testAssets = await AssetDatabase.FindAssetsByTagAsync("Test", TestContext.CancellationToken); var testAssets = await AssetService.FindAssetsByTagAsync("Test", TestContext.CancellationToken);
Assert.HasCount(2, testAssets, "Should find 2 assets with 'Test' tag"); Assert.HasCount(2, testAssets, "Should find 2 assets with 'Test' tag");
var playerAssets = await AssetDatabase.FindAssetsByTagAsync("Player", TestContext.CancellationToken); var playerAssets = await AssetService.FindAssetsByTagAsync("Player", TestContext.CancellationToken);
Assert.HasCount(1, playerAssets, "Should find 1 asset with 'Player' tag"); Assert.HasCount(1, playerAssets, "Should find 1 asset with 'Player' tag");
CheckInternalErrors(); CheckInternalErrors();
@@ -399,15 +399,15 @@ public class AssetDatabaseIntegrationTest
await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken); await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents(); await WaitForFileSystemEvents();
var guid1 = AssetDatabase.PathToGuid(filePath).Value; var guid1 = AssetService.PathToGuid(filePath).Value;
// Call RefreshAsync multiple times // Call RefreshAsync multiple times
await AssetDatabase.RefreshAsync(TestContext.CancellationToken); await AssetService.RefreshAsync(TestContext.CancellationToken);
await AssetDatabase.RefreshAsync(TestContext.CancellationToken); await AssetService.RefreshAsync(TestContext.CancellationToken);
await AssetDatabase.RefreshAsync(TestContext.CancellationToken); await AssetService.RefreshAsync(TestContext.CancellationToken);
// GUID should remain the same // GUID should remain the same
var guid2 = AssetDatabase.PathToGuid(filePath).Value; var guid2 = AssetService.PathToGuid(filePath).Value;
Assert.AreEqual(guid1, guid2, "GUID should not change after refresh"); Assert.AreEqual(guid1, guid2, "GUID should not change after refresh");
// Only one meta file should exist // Only one meta file should exist
@@ -424,7 +424,7 @@ public class AssetDatabaseIntegrationTest
{ {
var testFile = Path.Combine(_testAssetsDir, "test.txt"); var testFile = Path.Combine(_testAssetsDir, "test.txt");
await File.WriteAllTextAsync(testFile, "Hello World", TestContext.CancellationToken); await File.WriteAllTextAsync(testFile, "Hello World", TestContext.CancellationToken);
await AssetDatabase.RefreshAsync(TestContext.CancellationToken); // This will cause race conditions if not handle properly because both AssetDatabase and FileSystemWatcher are involved await AssetService.RefreshAsync(TestContext.CancellationToken); // This will cause race conditions if not handle properly because both AssetService and FileSystemWatcher are involved
} }
catch (Exception ex) catch (Exception ex)
{ {