forked from Misaki/GhostEngine
Refactor project structure and improve performance
Changed the `ProjectRepository` class to be static for easier usage. Changed `ProjectService` constants to public properties for accessibility. Changed `App.xaml` to consolidate theme resources into `Override.xaml`. Changed `App.xaml.cs` to implement an `AppStateMachine` for better state management. Changed `ConsolePage` and `HierarchyPage` to utilize the new ViewModel structure. Changed `ProjectPage` to use the `ExplorerItem` model for asset display. Changed `Entity` and `EntityManager` to enhance component management with a new `IComponentData` interface. Changed the `Logger` class to introduce structured logging functionality. Changed the system architecture to support dependency management for better organization. Changed the `QueryEnumerable` class to allow for more flexible entity queries. Changed the `TypeHandle` class to improve efficiency in retrieving type handles. Changed the `World` class to support robust world management and multiple worlds. Updated the `Test` class to demonstrate the new entity and component management system.
This commit is contained in:
@@ -9,10 +9,8 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<XamlControlsResources Source="/Controls/EditorControls.xaml" />
|
||||
<ResourceDictionary Source="/Themes/Dark.xaml" />
|
||||
<ResourceDictionary Source="/Themes/Light.xaml" />
|
||||
<ResourceDictionary Source="/Themes/Override.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Ghost.Editor.AppStates;
|
||||
using Ghost.Editor.Helpers;
|
||||
using Ghost.Editor.Helpers;
|
||||
using Ghost.Editor.Infrastructures.AppState;
|
||||
using Ghost.Editor.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -20,7 +20,7 @@ namespace Ghost.Editor
|
||||
|
||||
internal static Window? Window
|
||||
{
|
||||
get => (Current as App)?._window;
|
||||
get => (Current as App)!._window;
|
||||
set
|
||||
{
|
||||
if (Current is App app)
|
||||
@@ -48,16 +48,10 @@ namespace Ghost.Editor
|
||||
UseContentRoot(AppContext.BaseDirectory).
|
||||
ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
return new AppStateService(
|
||||
new LandingState(),
|
||||
new EditorState());
|
||||
});
|
||||
|
||||
HostHelper.AddLandingScope(context, services);
|
||||
HostHelper.AddEngineScope(context, services);
|
||||
|
||||
services.AddSingleton<AppStateMachine>();
|
||||
services.AddSingleton<StackedNotificationService>();
|
||||
})
|
||||
.Build();
|
||||
@@ -92,7 +86,11 @@ namespace Ghost.Editor
|
||||
|
||||
Host.Start();
|
||||
|
||||
await GetService<AppStateService>().TransitionToAsync(StateKey.Landing);
|
||||
var stateMachine = GetService<AppStateMachine>();
|
||||
stateMachine.RegisterState(StateKey.Landing, () => new LandingState());
|
||||
stateMachine.RegisterState(StateKey.EngineEditor, () => new EditorState());
|
||||
|
||||
await stateMachine.TransitionToAsync(StateKey.Landing);
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
|
||||
38
Ghost.Editor/Controls/ViewModelPage.cs
Normal file
38
Ghost.Editor/Controls/ViewModelPage.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Contracts;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Ghost.Editor.Controls;
|
||||
|
||||
public abstract partial class ViewModelPage<VM> : Page
|
||||
where VM : ObservableObject
|
||||
{
|
||||
public VM ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
protected ViewModelPage(VM viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
if (ViewModel is INavigationAware navigationAware)
|
||||
{
|
||||
navigationAware.OnNavigatedTo(e.Parameter);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedFrom(e);
|
||||
if (ViewModel is INavigationAware navigationAware)
|
||||
{
|
||||
navigationAware.OnNavigatedFrom();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,8 @@
|
||||
<None Remove="Controls\EditorControls.xaml" />
|
||||
<None Remove="Controls\Internal\InspectorView.xaml" />
|
||||
<None Remove="Controls\Internal\InternalControls.xaml" />
|
||||
<None Remove="Themes\Dark.xaml" />
|
||||
<None Remove="Themes\Light.xaml" />
|
||||
<None Remove="View\Pages\EngineEditor\ConsolePage.xaml" />
|
||||
<None Remove="View\Pages\EngineEditor\HierarchyPage.xaml" />
|
||||
<None Remove="View\Pages\EngineEditor\ProjectPage.xaml" />
|
||||
<None Remove="View\Pages\Landing\CreateProjectPage.xaml" />
|
||||
<None Remove="View\Pages\Landing\OpenProjectPage.xaml" />
|
||||
@@ -105,7 +104,7 @@
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\Converters\" />
|
||||
<Folder Include="AppStates\" />
|
||||
<Folder Include="Resources\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -113,6 +112,11 @@
|
||||
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\EngineEditor\HierarchyPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\EngineEditor\ProjectPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -124,12 +128,7 @@
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Themes\Light.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Themes\Dark.xaml">
|
||||
<Page Update="Themes\Override.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
40
Ghost.Editor/Helpers/Converters/AssetPathToGlyphConverter.cs
Normal file
40
Ghost.Editor/Helpers/Converters/AssetPathToGlyphConverter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ghost.Editor.Helpers.Converters;
|
||||
|
||||
public partial class AssetPathToGlyphConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is not string path)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
return "\uE8B7";
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path).ToLowerInvariant();
|
||||
|
||||
// TODO: Use resource dictionary for icons.
|
||||
return extension switch
|
||||
{
|
||||
".fbx" or ".obj" => "\uF158",
|
||||
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" => "\uE91B", // Image icon
|
||||
".mp3" or ".wav" or ".ogg" => "\uE767", // Audio icon
|
||||
".mp4" or ".avi" or ".mkv" => "\uE714", // Video icon
|
||||
".txt" or ".md" => "\uF000", // Text file icon
|
||||
".cs" or ".hlsl" => "\uE943", // Code file icon
|
||||
_ => "\uE8A5", // Default file icon
|
||||
};
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.View.Pages.EngineEditor;
|
||||
using Ghost.Editor.View.Pages.Landing;
|
||||
using Ghost.Editor.View.Windows;
|
||||
using Ghost.Editor.ViewModel.Pages.Landing;
|
||||
using Ghost.Editor.ViewModel.Windows;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Ghost.Editor.ViewModels.Pages.Landing;
|
||||
using Ghost.Editor.ViewModels.Windows;
|
||||
using Ghost.Engine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
@@ -12,19 +15,31 @@ internal static partial class HostHelper
|
||||
{
|
||||
public static void AddLandingScope(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<LandingWindow>();
|
||||
services.AddTransient<LandingWindow>();
|
||||
|
||||
services.AddTransient<CreateProjectPage>();
|
||||
services.AddTransient<CreateProjectViewModel>();
|
||||
|
||||
services.AddTransient<OpenProjectPage>();
|
||||
services.AddTransient<OpenProjectViewModel>();
|
||||
|
||||
services.AddTransient<ProjectService>();
|
||||
}
|
||||
|
||||
public static void AddEngineScope(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<EngineEditorWindow>();
|
||||
services.AddSingleton<EngineEditorViewModel>();
|
||||
services.AddSingleton<EngineCore>();
|
||||
|
||||
services.AddTransient<EngineEditorWindow>();
|
||||
services.AddTransient<EngineEditorViewModel>();
|
||||
|
||||
services.AddTransient<HierarchyPage>();
|
||||
services.AddTransient<HierarchyViewModel>();
|
||||
|
||||
services.AddTransient<ProjectPage>();
|
||||
services.AddTransient<ProjectViewModel>();
|
||||
|
||||
services.AddTransient<ConsolePage>();
|
||||
services.AddTransient<ConsoleViewModel>();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
using Ghost.Editor.AppStates;
|
||||
using Ghost.Editor.Contracts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ghost.Editor.Services;
|
||||
namespace Ghost.Editor.Infrastructures.AppState;
|
||||
|
||||
internal class AppStateService(params IEnumerable<IAppState<StateKey>> states)
|
||||
internal class AppStateMachine
|
||||
{
|
||||
private readonly Dictionary<StateKey, IAppState<StateKey>> _states = states.ToDictionary(s => s.StateKy, s => s);
|
||||
private IAppState<StateKey>? _current;
|
||||
private Dictionary<StateKey, Lazy<IAppState>> s_states = new();
|
||||
private IAppState? s_current;
|
||||
|
||||
public void RegisterState(StateKey key, Func<IAppState> stateFactory)
|
||||
{
|
||||
s_states[key] = new(stateFactory);
|
||||
}
|
||||
|
||||
public async Task TransitionToAsync(StateKey stateKey, object? parameter = null)
|
||||
{
|
||||
var previous = _current;
|
||||
var next = _states[stateKey];
|
||||
var previous = s_current;
|
||||
var next = s_states[stateKey].Value;
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
@@ -30,6 +33,6 @@ internal class AppStateService(params IEnumerable<IAppState<StateKey>> states)
|
||||
|
||||
await next.OnEnteredAsync(parameter);
|
||||
|
||||
_current = next;
|
||||
s_current = next;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Editor.Contracts;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.View.Windows;
|
||||
using Ghost.Engine;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ghost.Editor.AppStates;
|
||||
namespace Ghost.Editor.Infrastructures.AppState;
|
||||
|
||||
internal class EditorState : IAppState<StateKey>
|
||||
internal class EditorState : IAppState
|
||||
{
|
||||
private EngineEditorWindow? _window;
|
||||
|
||||
public StateKey StateKy => StateKey.EngineEditor;
|
||||
private EngineCore? _engineCore;
|
||||
|
||||
public Task OnExitingAsync()
|
||||
{
|
||||
@@ -20,23 +20,31 @@ internal class EditorState : IAppState<StateKey>
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnEnteringAsync(object? parameter)
|
||||
public async Task OnEnteringAsync(object? parameter)
|
||||
{
|
||||
if (parameter is not ProjectMetadata metadata)
|
||||
if (parameter is not ProjectMetadataInfo metadataInfo)
|
||||
{
|
||||
throw new System.ArgumentException("Parameter must be of type ProjectMetadata.", nameof(parameter));
|
||||
}
|
||||
|
||||
ProjectService.CurrentProject = metadataInfo;
|
||||
|
||||
_engineCore = App.GetService<EngineCore>();
|
||||
await _engineCore.StartAsync(new Engine.Models.LaunchArgument());
|
||||
|
||||
_window = App.GetService<EngineEditorWindow>();
|
||||
_window.ViewModel.CurrentProject = metadata;
|
||||
_window.Activate();
|
||||
|
||||
App.Window = _window;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnExitedAsync()
|
||||
public async Task OnExitedAsync()
|
||||
{
|
||||
if (_engineCore != null)
|
||||
{
|
||||
await _engineCore.ShutDownAsync();
|
||||
}
|
||||
|
||||
if (App.Window == _window)
|
||||
{
|
||||
App.Window = null;
|
||||
@@ -44,7 +52,6 @@ internal class EditorState : IAppState<StateKey>
|
||||
|
||||
_window?.Close();
|
||||
_window = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnEnteredAsync(object? parameter)
|
||||
@@ -1,14 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ghost.Editor.Contracts;
|
||||
namespace Ghost.Editor.Infrastructures.AppState;
|
||||
|
||||
internal interface IAppState<Key>
|
||||
internal interface IAppState
|
||||
{
|
||||
public Key StateKy
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when exiting the state.
|
||||
/// </summary>
|
||||
@@ -1,15 +1,12 @@
|
||||
using Ghost.Editor.Contracts;
|
||||
using Ghost.Editor.View.Windows;
|
||||
using Ghost.Editor.View.Windows;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ghost.Editor.AppStates;
|
||||
namespace Ghost.Editor.Infrastructures.AppState;
|
||||
|
||||
internal class LandingState : IAppState<StateKey>
|
||||
internal class LandingState : IAppState
|
||||
{
|
||||
private LandingWindow? _window;
|
||||
|
||||
public StateKey StateKy => StateKey.Landing;
|
||||
|
||||
public Task OnExitingAsync()
|
||||
{
|
||||
if (App.Window == _window)
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ghost.Editor.AppStates;
|
||||
namespace Ghost.Editor.Infrastructures.AppState;
|
||||
|
||||
internal enum StateKey
|
||||
{
|
||||
17
Ghost.Editor/Infrastructures/SceneGraph/EntityNode.cs
Normal file
17
Ghost.Editor/Infrastructures/SceneGraph/EntityNode.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Ghost.Entities;
|
||||
|
||||
namespace Ghost.Editor.Infrastructures.SceneGraph;
|
||||
|
||||
public partial class EntityNode : SceneGraphNode
|
||||
{
|
||||
private readonly Entity _entity;
|
||||
|
||||
public Entity Entity => _entity;
|
||||
public override NodeType Type => NodeType.Entity;
|
||||
|
||||
public EntityNode(Entity entity, string name)
|
||||
{
|
||||
_entity = entity;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
112
Ghost.Editor/Infrastructures/SceneGraph/SceneGraphHelpers.cs
Normal file
112
Ghost.Editor/Infrastructures/SceneGraph/SceneGraphHelpers.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using Ghost.Engine.Components;
|
||||
using Ghost.Entities;
|
||||
|
||||
namespace Ghost.Editor.Infrastructures.SceneGraph;
|
||||
|
||||
internal class SceneGraphHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="EntityNode"/> entity with default components.
|
||||
/// </summary>
|
||||
/// <param name="world">The world context where the entity will be created.</param>
|
||||
/// <param name="entity">The entity to be wrapped in the <see cref="EntityNode"/>.</param>
|
||||
public static EntityNode CreateEntityNode(World world, Entity entity, string name)
|
||||
{
|
||||
world.EntityManager.AddComponent(entity, LocalToWorld.Identity);
|
||||
world.EntityManager.AddComponent(entity, Hierarchy.Root);
|
||||
return new EntityNode(entity, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Entity"/> and <see cref="EntityNode"/> entity with default components.
|
||||
/// </summary>
|
||||
/// <param name="world">The world context where the entity will be created.</param>
|
||||
public static EntityNode CreateEntityNode(World world, string name)
|
||||
{
|
||||
var entity = world.EntityManager.CreateEntity();
|
||||
return CreateEntityNode(world, entity, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches childEntity to parentEntity in the scene graph.
|
||||
/// </summary>
|
||||
/// <param name="world">The world context where the entities exist.</param>
|
||||
/// <param name="parentNode">The parent entity to which the child will be attached.</param>
|
||||
/// <param name="childNode">The child entity to be attached.</param>
|
||||
public static void AttachChild(SceneNode scene, EntityNode parentNode, EntityNode childNode)
|
||||
{
|
||||
// 1) If the child already has a parent, detach it first
|
||||
var childHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(childNode.Entity);
|
||||
if (childHierarchy.ValueRO.parent != Entity.Invalid)
|
||||
{
|
||||
DetachFromParent(scene, childNode);
|
||||
}
|
||||
|
||||
// 2) Link child to new parent
|
||||
childHierarchy.ValueRW.parent = parentNode.Entity;
|
||||
|
||||
// 3) Insert child at the head of parent's child list
|
||||
var parentHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(parentNode.Entity);
|
||||
|
||||
childHierarchy.ValueRW.nextSibling = parentHierarchy.ValueRO.firstChild;
|
||||
parentHierarchy.ValueRW.firstChild = childNode.Entity;
|
||||
|
||||
// 4) Write back
|
||||
scene.World.EntityManager.SetComponent(parentNode.Entity, in parentHierarchy.ValueRO);
|
||||
scene.World.EntityManager.SetComponent(childNode.Entity, in childHierarchy.ValueRO);
|
||||
|
||||
// 5) Update children list in parent node
|
||||
parentNode.Children.Add(childNode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches the specified entity from its parent in the scene graph.
|
||||
/// </summary>
|
||||
/// <param name="world">The world context where the entities exist.</param>
|
||||
/// <param name="node">The entity to detach from its parent.</param>
|
||||
public static void DetachFromParent(SceneNode scene, EntityNode node)
|
||||
{
|
||||
var hierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(node.Entity);
|
||||
var parent = hierarchy.ValueRO.parent;
|
||||
if (parent == Entity.Invalid)
|
||||
{
|
||||
return; // already root
|
||||
}
|
||||
|
||||
var parentHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(parent);
|
||||
|
||||
// If entity is the first child, simply move head
|
||||
if (parentHierarchy.ValueRO.firstChild == node.Entity)
|
||||
{
|
||||
parentHierarchy.ValueRW.firstChild = hierarchy.ValueRO.nextSibling;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, find the previous sibling in the linked list
|
||||
var prevSibling = parentHierarchy.ValueRO.firstChild;
|
||||
while (prevSibling != Entity.Invalid)
|
||||
{
|
||||
var prevHierarchy = scene.World.EntityManager.GetComponent<Hierarchy>(prevSibling);
|
||||
if (prevHierarchy.ValueRW.nextSibling == node.Entity)
|
||||
{
|
||||
prevHierarchy.ValueRW.nextSibling = hierarchy.ValueRO.nextSibling;
|
||||
scene.World.EntityManager.SetComponent(prevSibling, in prevHierarchy.ValueRO);
|
||||
break;
|
||||
}
|
||||
|
||||
prevSibling = prevHierarchy.ValueRO.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear child's references
|
||||
hierarchy.ValueRW.parent = Entity.Invalid;
|
||||
hierarchy.ValueRW.nextSibling = Entity.Invalid;
|
||||
|
||||
// Write back
|
||||
scene.World.EntityManager.SetComponent(parent, in parentHierarchy.ValueRO);
|
||||
scene.World.EntityManager.SetComponent(node.Entity, in hierarchy.ValueRO);
|
||||
|
||||
// Remove from parent's children list
|
||||
scene.EntityNodeLookup[parent].Children.Remove(node);
|
||||
}
|
||||
}
|
||||
32
Ghost.Editor/Infrastructures/SceneGraph/SceneGraphNode.cs
Normal file
32
Ghost.Editor/Infrastructures/SceneGraph/SceneGraphNode.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.Infrastructures.SceneGraph;
|
||||
|
||||
public abstract partial class SceneGraphNode : ObservableObject
|
||||
{
|
||||
public enum NodeType
|
||||
{
|
||||
Scene,
|
||||
Entity,
|
||||
}
|
||||
|
||||
public abstract NodeType Type
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// Will the new collection allocated if ui bind to this property?
|
||||
private ObservableCollection<EntityNode>? _children;
|
||||
public ObservableCollection<EntityNode> Children
|
||||
{
|
||||
get => _children ??= new();
|
||||
}
|
||||
}
|
||||
58
Ghost.Editor/Infrastructures/SceneGraph/SceneNode.cs
Normal file
58
Ghost.Editor/Infrastructures/SceneGraph/SceneNode.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Ghost.Engine.Components;
|
||||
using Ghost.Entities;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ghost.Editor.Infrastructures.SceneGraph;
|
||||
|
||||
public partial class SceneNode : SceneGraphNode
|
||||
{
|
||||
private readonly World _world;
|
||||
private Dictionary<Entity, EntityNode> _entityNodeLookup = new();
|
||||
|
||||
public World World => _world;
|
||||
public Dictionary<Entity, EntityNode> EntityNodeLookup => _entityNodeLookup;
|
||||
|
||||
public override NodeType Type => NodeType.Scene;
|
||||
|
||||
public SceneNode(World world, string name)
|
||||
{
|
||||
_world = world;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
private EntityNode BuildNodeRecursive(Entity entity, World world)
|
||||
{
|
||||
// TODO: Node serialization.
|
||||
var node = new EntityNode(entity, "New Entity");
|
||||
_entityNodeLookup[entity] = node;
|
||||
|
||||
var hc = world.EntityManager.GetComponent<Hierarchy>(entity);
|
||||
var child = hc.ValueRO.firstChild;
|
||||
|
||||
while (child != Entity.Invalid)
|
||||
{
|
||||
node.Children.Add(BuildNodeRecursive(child, world));
|
||||
var childHC = world.EntityManager.GetComponent<Hierarchy>(child);
|
||||
child = childHC.ValueRO.nextSibling;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private void BuildGraph()
|
||||
{
|
||||
foreach (var (entity, hierarchy) in _world.Query<Hierarchy>())
|
||||
{
|
||||
if (hierarchy.ValueRO.parent == Entity.Invalid)
|
||||
{
|
||||
var node = BuildNodeRecursive(entity, _world);
|
||||
Children.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
BuildGraph();
|
||||
}
|
||||
}
|
||||
19
Ghost.Editor/Models/AssetItem.cs
Normal file
19
Ghost.Editor/Models/AssetItem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Ghost.Editor.Models;
|
||||
|
||||
internal struct AssetItem()
|
||||
{
|
||||
public string AssetPath
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public string AssetName
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public string IconGlyph
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
}
|
||||
27
Ghost.Editor/Models/ExplorerItem.cs
Normal file
27
Ghost.Editor/Models/ExplorerItem.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.Models;
|
||||
|
||||
internal class ExplorerItem(string name, string path, bool isDirectory)
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
} = name;
|
||||
|
||||
public string Path
|
||||
{
|
||||
get;
|
||||
} = path;
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get;
|
||||
} = isDirectory;
|
||||
|
||||
public ObservableCollection<ExplorerItem>? Children
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Entities;
|
||||
using Ghost.Entities.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.Models;
|
||||
|
||||
public partial class GameObject : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool IsActive
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsActiveHierarchy
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Entity Entity
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public Scene Scene
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public GameObject? Parent
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<IComponentData>? Components
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial IEnumerable<ScriptComponent>? ScriptComponents
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<GameObject>? Children
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public GameObject(Scene scene, string name)
|
||||
{
|
||||
Entity = scene.World.EntityManager.CreateEntity();
|
||||
Scene = scene;
|
||||
Name = name;
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
partial void OnIsActiveChanged(bool value)
|
||||
{
|
||||
IsActiveHierarchy = value && (Parent?.IsActiveHierarchy ?? true);
|
||||
HandleActiveStateChanged();
|
||||
|
||||
if (Children != null)
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.IsActiveHierarchy = value && IsActiveHierarchy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnIsActiveHierarchyChanged(bool value)
|
||||
{
|
||||
HandleActiveStateChanged();
|
||||
}
|
||||
|
||||
private void HandleActiveStateChanged()
|
||||
{
|
||||
if (IsActive && IsActiveHierarchy)
|
||||
{
|
||||
OnEnable();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDisable();
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnEnable()
|
||||
{
|
||||
if (ScriptComponents != null)
|
||||
{
|
||||
foreach (var script in ScriptComponents)
|
||||
{
|
||||
if (!script.Enable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
script.OnEnable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnDisable()
|
||||
{
|
||||
if (ScriptComponents != null)
|
||||
{
|
||||
foreach (var script in ScriptComponents)
|
||||
{
|
||||
if (!script.Enable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
script.OnDisable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddChild(GameObject child)
|
||||
{
|
||||
if (child.Scene != Scene)
|
||||
{
|
||||
throw new InvalidOperationException("Child GameObject must belong to the same Scene.");
|
||||
}
|
||||
|
||||
Children ??= new();
|
||||
Children.Add(child);
|
||||
child.Parent = this;
|
||||
}
|
||||
|
||||
public bool RemoveChild(GameObject child)
|
||||
{
|
||||
if (Children is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Children.Remove(child))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
child.Parent = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (ScriptComponents != null)
|
||||
{
|
||||
foreach (var component in ScriptComponents)
|
||||
{
|
||||
if (!component.Enable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
component.OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
if (Children != null)
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Destroy();
|
||||
}
|
||||
|
||||
Children.Clear();
|
||||
}
|
||||
|
||||
Parent?.Children?.Remove(this);
|
||||
Entity.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class GameObject
|
||||
{
|
||||
// TODO: Implement a more efficient synchronization mechanism for components
|
||||
internal void SyncComponents()
|
||||
{
|
||||
foreach (var (typeHandle, mask) in Scene.World.ComponentStorage.ComponentEntityMasks)
|
||||
{
|
||||
if (!mask.IsSet(Entity.ID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pool = Scene.World.ComponentStorage.ComponentPools[typeHandle];
|
||||
}
|
||||
}
|
||||
|
||||
internal void SyncScripts()
|
||||
{
|
||||
var scriptsPool = Scene.World.ComponentStorage.ScriptComponentPool.ScriptComponents;
|
||||
if (scriptsPool == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
scriptsPool.TryGetValue(Entity, out var scripts);
|
||||
ScriptComponents = scripts;
|
||||
}
|
||||
|
||||
public void AddComponent<T>(T component)
|
||||
where T : struct, IComponentData
|
||||
{
|
||||
Entity.AddComponent<T>(component);
|
||||
SyncComponents();
|
||||
}
|
||||
|
||||
public bool RemoveComponent<T>()
|
||||
where T : struct, IComponentData
|
||||
{
|
||||
var result = Entity.RemoveComponent<T>();
|
||||
SyncComponents();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void AddScript<T>()
|
||||
where T : ScriptComponent, new()
|
||||
{
|
||||
Entity.AddScript<T>();
|
||||
SyncScripts();
|
||||
}
|
||||
|
||||
public void AddScript(Type type)
|
||||
{
|
||||
Entity.AddScript(type);
|
||||
SyncScripts();
|
||||
}
|
||||
|
||||
public bool RemoveScript<T>()
|
||||
where T : ScriptComponent
|
||||
{
|
||||
var result = Scene.World.EntityManager.RemoveScript<T>(Entity);
|
||||
SyncScripts();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool RemoveScriptAt(int index)
|
||||
{
|
||||
var result = Scene.World.EntityManager.RemoveScriptAt(Entity, index);
|
||||
SyncScripts();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using Ghost.Entities;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ghost.Editor.Models;
|
||||
|
||||
public class Scene
|
||||
{
|
||||
private readonly HashSet<GameObject> _rootObjects = new();
|
||||
private readonly World _world = World.Create();
|
||||
|
||||
public IEnumerable<GameObject> RootObjects => _rootObjects;
|
||||
public World World => _world;
|
||||
|
||||
internal Scene()
|
||||
{
|
||||
}
|
||||
|
||||
internal void Load()
|
||||
{
|
||||
foreach (var gameObject in _rootObjects)
|
||||
{
|
||||
gameObject.OnEnable();
|
||||
}
|
||||
}
|
||||
|
||||
internal void Unload()
|
||||
{
|
||||
foreach (var gameObject in _rootObjects)
|
||||
{
|
||||
gameObject.OnDisable();
|
||||
gameObject.Destroy();
|
||||
}
|
||||
|
||||
_rootObjects.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
14
Ghost.Editor/Themes/Override.xaml
Normal file
14
Ghost.Editor/Themes/Override.xaml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.UI.Xaml.Controls">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -21,19 +21,19 @@
|
||||
BorderThickness="0,0,0,1">
|
||||
<CommandBar Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" DefaultLabelPosition="Collapsed">
|
||||
<CommandBar.PrimaryCommands>
|
||||
<AppBarButton Content="Clear" />
|
||||
<AppBarButton Command="{x:Bind ViewModel.ClearLogsCommand}" Content="Clear" />
|
||||
<AppBarSeparator />
|
||||
<AppBarToggleButton Width="45">
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowInfo, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
<AppBarToggleButton Width="45">
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowWarning, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
<AppBarToggleButton Width="45">
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowError, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
@@ -42,7 +42,10 @@
|
||||
|
||||
<CommandBar.SecondaryCommands>
|
||||
<AppBarToggleButton BorderThickness="0" Label="Clear On Play" />
|
||||
<AppBarToggleButton BorderThickness="0" Label="Show Stack Trace" />
|
||||
<AppBarToggleButton
|
||||
BorderThickness="0"
|
||||
IsChecked="{x:Bind ViewModel.ShowStackTrace, Mode=TwoWay}"
|
||||
Label="Show Stack Trace" />
|
||||
</CommandBar.SecondaryCommands>
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
@@ -54,7 +57,11 @@
|
||||
<RowDefinition Height="100" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ListView Grid.Row="0" />
|
||||
<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"
|
||||
@@ -64,7 +71,7 @@
|
||||
<TextBlock
|
||||
IsTextSelectionEnabled="True"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="Test Log"
|
||||
Text="{x:Bind ViewModel.SelectedLog.ToStringWithStackTrace(), Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
|
||||
public sealed partial class ConsolePage : Page
|
||||
internal sealed partial class ConsolePage : Page
|
||||
{
|
||||
public ConsoleViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ConsolePage()
|
||||
{
|
||||
ViewModel = App.GetService<ConsoleViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
||||
43
Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml
Normal file
43
Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Ghost.Editor.View.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:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:sg="using:Ghost.Editor.Infrastructures.SceneGraph"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<DataTemplate x:Key="SceneTemplate" x:DataType="sg:SceneGraphNode">
|
||||
<TreeViewItem
|
||||
AutomationProperties.Name="{x:Bind Name}"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
IsExpanded="True"
|
||||
ItemsSource="{x:Bind Children}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock Margin="10,0" Text="{x:Bind Name}" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="EntityTemplate" x:DataType="sg:SceneGraphNode">
|
||||
<TreeViewItem AutomationProperties.Name="{x:Bind Name}" ItemsSource="{x:Bind Children}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock Margin="10,0" Text="{x:Bind Name}" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<TreeView ItemsSource="{x:Bind ViewModel.SceneList}">
|
||||
<TreeView.ItemTemplateSelector>
|
||||
<local:HierarchyTemplateSector EntityTemplate="{StaticResource EntityTemplate}" SceneTemplate="{StaticResource SceneTemplate}" />
|
||||
</TreeView.ItemTemplateSelector>
|
||||
</TreeView>
|
||||
</Grid>
|
||||
</Page>
|
||||
57
Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml.cs
Normal file
57
Ghost.Editor/View/Pages/EngineEditor/HierarchyPage.xaml.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Ghost.Editor.Infrastructures.SceneGraph;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
internal sealed partial class HierarchyPage : Page
|
||||
{
|
||||
public HierarchyViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public HierarchyPage()
|
||||
{
|
||||
ViewModel = App.GetService<HierarchyViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class HierarchyTemplateSector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? SceneTemplate
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public DataTemplate? EntityTemplate
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
if (SceneTemplate == null || EntityTemplate == null)
|
||||
{
|
||||
return base.SelectTemplateCore(item);
|
||||
}
|
||||
|
||||
var node = (SceneGraphNode)item;
|
||||
return node.Type switch
|
||||
{
|
||||
SceneGraphNode.NodeType.Scene => SceneTemplate,
|
||||
SceneGraphNode.NodeType.Entity => EntityTemplate,
|
||||
_ => base.SelectTemplateCore(item)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,17 @@
|
||||
x:Class="Ghost.Editor.View.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.Helpers.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Ghost.Editor.View.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" />
|
||||
@@ -17,55 +23,116 @@
|
||||
<!-- Folder Tree View -->
|
||||
<Grid
|
||||
Grid.Column="0"
|
||||
Padding="4"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<TreeView
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<TreeView.ItemContainerStyle>
|
||||
<Style TargetType="TreeViewItem">
|
||||
<Setter Property="Padding" Value="4,2" />
|
||||
<Setter Property="Margin" Value="0,0,0,2" />
|
||||
</Style>
|
||||
</TreeView.ItemContainerStyle>
|
||||
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="" />
|
||||
<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 -->
|
||||
<ScrollViewer
|
||||
Grid.Column="1"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<GridView HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<GridView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
|
||||
<Setter Property="Margin" Value="5,5,5,5" />
|
||||
</Style>
|
||||
</GridView.ItemContainerStyle>
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="0.2*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ImageIcon
|
||||
Grid.Row="0"
|
||||
Width="24"
|
||||
Height="24" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
</ScrollViewer>
|
||||
<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
|
||||
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 Path, 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.Path, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class ProjectPage : Page
|
||||
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.NavigateToSelected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ghost.Editor.ViewModel.Pages.Landing;
|
||||
using Ghost.Editor.ViewModels.Pages.Landing;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:converters="using:Ghost.Editor.Helpers.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Ghost.Data.Models"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
NavigationCacheMode="Enabled"
|
||||
@@ -73,7 +74,7 @@
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_ItemClick"
|
||||
ItemsSource="{x:Bind projects}"
|
||||
ItemsSource="{x:Bind ViewModel.projects}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:ProjectMetadataInfo">
|
||||
|
||||
@@ -1,68 +1,47 @@
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.AppStates;
|
||||
using Ghost.Editor.Services;
|
||||
using Ghost.Editor.ViewModels.Pages.Landing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.Landing;
|
||||
|
||||
internal sealed partial class OpenProjectPage : Page
|
||||
{
|
||||
private readonly ProjectService _projectService;
|
||||
private readonly StackedNotificationService _notificationService;
|
||||
private readonly AppStateService _stateService;
|
||||
|
||||
public readonly ObservableCollection<ProjectMetadataInfo> projects = new();
|
||||
public OpenProjectViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public OpenProjectPage()
|
||||
{
|
||||
_notificationService = App.GetService<StackedNotificationService>();
|
||||
_projectService = App.GetService<ProjectService>();
|
||||
_stateService = App.GetService<AppStateService>();
|
||||
ViewModel = App.GetService<OpenProjectViewModel>();
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void UpdateEmptyPlaceHolderVisibility()
|
||||
{
|
||||
EmptyPlaceHolder.Visibility = projects.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
ViewModel.OnNavigatedTo(e.Parameter);
|
||||
}
|
||||
|
||||
projects.Clear();
|
||||
await foreach (var projectInfo in _projectService.LoadAllProjectAsync())
|
||||
{
|
||||
var metadata = await ProjectService.LoadMetadataAsync(projectInfo.MetadataPath);
|
||||
if (metadata == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
projects.Add(new(projectInfo.MetadataPath, metadata));
|
||||
}
|
||||
|
||||
UpdateEmptyPlaceHolderVisibility();
|
||||
override protected void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedFrom(e);
|
||||
ViewModel.OnNavigatedFrom();
|
||||
}
|
||||
|
||||
private void ProjectContainer_DragEnter(object sender, DragEventArgs e)
|
||||
{
|
||||
DragVisual.Visibility = Visibility.Visible;
|
||||
EmptyPlaceHolder.Visibility = Visibility.Collapsed;
|
||||
ViewModel.DragVisibility = Visibility.Visible;
|
||||
ViewModel.EmptyVisibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ProjectContainer_DragLeave(object sender, DragEventArgs e)
|
||||
{
|
||||
DragVisual.Visibility = Visibility.Collapsed;
|
||||
UpdateEmptyPlaceHolderVisibility();
|
||||
ViewModel.DragVisibility = Visibility.Collapsed;
|
||||
ViewModel.UpdateEmptyPlaceHolderVisibility();
|
||||
}
|
||||
|
||||
private void ProjectContainer_DragOver(object sender, DragEventArgs e)
|
||||
@@ -79,55 +58,14 @@ internal sealed partial class OpenProjectPage : Page
|
||||
|
||||
private async void ProjectContainer_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
var errorMessage = string.Empty;
|
||||
if (e.DataView.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var items = await e.DataView.GetStorageItemsAsync();
|
||||
var rootFolder = items.OfType<StorageFolder>().FirstOrDefault();
|
||||
if (rootFolder != null)
|
||||
{
|
||||
var result = await _projectService.AddProjectFromDirectoryAsync(rootFolder.Path);
|
||||
if (result.success)
|
||||
{
|
||||
projects.Add(result.data);
|
||||
DragVisual.Visibility = Visibility.Collapsed;
|
||||
goto CloseDropPanel;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = result.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Unsupported data format. Please drop a folder containing a project.";
|
||||
}
|
||||
|
||||
_notificationService.ShowNotification(errorMessage, InfoBarSeverity.Error);
|
||||
|
||||
CloseDropPanel:
|
||||
DragVisual.Visibility = Visibility.Collapsed;
|
||||
UpdateEmptyPlaceHolderVisibility();
|
||||
await ViewModel.ContentDrop(e.DataView);
|
||||
}
|
||||
|
||||
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is not ProjectMetadataInfo project)
|
||||
if (e.ClickedItem is ProjectMetadataInfo project)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
project.Metadata.LastOpened = DateTime.Now;
|
||||
await ProjectService.CreateMetadataFileAsync(project.Path, project.Metadata);
|
||||
|
||||
await _stateService.TransitionToAsync(StateKey.EngineEditor, project.Metadata);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
_notificationService.ShowNotification($"Failed to load project: {exp.Message}", InfoBarSeverity.Error);
|
||||
await ViewModel.LoadProject(project);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.CurrentProject.Name, Mode=OneWay}" />
|
||||
Text="{x:Bind ViewModel.CurrentProject.Metadata.Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Toolbar -->
|
||||
@@ -83,13 +83,26 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid
|
||||
<TabView
|
||||
Grid.Column="0"
|
||||
Width="350"
|
||||
Background="Aquamarine" />
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
TabWidthMode="Compact">
|
||||
<TabView.TabItems>
|
||||
<TabViewItem Header="Hierarchy">
|
||||
<TabViewItem.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</TabViewItem.IconSource>
|
||||
<ee:HierarchyPage />
|
||||
</TabViewItem>
|
||||
</TabView.TabItems>
|
||||
</TabView>
|
||||
|
||||
<Grid Grid.Column="1">
|
||||
<Image Source="C:\Users\Misaki\OneDrive\Pictures\Screenshots\Screenshot 2024-07-20 021657.png" Stretch="UniformToFill" />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
Grid.Column="2"
|
||||
Width="350"
|
||||
@@ -103,7 +116,7 @@
|
||||
<TabView.TabItems>
|
||||
<TabViewItem Header="Project">
|
||||
<TabViewItem.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
<FontIconSource Glyph="" />
|
||||
</TabViewItem.IconSource>
|
||||
<ee:ProjectPage />
|
||||
</TabViewItem>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Ghost.Data.Resources;
|
||||
using Ghost.Editor.ViewModel.Windows;
|
||||
using Ghost.Editor.ViewModels.Windows;
|
||||
using Ghost.Engine.Resources;
|
||||
using WinUIEx;
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Ghost.Editor.ViewModel.Pages.EngineEditor;
|
||||
|
||||
internal class ProjectViewModel
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Ghost.Engine.Models;
|
||||
using Ghost.Engine.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class ConsoleViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<LogMessage> Logs
|
||||
{
|
||||
get; set;
|
||||
} = new();
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
public ConsoleViewModel()
|
||||
{
|
||||
foreach (var log in Logger.Logs)
|
||||
{
|
||||
Logs.Add(log);
|
||||
}
|
||||
|
||||
Logger.OnLogsUpdate += UpdateLogs;
|
||||
}
|
||||
|
||||
~ConsoleViewModel()
|
||||
{
|
||||
Logger.OnLogsUpdate -= UpdateLogs;
|
||||
}
|
||||
|
||||
private void UpdateLogs(LogChangeType updateType)
|
||||
{
|
||||
switch (updateType)
|
||||
{
|
||||
case LogChangeType.LogAdded:
|
||||
Logs.Add(Logger.Logs[^1]);
|
||||
break;
|
||||
case LogChangeType.LogRemoved:
|
||||
if (Logs.Count > 0)
|
||||
{
|
||||
Logs.RemoveAt(0);
|
||||
}
|
||||
break;
|
||||
case LogChangeType.LogsCleared:
|
||||
Logs.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnShowStackTraceChanged(bool value)
|
||||
{
|
||||
Logger.HasStackTrace = value;
|
||||
Logger.LogInfo($"Stack trace visibility set to {value}.");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearLogs()
|
||||
{
|
||||
Logger.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Infrastructures.SceneGraph;
|
||||
using Ghost.Entities;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class HierarchyViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<SceneNode> SceneList
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new();
|
||||
|
||||
public HierarchyViewModel()
|
||||
{
|
||||
// Test only
|
||||
var testWorld = World.Create();
|
||||
var entity1 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 1");
|
||||
var entity2 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 3");
|
||||
var entity3 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 4");
|
||||
var entity4 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 5");
|
||||
var entity5 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 2");
|
||||
|
||||
var testScene = new SceneNode(testWorld, "Test Scene");
|
||||
|
||||
SceneGraphHelpers.AttachChild(testScene, entity1, entity2);
|
||||
SceneGraphHelpers.AttachChild(testScene, entity1, entity3);
|
||||
SceneGraphHelpers.AttachChild(testScene, entity2, entity4);
|
||||
|
||||
testScene.Children.Add(entity1);
|
||||
testScene.Children.Add(entity5);
|
||||
|
||||
SceneList.Add(testScene);
|
||||
}
|
||||
}
|
||||
142
Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs
Normal file
142
Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class ProjectViewModel : ObservableObject
|
||||
{
|
||||
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()
|
||||
{
|
||||
if (ProjectService.CurrentProject.Metadata == null)
|
||||
{
|
||||
throw new InvalidOperationException("Current project is not set.");
|
||||
}
|
||||
|
||||
var assetsItem = new ExplorerItem("Assets", Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER), true);
|
||||
LoadSubFolderRecursive(ref assetsItem);
|
||||
|
||||
SubDirectories.Add(assetsItem);
|
||||
}
|
||||
|
||||
private void LoadSubFolderRecursive(ref ExplorerItem parentItem)
|
||||
{
|
||||
foreach (var directory in Directory.EnumerateDirectories(parentItem.Path))
|
||||
{
|
||||
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.Path == path);
|
||||
});
|
||||
}
|
||||
|
||||
public void NavigateToSelected()
|
||||
{
|
||||
if (SelectedAsset == null || !SelectedAsset.IsDirectory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NavigateToDirectory(SelectedAsset.Path);
|
||||
}
|
||||
|
||||
partial void OnSelectedDirectoryChanged(ExplorerItem? value)
|
||||
{
|
||||
DirectoryAssets.Clear();
|
||||
if (value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NavigateToDirectory(value.Path);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.AppStates;
|
||||
using Ghost.Editor.Contracts;
|
||||
using Ghost.Editor.Helpers;
|
||||
using Ghost.Editor.Infrastructures.AppState;
|
||||
using Ghost.Editor.Services;
|
||||
using Ghost.Engine.Resources;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -13,9 +13,9 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ghost.Editor.ViewModel.Pages.Landing;
|
||||
namespace Ghost.Editor.ViewModels.Pages.Landing;
|
||||
|
||||
internal partial class CreateProjectViewModel(StackedNotificationService notificationService, ProjectService projectService, AppStateService stateService) : ObservableRecipient, INavigationAware
|
||||
internal partial class CreateProjectViewModel(StackedNotificationService notificationService, ProjectService projectService, AppStateMachine stateService) : ObservableObject, INavigationAware
|
||||
{
|
||||
public ObservableCollection<TemplateData> templates = new();
|
||||
|
||||
@@ -77,16 +77,15 @@ internal partial class CreateProjectViewModel(StackedNotificationService notific
|
||||
}
|
||||
|
||||
var result = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, EngineData.s_engineVersion, SelectedTemplate.Value.directory);
|
||||
if (!result.success || result.data == null)
|
||||
if (!result.success)
|
||||
{
|
||||
notificationService.ShowNotification(result.message, InfoBarSeverity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = await ProjectService.LoadMetadataAsync(result.data.MetadataPath); // Metadata should not be null here if create project succeeded
|
||||
try
|
||||
{
|
||||
await stateService.TransitionToAsync(StateKey.EngineEditor, metadata);
|
||||
await stateService.TransitionToAsync(StateKey.EngineEditor, result.data);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
109
Ghost.Editor/ViewModels/Pages/Landing/OpenProjectViewModel.cs
Normal file
109
Ghost.Editor/ViewModels/Pages/Landing/OpenProjectViewModel.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.Contracts;
|
||||
using Ghost.Editor.Infrastructures.AppState;
|
||||
using Ghost.Editor.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.Landing;
|
||||
|
||||
internal partial class OpenProjectViewModel(ProjectService projectService, StackedNotificationService _notificationService, AppStateMachine _stateService) : ObservableObject, INavigationAware
|
||||
{
|
||||
public readonly ObservableCollection<ProjectMetadataInfo> projects = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Visibility EmptyVisibility
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Visibility DragVisibility
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void UpdateEmptyPlaceHolderVisibility()
|
||||
{
|
||||
EmptyVisibility = projects.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public async void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
projects.Clear();
|
||||
await foreach (var projectInfo in projectService.LoadAllProjectAsync())
|
||||
{
|
||||
var metadata = await ProjectService.LoadMetadataAsync(projectInfo.MetadataPath);
|
||||
if (metadata == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
projects.Add(new(projectInfo.MetadataPath, metadata));
|
||||
}
|
||||
|
||||
UpdateEmptyPlaceHolderVisibility();
|
||||
}
|
||||
|
||||
public void OnNavigatedFrom()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ContentDrop(DataPackageView dataView)
|
||||
{
|
||||
var errorMessage = string.Empty;
|
||||
if (dataView.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var items = await dataView.GetStorageItemsAsync();
|
||||
var rootFolder = items.OfType<StorageFolder>().FirstOrDefault();
|
||||
if (rootFolder != null)
|
||||
{
|
||||
var result = await projectService.AddProjectFromDirectoryAsync(rootFolder.Path);
|
||||
if (result.success)
|
||||
{
|
||||
projects.Add(result.data);
|
||||
goto CloseDropPanel;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = result.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Unsupported data format. Please drop a folder containing a project.";
|
||||
}
|
||||
|
||||
_notificationService.ShowNotification(errorMessage, InfoBarSeverity.Error);
|
||||
|
||||
CloseDropPanel:
|
||||
DragVisibility = Visibility.Collapsed;
|
||||
UpdateEmptyPlaceHolderVisibility();
|
||||
}
|
||||
|
||||
public async Task LoadProject(ProjectMetadataInfo project)
|
||||
{
|
||||
try
|
||||
{
|
||||
project.Metadata.LastOpened = DateTime.Now;
|
||||
await ProjectService.CreateMetadataFileAsync(project.Path, project.Metadata);
|
||||
|
||||
await _stateService.TransitionToAsync(StateKey.EngineEditor, project);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_notificationService.ShowNotification($"Failed to load project: {e.Message}", InfoBarSeverity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Engine.Resources;
|
||||
|
||||
namespace Ghost.Editor.ViewModel.Windows;
|
||||
namespace Ghost.Editor.ViewModels.Windows;
|
||||
|
||||
internal partial class EngineEditorViewModel : ObservableRecipient
|
||||
{
|
||||
public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.s_engineVersion}";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ProjectMetadata CurrentProject
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public ProjectMetadataInfo CurrentProject => ProjectService.CurrentProject;
|
||||
}
|
||||
Reference in New Issue
Block a user