241 Commits

Author SHA1 Message Date
2cbe1ca789 Refactor hierarchy drag-and-drop and cleanup inspector
Refactored HandleDrawer to use a static UpdateUI method for clarity. Removed unused PropertyDrawerContext. Added EntityCreated event to EditorWorldService. Updated Hierarchy.xaml to modern TreeView drag-and-drop events. Overhauled drag-and-drop logic in Hierarchy.xaml.cs for better validation, cycle prevention, and scene graph consistency. Introduced helper methods for parent retrieval and scene graph rebuilding. Improved error handling throughout drag-and-drop operations.
2026-05-27 20:08:40 +09:00
1fe080dd87 refactor(editor): unify PropertyModel and IFieldMiddleware into PropertyNode
This refactoring eliminates transient PropertyModel allocations and synchronizes UI data binding directly with the SceneGraph ComponentNode's persistent PropertyNode array. All PropertyDrawers have been updated to consume PropertyNode<T>. Additionally, this commit stages and includes various updates across the engine, graphics, and third-party wrappers as requested.
2026-05-26 19:13:59 +09:00
211ea2254d feat(inspector): rewrite to zero-boxing generic property models
Refactor the ECS component inspector architecture to eliminate per-frame GC allocations (boxing).

- Replace object-based PropertyModel with generic PropertyModel<T>
- Introduce IPropertyModel interface for erased type holding
- Create PropertyDrawerRegistry and ComponentEditorRegistry
- Implement recursive nested struct support in PropertyDescriptor
- Fix EntityDrawer to update text dynamically on sync
- Delete obsolete object-boxing UI bindings
2026-05-25 14:41:04 +09:00
914dde2448 feat(inspector): redesign data-driven ECS inspector
Replaced the hardcoded ComponentView approach with a data-driven ECS inspector that works directly with memory pointers. Introduced ComponentDescriptor and PropertyDescriptor to cache reflection metadata and C# struct layout offsets. Added InspectorSyncService which hooks into CompositionTarget.Rendering to safely synchronize oid* chunk memory to UI via PropertyBinding. Added pluggable PropertyDrawer architecture for rendering scalar properties (Float, Int, Bool, Enum). Updated EntityManager with GetEntityArchetype to easily iterate through component signatures for an entity.
2026-05-24 15:06:50 +09:00
5222e801b9 Add asset open handler system and async open support
Introduced AssetOpenHandlerAttribute for extension-based asset open logic.
Implemented async OpenAssetAsync methods in IAssetRegistry and AssetRegistry, using reflection to invoke handlers.
Updated ContentBrowser and ViewModel to support async asset opening.
Refactored Hierarchy initialization and XAML for clarity.
Logger now throws exceptions in DEBUG for errors/asserts.
Removed unused code and cleaned up usings.
2026-05-23 13:05:25 +09:00
a1c5ccf937 Refactor scene loading/materialization & asset streaming
- Overhauled scene loading with async, incremental, and cancellable operations using new types (SceneLoadStatus, SceneLoadOptions, etc.)
- Added thread-safe management of pending/loaded scenes and per-frame materialization budgeting
- Changed mesh header offsets from ulong to long; updated all related code to use stream positions directly
- Improved resource release logic for mesh/texture asset entries
- Refactored asset loading jobs and made reimport flag atomic
- Stream utility now always aligns memory blocks to 16 bytes
- Misc: fixed mock content headers, cleaned up code, and improved interface visibility
2026-05-22 18:21:16 +09:00
6b501efda0 Refactor and expand scene/entity hierarchy system
- SceneGraphBuilder: support initial entity names in tree
- EditorWorldService: full CRUD, events, scene graph rebuild
- SceneGraphSyncService: event-driven sync, node map, TryGetNode
- SceneSerializationService: serialize/deserialize names, sync
- Hierarchy UI: context menus, drag-and-drop, delete, no polling
- SceneManager: fix component type ID handling
- Add SceneGraphSync unit tests
- Remove obsolete asset handler/importer attributes
- Scene hierarchy is now reactive, robust, and testable
2026-05-22 17:55:25 +09:00
a40140cabd Refactor editor DI, update scene graph infra & deps
- Removed `[EditorInjectionAttribute]` and switched to direct service registration in `App.xaml.cs`
- Deleted `EditorIconSource` and moved icon handling to XAML
- Moved and enhanced `PathUtility` for path normalization and unique naming
- Renamed `SceneManager.UnloadScene` to `DestroyScene` for clarity
- Optimized `EntityQuery.Any()` with bitmask logic
- Improved `SceneGraphBuilder` and `SceneGraphSyncService` for better scene/entity handling
- Simplified entity field detection in `SceneSerializationService`
- Removed unused `TypeCache.Initialize()`
- Updated `ContentBrowser` to support scene asset creation and adjusted selection logic
- Upgraded NuGet package versions in project files
- Changed code generators to use `#if GHOST_EDITOR`
- Made `ShaderVariantCompiledHandler` safe
- Updated or removed scene graph planning docs to match new architecture
2026-05-22 15:32:30 +09:00
7dac1e4437 refactor: rewrite scene streaming & shader bridge
This commit overhauls the scene management and streaming architecture to use a chunk-based asynchronous loading paradigm, and completely decouples the graphics runtime from editor compilation services to eliminate circular dependencies.

Shader Bridge Decoupling:
- Resolved circular runtime dependency between Ghost.Graphics and Ghost.Editor.Core.
- Shifted EditorShaderCompilerBridge from querying EngineCore to a decoupled event-driven model using IShaderCompilationBridge.
- Introduced custom stack-friendly delegate ShaderVariantCompiledHandler to handle ReadOnlySpan<ShaderByteCode> compilation buffers while maintaining zero-allocation constraints.
- Updated ShaderLibrary to self-manage bytecode caching and stale pipeline eviction by listening to compiler events.

Scene & Streaming Overhaul:
- Re-implemented Scene.cs with a high-performance two-phase asynchronous scene loading architecture.
- Replaced outdated per-entity component processing with chunk-level shared data management using the ISharedComponent paradigm for SceneID.
- Optimized ECS Query.cs and Archetype.cs to handle streaming chunk operations efficiently.
- Updated AssetManager, ResourceStreamingProcessor, and asset entries (SceneAssetEntry, MeshAssetEntry, TextureAssetEntry) to support the new streaming workflow.
2026-05-21 21:43:41 +09:00
e04c7eb6a7 Refactor ECS: split IComponent into Data/Shared types
Refactored ECS to distinguish IComponentData and ISharedComponent. Updated all component implementations and method constraints accordingly. Changed SceneID handling to use shared components and updated related APIs and tests. Fixed BufferReader pointer advancement and initialized EntityManager managed storage.
2026-05-18 10:51:02 +09:00
84c936bb7a Refactor safety checks, buffer reading, and scene loading
Replaces GHOST_EDITOR with GHOST_SAFETY_CHECKS for debug checks. Introduces IBufferReader and StreamBufferReader to unify buffer/stream reading. Refactors SceneManager.ParseSceneData to support both buffer and stream sources, improving error handling and resource management. Updates usages of ReadMemory to ReadBuffer. Expands and modernizes SceneSerializationTests with async and expressive assertions. Adds ergonomic QueryBuilder methods. Applies minor code style and safety improvements throughout.
2026-05-17 23:56:53 +09:00
18505cdff6 Refactor scene loading, shared components, and cleanup
- Split scene loading into parsing and materialization steps
- Make SceneID an ISharedComponent; add SharedComponentSet
- Centralize archetype/cleanup logic for entity destruction
- Add batch DestroyEntities to EntityCommandBuffer
- Use shared component filtering for SceneID queries
- Move AssetType/AssetState enums to AssetEntry.cs
- Remove ManagedEntity/ScriptComponent logic
- Misc: Write<T> signature, AsSpan, code style, GC fixes
2026-05-16 21:07:54 +09:00
f85cf4edde add ICleanupComponent support at DestroyEntities 2026-05-16 10:29:42 +09:00
982ce7d8e0 Add robust shared component support to ECS
- Introduce ISharedComponent and remove legacy shared wrapper types
- Archetype now supports chunk groups for shared values
- EntityManager: Add/Remove/Set shared component APIs with correct migration
- EntityCommandBuffer: shared component operations supported
- ChunkView: GetSharedComponent<T>() for fast shared access
- ComponentSet/ComponentSetView: track type and shared data hashes
- Query and entity counting handle chunk groups/shared components
- Switch ReadOnlyUnsafeCollection to ReadOnlyView throughout
- Scene/SceneGraph: SceneID is now ushort, not struct
- BufferWriter/Reader and SpanWriter/Reader API improvements
- World: thread-local ECBs use UnsafeArray
- Add comprehensive unit tests for shared component features
- Update dependencies, clean up legacy code, improve comments
2026-05-15 19:17:55 +09:00
cf5af7ee50 Refactor asset/scene loading to use streams, update APIs
Refactored asset and scene loading to operate on streams instead of raw memory blocks, improving flexibility and efficiency. Replaced World.Clear(TimeData) with World.Reset() and updated all usages. Updated SceneManager and asset entry APIs to use streams, added new StreamUtility methods, and removed IDisposable from Scene. Refactored SystemManager to remove TimeData from lifecycle methods. Performed related code cleanups and updated tests.
2026-05-14 21:51:31 +09:00
60b807abd7 Refactor Scene/SceneID to value-based model, move loading
Refactored Scene and SceneID to use a value-based approach, with SceneID now storing a ushort value instead of a Scene struct. Updated all usages to reference the new value and ID properties. Moved scene loading logic from SceneLoader (SceneAssetEntry.cs) into SceneManager for better consolidation. Updated entity creation, serialization, and tests to use the new SceneID pattern. Cleaned up obsolete code and improved comments regarding scene streaming and loading workflow. Added IDisposable to Scene and related structs for resource management.
2026-05-13 19:48:31 +09:00
cbaa129d9e Refactor ECS core, improve thread safety, add tests
- Switch Scene IDs to ushort, update invalid value logic
- Add thread safety to SceneManager and ComponentRegistry
- Add GHOST_ZERO_INIT_COMPONENT define for editor configs
- Update mesh header counts to use int, not uint
- Add Collect methods for archetype/component cleanup
- Add With/Without/AsView to ComponentSet for easier use
- Refactor World creation/destruction, version handling
- Refactor ResourceStreamingContext to use init-only props
- Add unit tests for entity queries and world lifecycle
- Minor code cleanups and formatting fixes
2026-05-13 13:36:51 +09:00
bb07644580 Refactor asset streaming & resource management system
- Introduce Ghost.Engine.Streaming namespace and split asset entry logic into type-specific classes (TextureAssetEntry, MeshAssetEntry, SceneAssetEntry)
- Make AssetEntry abstract; add AssetEntryFactory for type dispatch
- Update AssetManager and ResourceStreamingProcessor for new entry model, supporting uploadable and processable assets
- Redesign scene/mesh asset loading, serialization, and binary formats with versioning (SceneContentHeader, MeshContentHeader)
- Move SceneLoadingType to Ghost.Engine and make public
- Inline performance-critical APIs with MethodImplOptions.AggressiveInlining
- Add deep cloning and improved resource management for Mesh and meshlet data
- Allow nullable log messages in Logger
- Update Misaki.HighPerformance package references
- Remove obsolete files (Asset.cs, ActivationHandler.cs, old mesh logic)
- Improve resource release logic in ResourceManager
- Update RenderContext and ResourceStreamingContext for new streaming model
- Add UnsafeArray/UnsafeList clone utilities
- Update scene serialization/deserialization for new format
- Update tests for new APIs, asset states, and formats
2026-05-12 22:51:51 +09:00
314b0111f0 fix(scene): avoid scene ID collision on load
Do not serialize the SceneID component.
Generate a new SceneID when loading a scene file dynamically to prevent ID collisions.
Update related tests and scene graph builders.
2026-05-10 22:50:05 +09:00
a95ff01366 feat: add scene serialization (JSON + binary) with import pipeline
Implement scene save/load for editor and runtime.
Editor JSON (.gscene) uses Utf8JsonWriter for inline component objects.
Runtime binary (.imported) stores marshalled component data with
entity field offset metadata for AOT-safe remapping.

- SceneSerializationService: save from EditorWorld, load into EditorWorld
- SceneAsset + SceneAssetHandler: .gscene import/pack pipeline
- AssetManager.Scene + SceneLoader: runtime binary deserialization
- Scene: [JsonConstructor] + [JsonIgnore] for round-trip
- Component: GetComponentIDByName for editor-side type lookup
- 10 unit tests (save, load, round-trip, unknown comp, invalid version)

Also guard DSLShaderCompiler editor code with #if GHOST_EDITOR,
add GC.SuppressFinalize to EditorWorldService, and switch Archetype
debug fields from #if GHOST_EDITOR to #if DEBUG.
2026-05-10 16:21:56 +09:00
7e1db7b908 change #if DEBUG || GHOST_EDITOR to #if GHOST_EDITOR 2026-05-10 12:01:06 +09:00
2ea3c509b0 feat: implement scene graph system with ECS hierarchy support
Add hierarchical scene graph for editor with TreeView UI, runtime
HierarchyUtility for parent/child linked-list management, and
incremental sync between editor world and scene graph nodes.

- SceneGraphNode/SceneNode/EntityNode with World, Scene, Entity refs
- SceneGraphBuilder — construct tree from ECS World queries
- HierarchyUtility — SetParent, RemoveParent, IsAncestor, cascade destroy
- EditorWorldService + SceneGraphSyncService — editor world lifecycle & incremental sync
- Hierarchy.xaml — TreeView with DataTemplate + SceneGraphTemplateSelector
- 25 unit tests covering hierarchy ops and scene graph building
2026-05-10 00:14:55 +09:00
e2bc68d359 Refactor UI panels, shader logic, and project settings
Refactored Hierarchy and Inspector panels into separate controls with improved styling and modularity. Cached EngineCore in EditorShaderCompilerBridge and updated shader cache logic for correctness. Renamed static field in PropertyField for consistency. Enhanced test coverage and fixed IDisposable implementation. Added XAML debugging properties for Debug_Editor and cleaned up obsolete scripts.
2026-05-09 15:20:22 +09:00
1cc65e8218 refactor(shader): rewrite editor shader compilation bridge with keyword resolution
- Add AssetCatalog.EnumerateByTypes for filtered SQL queries
- Thread LocalKeywordSet through IShaderCompilationBridge API so the
  bridge can resolve keyword bitmask to string defines at compile time
- Eager/lazy popluation of shader-id-to-asset-id map eliminating
  full catalog scan per compilation miss
- Build keyword mapping from PassDescriptor groups to reconstruct
  localIndex -> keywordName in the bridge
- Use CompileShaderPass extension for multi-stage AS/MS/PS compilation
  with correct ShaderModel from descriptor
- Remove double-load of shader asset in compilation flow
- Update test mock to match new interface signature
2026-05-08 19:36:24 +09:00
d0076c852f Add editor shader compilation bridge & pipeline cache mgmt
Introduced EditorShaderCompilerBridge and IShaderCompilationBridge for async shader variant compilation and cache invalidation in the editor. Refactored ShaderLibrary to support the bridge, updating hash/caching logic and triggering compilation on cache misses. Changed pipeline library to use ulong content hashes and added stale pipeline eviction. Updated EngineCore and render code to integrate the new system. Added unit tests for ShaderLibrary cache and bridge behavior. Minor improvements to shader property code generation and test generator.
2026-05-08 17:06:37 +09:00
ba8694ed0c Add shader property reflection and resource handles
Refactor shader property system to support runtime reflection via ShaderPropertyType and ShaderPropertyFieldInfo. Introduce strongly-typed Texture2D/3D and Buffer handle structs. Update ShaderPropertiesGenerator to emit field metadata and register it. Move mesh content structs to AssetManager.Mesh.cs and mark as internal. Update DSLShaderCompiler and registry for new property API. Remove obsolete files and clean up namespaces. Add sample TestShaderProperty struct.
2026-05-08 15:37:30 +09:00
80e820a858 Add Editor configs, refactor test core, DXC test
Added Debug_Editor/Release_Editor configs to all projects and solution. Refactored test utilities into Ghost.TestCore and updated references. Introduced DXCBindingTest for shader compilation. Updated conditional compilation to use GHOST_EDITOR. Improved platform mappings and performed minor code cleanup.
2026-05-08 13:59:36 +09:00
b42398bbce Refactor asset handler system and catalog for safety
- Introduced AssetHandlerInfo struct for handler registration and lookup, enabling handler caching and decoupling instantiation from extension/type.
- Changed CustomAssetHandlerAttribute to use required named properties; updated source generator.
- Replaced HandlerTypeId with AssetTypeId throughout metadata, catalog, and sub-asset records for clarity.
- Refactored asset catalog to use connection pooling and local command creation for thread safety.
- Updated asset handler interfaces and implementations to align with new registration system and removed redundant properties.
- Migrated mesh import and meshlet building to async JobScheduler jobs; switched to TLSF allocator and improved safety checks.
- Made meshlet/LOD hierarchy building async and job-based with better memory management.
- Updated usages and tests for new APIs; refreshed project references and package versions.
- Improved documentation and code comments for clarity.
2026-05-08 11:50:06 +09:00
d052ca848f Refactor resource management and update project configs
- Use `using` for MeshNode disposal in MeshAssetHandler
- Switch to `ref` UnsafeList in meshlet hierarchy methods for perf
- Ensure proper disposal of UnsafeList<int> and TempBinaryNode
- Add launchSettings.json for Ghost.Editor.Core debugging
- Update GhostEngine.slnx with platform mappings for Editor.Core
- Remove MHP_ENABLE_SAFETY_CHECKS from Debug|AnyCPU in csproj
2026-05-07 00:23:51 +09:00
744b058e6a Refactor mesh asset handling and memory allocation
- Unified FBX/OBJ logic into MeshAssetHandler and moved mesh node classes to MeshNode.cs
- Updated IAssetHandler to use CreateDefaultSettings(string ext)
- Made MeshAsset the abstract base, removed FBXAsset
- Switched mesh import/processing to use memory pools and explicit AllocationHandle
- Standardized manifest serialization options
- Improved error handling and normalized project paths
- Updated tests, project files, and AssetReference struct
2026-05-05 21:12:15 +09:00
5de480e231 Refactor asset import API and mesh streaming pipeline
- Standardize IImportableAssetHandler.ImportAsync to return sub-asset results
- Remove ISubAssetImportableAssetHandler, merge into main interface
- Update FBX/Texture handlers for new import contract
- Add StreamUtility for efficient (async) binary writes
- Refactor meshlet/LOD building to use ref structs and safe memory
- Use new streaming utilities in mesh import/export and tests
- AssetCatalog.Remove now recursively deletes sub-assets
- Improve asset registry file watcher for better change detection
- Log unhandled exceptions in App instead of breaking
- Add interpolated collection support to NativeMemoryManager
- Update project references and fix minor bugs
2026-05-05 17:19:24 +09:00
8d3e1c91d7 Add sub-asset import and mesh asset support
- Implement sub-asset import for mesh/model assets with manifest generation and deterministic GUIDs
- Extend AssetCatalog for sub-asset tracking and management
- Update AssetRegistry and ImportCoordinator for sub-asset workflows
- Add mesh asset parsing, GPU upload, and resource management
- Update mesh data structures for meshlet groups/hierarchy and LODs
- Improve tests for sub-asset import and mesh handling
- Enhance mocks for mesh asset testing and resource mapping
- Fix path handling and native DLL loading issues
- Miscellaneous bug fixes and refactoring
2026-05-04 21:25:03 +09:00
bffe05f0ef Added LICENSE file. 2026-05-04 16:44:02 +09:00
220db828a0 Remove MHP_ENABLE_STACKTRACE from Debug constants 2026-05-03 17:06:43 +09:00
d2bf2f12a2 Refactor asset streaming, error handling, and unit tests
- Add new compile-time constants and update package versions
- Refactor AssetEntry upload logic to return Result and propagate errors
- Enhance error handling in ResourceStreamingProcessor uploads
- Make ResourceStreamingContext a readonly struct
- Implement IDisposable and finalizers for resource cleanup
- Overhaul AssetManagerTest with async tests and improved mocks
- Add mock implementations for graphics interfaces for testing
- Refactor MockingCommandBuffer and MockingResourceDatabase for better simulation
- Update internals visibility for unit testing
2026-05-03 17:05:52 +09:00
e7fedfd35a Update asset system for deferred allocation & add unit tests
Modernize Misaki.HighPerformance dependencies. Refactor texture asset creation to use deferred resource slots via CreateEmpty(). Remove fallback resource fields and update texture resolution logic. Add CreateEmpty() to resource database interfaces. Introduce comprehensive unit tests with mocks for asset management. Enable unsafe code in tests.
2026-05-02 22:54:58 +09:00
e384a2f38c feat(meshlet): add cluster LOD hierarchy & API upgrades
Implemented meshlet cluster LOD hierarchy with binary-to-4-ary conversion. Updated MeshletHierarchyNode to 4-ary structure. Enhanced SIMD optimizations in GGX mipmap generation. ResourceManager mesh/material creation now supports dynamic buffers and optional naming. Upgraded SPMD package to 1.3.2. Performed minor code cleanups and doc improvements.
2026-05-01 15:06:27 +09:00
0eaf7cd51d Refactor material palette system with GPU indirection
Major overhaul of material palette management:
- Added two-buffer indirection (PaletteOffsetBuffer, MaterialIndexBuffer) for GPU material lookup, with incremental upload and resizing.
- MaterialPaletteStore now tracks dirty ranges, supports deferred slot reclamation, and exposes CPU-side arrays for upload.
- ResourceManager manages persistent GPU buffers and uploads only dirty subranges per frame.
- Updated HLSL and C# structs to use palette indices.
- Refactored systems/components to use new palette index and release logic.
- Added RenderContext.UploadBufferRange for partial uploads.
Minor: Fixed StbIApi interop signatures, updated test namespaces, and performed code cleanups.
2026-04-28 18:22:09 +09:00
631638f3fb feat: implement material palette management and core mesh asset handling infrastructure 2026-04-27 22:55:55 +09:00
e3a02437c3 Refactor mesh/texture pipeline for unified buffers & cubemaps
- Switch mesh import to unified vertex/index buffers with multi-material partitioning (`MaterialPartInfo`)
- Update `GeometryMeshNode` and meshlet builder for unified buffer layout
- Refactor cubemap texture pipeline: packed faces, improved GGX mip generation, equirect->cubemap conversion, and cubemap sampling
- Change MeshBuilder normal/tangent utilities to use `Span<T>`
- Add mimalloc allocator dependency and enable in Debug/Release
- Misc bug fixes, resource management, and code cleanup
2026-04-26 21:40:24 +09:00
5903ddda2b Refactor mesh import, meshlet, and asset handler systems
- Mesh import now builds full node hierarchy and splits geometry by material, with robust normal/tangent handling
- Meshlet generation supports material indices for correct assignment
- Refactored texture cube map compression and mipmap handling
- Updated asset handler registration to new namespace
- Enabled asset reimport on import events
- Improved code quality, resource management, and formatting
2026-04-26 14:49:58 +09:00
1a91811621 Refactor asset pipeline to use file paths, improve import
- Switched asset handler interfaces and implementations to use file paths instead of FileStreams for all operations.
- Refactored mesh asset structure and parsing, moved meshlet logic to MeshProcessor, and introduced hierarchical MeshNode types.
- Updated texture asset handling: switched to bits-per-channel, improved mipmap/cubemap generation, and SPMD HDRI support.
- Updated shader asset handlers to use file paths and split code compilation logic.
- Improved asset registry: added event debouncing, better path handling, and import time/hash tracking.
- Added source generator for IAssetSettings registration to support polymorphic JSON serialization.
- Updated dependencies and tests; various minor fixes and cleanups.
2026-04-25 18:23:21 +09:00
4757c0c91a Replace Magick.NET with stb_image; refactor asset pipeline
- Switched image loading/saving from Magick.NET to native stb_image (Ghost.StbI), removing Magick.NET dependency.
- Added Ghost.StbI project with native DLL, P/Invoke bindings, and wrapper.
- Refactored TextureAssetHandler and TextureProcessor for stb_image, memory-mapped IO, and HDR/16-bit support.
- Split IAssetHandler into IImportableAssetHandler and IPackableAssetHandler; updated interfaces to use FileStream.
- Added shader and mesh asset handlers (GraphicsShaderAssetHandler, ComputeShaderAssetHandler, FBXAssetHandler).
- Improved asset registry/catalog path handling and naming consistency.
- Updated asset import pipeline to use new interfaces and trigger engine reimport.
- Enhanced UI toolbar button styles and EditPage layout.
- Added StbIBindingTest, DisableRuntimeMarshalling, and native wrapper attributes.
- Updated wrapper generator for regex derivesFrom; added stbi.json config.
- Removed Magick.NET reference; added Ghost.StbI and Ghost.Ufbx references.
- Miscellaneous bugfixes and code cleanup.
2026-04-24 00:40:27 +09:00
3533d3367f feat: Implement LogViewer control and integrate into EditPage
- Added LogViewer control to display log messages with filtering options.
- Integrated LogViewer into EditPage for better log management.
- Updated EngineEditorWindow to navigate to EditPage.
- Enhanced Logger implementation for improved performance and stack trace capturing.
- Introduced PathUtility for path normalization.
- Refactored AssetManager to correct shader asset type naming.
- Removed obsolete AssetHandlerRegistryTests and cleaned up related tests.
- Updated ImportCoordinatorTests for streamlined asset import process.
2026-04-22 20:25:14 +09:00
884611181a Refactor instance update flow, asset registry, and texture IO
- Renamed AddInstanceRequest to UpdateInstanceRequest; unified add/update logic for GPU instances
- Introduced UpdateGPUInstanceSystem to handle changed MeshInstance components
- Replaced QueryBuilder.Create() with QueryBuilder.New() for consistency
- Switched versioning in ChunkView HasChanged/HasStructuralChanged to uint
- Added extension-to-AssetType mapping in AssetHandlerRegistry
- Changed TextureAssetHandler/Processor to use nint for image data
- Enhanced DDS cache: read mipmap count, handle invalid files
- Updated ProjectBrowserViewModel to use IAssetRegistry
- Upgraded Misaki.HighPerformance and System.IO.Hashing packages
- Set DependencyChainCapacity in JobSchedulerDesc
- Fixed instance buffer logic in GhostRenderPipeline
- Miscellaneous cleanups and namespace improvements
2026-04-22 15:36:49 +09:00
cb4092179f refactor(core): asset pipeline overhaul & dock removal
- Introduced IAsset interface and refactored asset loading/saving.
- Migrated TextureContentHeader to Ghost.Engine; updated usage.
- Rewrote AssetRegistry, AssetCatalog, ImportCoordinator for new asset flow.
- Added thread-safe ConcurrentHashSet utility.
- Improved EditorApplication folder management/init.
- Updated TextureAssetHandler/TextureProcessor for new import/export.
- Added EditorContentProvider for asset access.
- Updated AssetManager to use new AssetType enum; removed GCHandle.
- Removed all custom docking controls and templates.
- Deleted obsolete ViewModels/Pages (Console, Hierarchy, Inspector, Project).
- Renamed ProjectBrowser to ContentBrowser; updated references.
- Updated NuGet packages, Result conversions, and commit instructions.
- General cleanup: namespaces, dead code, structure.
2026-04-21 23:20:29 +09:00
c249a389e3 Refactor AssetHandlerRegistry, modernize editor window UI
Refactored AssetHandlerRegistry to a static class and updated all usages. Replaced the docking-based EngineEditorWindow with a new grid/panel layout, adding modern toolbar, menu bar, and status bar. Introduced new divider styles and improved UI details. Removed obsolete and unused files, updated project references, and made minor code and UI/UX improvements.
2026-04-20 16:54:49 +09:00
ed00f205b0 feat: implement asynchronous asset management system with texture streaming support 2026-04-20 01:09:59 +09:00
4f5556ee1b feat: implement core graphics rendering system and D3D12 RHI backend infrastructure 2026-04-18 21:03:05 +09:00
abd5ad74d5 Refactor asset pipeline: new registry, loader, and runtime
Major overhaul of asset system:
- Split assets into source, .gmeta (JSON), and cooked .imported binaries
- Replaced Asset base class; added TextureAsset, TextureLoader
- AssetManager now uses job-based, dependency-aware loading
- Unified IAssetHandler API; removed legacy handler interfaces
- Updated D3D12 allocator and graphics code for new resource model
- Improved error handling, memory management, and GPU upload logic
- Updated docs and removed obsolete code/interfaces
2026-04-18 01:46:37 +09:00
13bf1501e4 feat(asset): asset manager + registration generator
Add runtime AssetManager and AssetHandlerRegistrationGenerator source generator.
Update editor asset handler types and services to work with new registration
mechanism and asset catalog. Remove legacy contracts ICloneable and IReleasable.

Files added:
- src/Runtime/Ghost.Engine/AssetManager.cs
- src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs

Major edits:
- Editor asset handler classes and services (Asset*, Texture*, Registry)
- Runtime Handle.cs and project files
- Render graph executor and tests updated accordingly

This commit introduces the foundation for the modern asset pipeline
including generated registration of asset handlers and a centralized
runtime AssetManager that will drive asset lifecycle.
2026-04-15 22:21:00 +09:00
6615fe794e feat(asset): modern asset system with SQLite catalog
Refactored asset management to use a persistent, thread-safe SQLite-backed AssetCatalog, replacing in-memory dictionaries.
Added AssetHandlerRegistry for O(1) handler lookup, ImportCoordinator for async background importing, and robust AssetMeta/AssetMetaIO for JSON-based metadata and settings.
Refactored AssetRegistry to integrate these components and support auto-import via file system watcher.
Updated IImportableAssetHandler for handler-specific settings and polymorphic serialization.
Added comprehensive unit tests for all new systems.
Removed obsolete code and legacy integration tests.

BREAKING CHANGE: Asset system APIs and storage format have changed; migration required for existing projects.
2026-04-14 20:18:38 +09:00
d9bfa43663 feat(rendering): add GPU scene updates and optimizations
Added a new `code-executor` agent with strict TDD and performance focus. Refactored `TextureProcessor` and `TextureAssetHandler` to use `Magick.NET` for image processing. Enhanced `GPUScene` with `InstanceCounterBuffer` and improved instance management. Introduced a compute shader for GPU scene updates. Updated `GhostRenderPipeline` to handle add/remove instance buffers.

BREAKING CHANGE: Removed `x86` platform support and replaced `CachesFolderPath` with `LibraryFolderPath`. Updated project dependencies and removed unused utility classes.
2026-04-14 17:56:23 +09:00
817b32b8d9 feat(graphics): refactor pipeline keying and allocators
Major refactor of graphics pipeline keying, shader cache, and resource allocation.
Replaced most Allocator usage with AllocationHandle, modernized logger usage,
and unified pipeline state keys. Updated MeshUtility to use AllocationHandle.FreeList.
Added new shader pipeline architecture docs and improved error handling throughout.

BREAKING CHANGE: Pipeline keying and resource allocation APIs have changed.
2026-04-13 23:07:52 +09:00
c66fda5332 feat(shader): refactor and enhance shader pipeline
Refactored the shader compilation pipeline to introduce modularity, improve performance, and enhance maintainability. Key changes include:

- Added `ShaderCompilationConfig`, `CompilerOptimizeLevel`, and `ShaderStage` enums.
- Replaced `SM` property with `ShaderModel` in shader models.
- Introduced `ShaderLibrary` for in-memory and disk-based shader caching.
- Refactored `DSLShaderCompiler` and `AntlrShaderCompiler` for better hashing and error handling.
- Centralized shader compilation logic in `ShaderCompilerUtility`.
- Removed legacy shader compilation logic from `IShaderCompiler`.
- Updated `RenderGraph`, `ResourceManager`, and `Material` to integrate with the new caching system.
- Improved memory management with `NativeMemoryManager<T>`.

BREAKING CHANGE: Removed legacy shader compilation methods and replaced them with a new caching and compilation system.
2026-04-11 23:10:39 +09:00
f9a6e9cbbe feat(shader): refactor and enhance shader compilation
Refactored shader compilation and resource management systems:
- Introduced `DXCShaderCompiler` for HLSL compilation and reflection.
- Added `BuildFinalShaderCode` method for robust shader code generation.
- Replaced raw strings with `ShaderEntryPoint` struct for shader paths.
- Updated `RenderContext` and `RenderGraphContext` for new pipeline methods.
- Added thread-safe resource management methods in `ResourceManager`.
- Introduced `DXCShaderReflectionData` for shader reflection handling.
- Removed redundant code and simplified `ShaderPropertiesRegistry`.

BREAKING CHANGE: Updated shader and resource APIs to use new structures and methods.
2026-04-11 00:45:46 +09:00
4ed5572ce7 feat(shader): add compute shader support and refactor pipeline
Refactored shader system to support both graphics and compute shaders.
- Updated ANTLR grammars and parser logic for explicit shader model and compute shader entry points.
- Split shader models and descriptors for graphics and compute.
- Refactored pipeline key generation and D3D12 pipeline library for compute support.
- Updated push constant layouts and HLSL includes for both shader types.
- Improved error handling and test coverage with new example files.

BREAKING CHANGE: Shader model, descriptor, and pipeline APIs have changed. Existing shader and pipeline code must be updated to use the new types and conventions.
2026-04-10 02:53:40 +09:00
68fda03aa9 feat(render): refactor pipeline & shader system for DX12 WG
Major refactor of render pipeline and shader system:
- Replaced legacy shader properties with source generator and attribute-based HLSL struct generation.
- Introduced ShaderPropertiesRegistry for runtime property layout/code registration.
- Added modular IRenderPipeline, IRenderPipelineSettings, and IRenderPayload interfaces.
- Implemented GhostRenderPipeline and ECS-driven GPUScene management.
- Added experimental DirectX 12 Work Graph support.
- Refactored shader compilation, variant hashing, and caching.
- Updated APIs for consistency and improved codegen for registration.

These changes modernize the rendering infrastructure for advanced features like work graphs and dynamic pipelines.

BREAKING CHANGE: Shader DSL, pipeline, and property APIs have changed. Existing shaders and pipeline integrations must be updated.
2026-04-08 23:08:02 +09:00
0fc449bc78 feat(ufbx): switch to native ufbx_vec/quat/matrix types
Replaces all Misaki.HighPerformance.Mathematics vector, quaternion, and matrix types in Ghost.Ufbx bindings with new native ufbx_vec2, ufbx_vec3, ufbx_vec4, ufbx_quat, and ufbx_matrix structs. Updates all interop code, struct fields, and API signatures accordingly. Adds struct definitions for the new types and provides matrix operations as struct methods. Removes unnecessary math package reference. Also includes minor fixes to system attributes, meshlet LOD logic, and mesh utility.

BREAKING CHANGE: All Ufbx-related APIs now use ufbx_* types instead of Misaki.HighPerformance.Mathematics types. Existing code using the old types will require updates.
2026-04-07 23:50:55 +09:00
a5c10cfe5a feat(render): support per-frame render payloads
Refactored the render pipeline system to introduce per-frame IRenderPayload management.
IRenderPipelineSettings now requires CreatePipeline and CreatePayload methods.
Updated RenderSystem and test pipeline to use the new payload model.
Removed legacy GhostRenderPipeline and test code.
Added RenderPipelineSystemAttribute for pipeline system registration.
Includes minor fixes such as version field type corrections and typo fixes.

BREAKING CHANGE: Render pipeline and payload creation APIs have changed; implementers must update to the new interface methods.
2026-04-07 17:12:01 +09:00
6c96d4cf50 feat(core,rendering)!: add cleanup component support, refactor render pipeline
Introduce ICleanupComponent and cleanup archetype logic in ECS. Refactor component versioning to uint. Update IResourceDatabase to use map/unmap pattern. Decouple per-frame render requests from RenderSystem via IRenderPayload. Update render pipeline and extraction system to new API.

BREAKING CHANGE: Entity destruction and render pipeline APIs have changed. IResourceDatabase.MapResource signature is updated; all callers must use map/memcpy/unmap. RenderSystem no longer manages per-frame render requests directly.
2026-04-06 22:05:48 +09:00
c6bdbe0710 feat(d3d12): add indirect command execution support
Added ICommandSignature and D3D12CommandSignature for indirect command execution in the D3D12 backend, with supporting types. Updated ICommandBuffer and IGraphicsEngine interfaces to support indirect execution and command signature creation. Refactored command buffer pooling in D3D12GraphicsEngine for more flexible reuse. Changed BeginFrame and EndFrame to void and clarified parameter names. Updated resource and frame data structures to use direct buffer indices. Added RenderingUtility for buffer and texture uploads. Removed IRenderOutput interface. Updated RenderSystem render loop and HLSL/C# code to match new buffer usage patterns.

BREAKING CHANGE: ICommandBuffer, IGraphicsEngine, and related APIs have changed signatures and behaviors. Indirect command execution is now supported and required for some advanced features.
2026-04-05 23:11:08 +09:00
effd33b285 feat(rendergraph): async queue, pool refactor, barrier cleanup
Refactor resource pool to use UnsafeQueue/UnsafeList for transient resources, improving memory management and performance.
Add async GPU wait support to ICommandQueue and D3D12.
Refactor render graph barrier system, streamline CompiledBarrier, and remove ResourceBarrier.
RenderGraphCompiler now returns Result<float2, Error> for dynamic resolution scaling.
Replace custom memory pools with Allocator.FreeList for temp allocations.
Add ResourceUploadBatch for async/sync resource uploads.
Fix D3D12 disposal and fence tracking bugs.
Update NuGet dependencies.
Numerous minor cleanups and code improvements.
2026-04-05 17:54:23 +09:00
92970f85ef feat(render): improve frame sync and CPU write tracking
Refactored frame submission and synchronization logic for more accurate GPU/CPU coordination. Introduced CpuWriteOpen property to enforce correct CPU frame access patterns. Updated ResourceManager to track _submittedFrame and improved event waiting logic. Added debug assertions and enhanced logging for frame start events. Documented known command submission issue in GraphicsTestWindow.
2026-04-03 22:17:54 +09:00
2dc97f3149 feat(resource-manager): add reusable page pool for efficiency
Refactored ResourceManager to introduce a _freePages queue for reusable pages, reducing unnecessary allocations. Added TryRentReusablePage and IsHeapFlagsCompatible helpers to efficiently rent compatible pages. Updated page creation and retirement logic to use the free pool, and ensured all page pools are properly released during cleanup.
2026-04-03 18:19:00 +09:00
ba9e24c46c feat(rhi): refactor resource & barrier management for D3D12
Modernizes resource and barrier management for the D3D12 backend. Key changes:
- Simplifies BarrierDesc by removing nullable "before" states; now inferred from resource database.
- Adds IsAliasing flag to BarrierDesc for aliasing transitions.
- Replaces ResourceMemoryType with HeapType in BufferDesc and related APIs.
- Enhances ResourceViewGroup with usage inference methods.
- Adds D3D12 utility helpers for heap/flag conversions and resource description extraction.
- Optimizes command buffer barrier emission, skipping redundant barriers.
- Refactors Material and RenderContext to use new APIs and state tracking.
- Updates ResourceManager pooling to use HeapType and standard Queue.
- Simplifies RenderGraphExecutor barrier logic and aliasing handling.
- Improves RenderSystem frame synchronization and resource retirement.
- Cleans up obsolete code and improves debug output.

BREAKING CHANGE: Updates to resource and barrier APIs require changes to all code interfacing with resource creation, barriers, and memory types.
2026-04-03 17:03:41 +09:00
6321b36ef5 feat(resource): refactor heap management & suballocation
Major overhaul of GPU resource/heap management:
- Replace resource pooling and upload buffer logic with transient heap/page-based suballocation in ResourceManager.
- Add support for suballocation and heap flags/types, with D3D12 helpers.
- Remove ICommandBuffer.UploadBuffer/UploadTexture; add UpdateSubResources and CopyBuffer, move upload logic to RenderingContext.
- Refactor D3D12ResourceAllocator/Database for suballocation, heap flags, and mapping.
- Standardize on Handle<GPUBuffer> usage.
- Update meshlet/mesh utilities for new allocation handles and memory pools.
- Refactor RenderGraph and docs to use "heap" terminology.
- Use cpuFrame/gpuFrame consistently for frame sync.
- Add s2h.hlsl, s2h_3d.hlsl, s2h_scatter.hlsl shader debug libs.
- Miscellaneous fixes, cleanup, and dependency updates.

BREAKING CHANGE: Resource pooling and upload APIs replaced with new heap/page-based suballocation system. Update all buffer/texture creation and upload code to use new ResourceManager and ICommandBuffer methods.
2026-04-03 01:48:49 +09:00
d03eb659fa feat(meshlet): refactor meshlet pipeline & add benchmark
Refactor meshlet build pipeline for robustness and performance.
Rename DxcShaderCompiler to DXCShaderCompiler. Enhance meshlet
data structures with bounds and LOD info. Add fallback mesh
simplification. Remove obsolete MeshRenderPass. Add
MeshoptBenchmark for meshlet build performance. Update mesh
import utilities for correct handedness. Minor bug fixes and
code cleanups.
2026-04-02 17:50:44 +09:00
e32a24739d feat(rhi): add NoAccess ops, axis conversion, meshlet color
Added NoAccess to attachment ops for depth/stencil, updated D3D12 and RenderGraph to handle new ops, and improved axis/handedness conversion in mesh loading. Enabled meshlet color hashing in test shader. Changed default rasterizer winding, added format helpers, and updated camera transform for correct mesh orientation. JobScheduler usage commented out for now.
2026-04-01 19:57:27 +09:00
eb41f23582 feat(rendergraph): skip redundant SRV barriers, add usage param
Add logic to skip generic SRV barriers for resources explicitly handled as color, depth, or random access in raster passes, preventing redundant transitions. Update RGTextureDesc to accept a TextureUsage parameter for more flexible texture descriptor creation.
2026-04-01 17:20:18 +09:00
3157596b5d Fix D3D12 depth format and stencil barrier issues in Render Graph 2026-04-01 15:38:37 +09:00
a00cb27529 feat(wrapper): span-based interop, resource API refactor
Refactored native wrappers to use ReadOnlySpan<T> for pointer parameters, improving .NET safety and interop. Enhanced wrapper generator with $TYPE and prefix/suffix-based parameter remapping. Added platform-specific native library loading for meshoptimizer, nvtt, and ufbx. Updated D3D12GraphicsEngineFactory for native DLL resolution and removed redundant logic from UnitTestApp. Changed RenderGraphBuilder's resource extraction API to use QueryTextureExtraction/QueryBufferExtraction with explicit handles and flags. Removed IRenderer and D3D12Renderer, moving RenderContext to RenderPipeline. Improved mesh loading, resource management, and updated test shader conventions. Updated project references, build settings, and added launchSettings.json for tooling.

BREAKING CHANGE: Native wrapper APIs now use ReadOnlySpan<T> instead of pointers. RenderGraphBuilder resource extraction API has changed. IRenderer and D3D12Renderer have been removed.
2026-04-01 14:50:20 +09:00
0b6e5b8501 feat(mesh): update Vertex layout, add mesh loader
Refactored Vertex to use float3 position/normal, float2 uv, float4 tangent, and Color128 color, updating all mesh generation and HLSL code accordingly. Added MeshUtility for loading .obj/.fbx meshes with deduplication and normal/tangent computation. Updated GraphicsTestWindow to use the new loader and improved resource management. Fixed D3D12ResourceAllocator resource creation logic, improved camera projection math, and simplified RenderingLayerMask. Updated package references and app display name.

BREAKING CHANGE: Vertex struct layout changed; all mesh code and shaders must use the new format.
2026-04-01 00:06:31 +09:00
89e6c68f2a feat(rhi)!: refactor resource handles to GPUTexture/GPUBuffer
Refactored all graphics resource handles to use Handle<GPUTexture> and Handle<GPUBuffer> instead of Handle<Texture> and Handle<GraphicsBuffer>. Updated all APIs, interfaces, and implementations to use the new types, including ICommandBuffer, IResourceAllocator, ISwapChain, IRenderOutput, IRenderGraphBuilder, and related classes. Introduced TempJobAllocator for frame-latency-aware allocations. Updated ResourceHandleExtensions for new conversions. Performed minor code cleanups and removed the empty ClusterLod.cs file.

BREAKING CHANGE: All usages of Handle<Texture> and Handle<GraphicsBuffer> are replaced with Handle<GPUTexture> and Handle<GPUBuffer>. This affects all APIs and resource management code. Callers must update their code to use the new handle types.
2026-03-30 21:27:16 +09:00
b28b32f502 feat(ui): migrate ProjectBrowser to GridView, improve cleanup
Refactored ProjectBrowser to use GridView instead of ItemsView for file display, updated selection logic, and set a minimum grid height. FloatingWindow now manages DockingLayout cleanup on close to prevent resource leaks. Simplified DockDocument and DockGroup instantiations in EngineEditorWindow. Updated GetDirectoryNameConverter to use Path.GetDirectoryName directly. App shutdown now calls EditorApplication.Shutdown(). Added Ghost.Engine reference in ActivationHandler.
2026-03-29 19:55:05 +09:00
fa617accc3 fix(docking): restore incremental layout updates to prevent visual tree rebuilds 2026-03-29 19:32:44 +09:00
ff22b89ba3 feat(docking): implement proportional sizing for docking layout
- Add DockLength property to DockModule to track star weights
- Add SyncLengths() to DockPanel to capture current Grid weights
- Update DockPanel.UpdateLayoutStructure to use DockLength
- Update DockingLayout.SplitGroup to distribute weights when splitting
2026-03-29 19:02:21 +09:00
2e6e705558 fix: update DockGroup to use DockTabItem and fix tab selection 2026-03-29 18:51:52 +09:00
e6e38f5eea fix(docking): defer container cleanup to avoid visual tree modification during layout 2026-03-29 18:47:38 +09:00
d15bd22743 feat(docking): improve tab management and error handling
Refactored DockGroup to only remove obsolete TabViewItems and restore tab selection more reliably. Updated DockGroup.xaml to enable tab reordering and add-tab button. Switched to CommunityToolkit.WinUI.Controls for GridSplitter and added a style for it. Made DockPanel, DockRegionHighlight, and DockingLayout partial classes. In App.xaml.cs, wrapped initialization in a try-catch to exit on error, and ensured process exit on window close. Improved ProjectBrowser scrollbar behavior and layout settings.
2026-03-29 16:07:18 +09:00
15870ffe89 refactor: adopt WinUI.Dock approach for layout updates and tab content hosting 2026-03-29 15:16:08 +09:00
70b7e56eb7 feat(editor): implement scroll state saving for ProjectBrowser ItemsView 2026-03-29 14:54:26 +09:00
257838b33e refactor: make DockPanel grid definitions incremental to preserve virtualization state 2026-03-29 14:18:49 +09:00
8ff98c56be refactor: remove SizeChanged hack from ProjectBrowser 2026-03-29 14:17:19 +09:00
2c84696994 fix: make DockPanel layout updates incremental to preserve visual tree state 2026-03-29 14:03:38 +09:00
a33a150d06 fix: resolve root module null reference and itemsview scroll bug 2026-03-29 12:30:07 +09:00
60ef684d80 Fixed package version issue. 2026-03-29 01:24:21 +09:00
90ac5e6d4b Untrak the NUL file 2026-03-29 01:21:41 +09:00
bd13e7faa0 Merge branch 'develop' into feature/docking-layout
# Conflicts:
#	src/Editor/Ghost.Editor/ActivationHandler.cs
#	src/Editor/Ghost.Editor/App.xaml
#	src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml
2026-03-29 01:20:01 +09:00
b5d8009bec Fixed the issue that crash when close. 2026-03-29 01:13:51 +09:00
3aef53cad9 fix: resolve element already child exception during tab drag and drop 2026-03-28 23:44:52 +09:00
99adf8fc3b fix: merge docking resource dictionary and add test layout 2026-03-28 23:36:23 +09:00
1c553a55fa fix(editor): ensure source container cleanup in ReplaceChild and document cross-layout moves 2026-03-28 23:10:46 +09:00
e9f822409d fix(docking): prevent layout tree loss and enforce cross-layout ownership 2026-03-28 23:07:28 +09:00
0d8bc6f868 fix(docking): improve root cleanup and simplify DockPanel cleanup logic 2026-03-28 23:05:23 +09:00
c8f24edfd8 fix(docking): address final code quality issues in docking layout 2026-03-28 23:03:32 +09:00
2946b905c6 fix(docking): address final code quality issues in docking layout 2026-03-28 23:01:28 +09:00
666528263b fix(docking): address final code quality issues in docking layout 2026-03-28 22:57:54 +09:00
c52daf3914 fix(docking): address code quality issues in DockingLayout 2026-03-28 22:54:30 +09:00
9738971369 fix(docking): address code quality issues in DockingLayout and FloatingWindow 2026-03-28 22:52:34 +09:00
af56338347 feat(docking): add floating window support 2026-03-28 22:50:28 +09:00
45711e7770 fix: address re-entrancy in ReplaceChild and invalid split in AddDocument 2026-03-28 22:48:58 +09:00
0f0b36a932 fix: address code quality issues in DockContainer and DockPanel
- Throw ArgumentException in DockContainer.ReplaceChild if newChild is already in the container to avoid index shifting bugs.
- Add comment in DockPanel.CheckCleanup explaining the asymmetric root panel collapse behavior.
2026-03-28 22:46:26 +09:00
e6d0529ef1 fix(docking): address code quality issues in Docking system 2026-03-28 22:43:44 +09:00
d367cff79f fix(docking): address code quality issues and improve structural integrity 2026-03-28 22:42:07 +09:00
35731d4ebe fix(docking): address code quality issues and improve structural integrity 2026-03-28 22:39:57 +09:00
8d3c5ecb1f fix(docking): address reentrancy and validation issues in DockContainer 2026-03-28 22:37:59 +09:00
1d48784a1c fix(docking): improve structural integrity and add null validation 2026-03-28 22:35:43 +09:00
e5aa328576 fix(docking): address code quality issues and improve docking robustness 2026-03-28 22:32:57 +09:00
55eb240de6 fix(docking): improve type safety, document retention, and container cleanup 2026-03-28 22:29:14 +09:00
45d810e01c feat(docking): implement drag and drop logic 2026-03-28 22:17:16 +09:00
1ec8496b8b fix(docking): add ownership guards and rename FindFirstLeafDockGroup 2026-03-28 22:15:55 +09:00
45375ac2ff fix(docking): address code quality issues in DockingLayout and DockRegionHighlight 2026-03-28 22:14:40 +09:00
4188152f49 fix(docking): address code quality issues in DockingLayout 2026-03-28 22:11:49 +09:00
5521a8cce2 fix(docking): address code quality issues in DockingLayout and DockRegionHighlight 2026-03-28 22:10:08 +09:00
baca976c6f feat(docking): add DockRegionHighlight and DockingLayout 2026-03-28 22:08:20 +09:00
b87e01f6b3 refactor: replace magic numbers and string literals in DockPanel 2026-03-28 22:05:54 +09:00
2fa9976658 feat(docking): add DockPanel 2026-03-28 22:01:49 +09:00
e92e365a3a fix(docking): improve DockGroup robustness and state preservation 2026-03-28 21:59:44 +09:00
09576bb6e1 fix(docking): enforce DockDocument children in DockGroup and fix style 2026-03-28 21:58:21 +09:00
332a940993 fix(docking): improve DockDocument nullability and DockGroup property reactivity 2026-03-28 21:56:05 +09:00
acbf315e8f feat(docking): add DockDocument and DockGroup 2026-03-28 21:53:50 +09:00
11101f8352 fix(editor): improve DockContainer robustness and style consistency
- Added cycle detection in AddChild to prevent tree-cycle bugs
- Added defensive null validation to AddChild and RemoveChild
- Standardized using directives and exception throwing style
2026-03-28 21:51:52 +09:00
bf40eabcac fix(docking): improve DockContainer robustness and encapsulation 2026-03-28 21:48:25 +09:00
ea4d1084e9 fix(docking): improve container consistency and re-parenting 2026-03-28 21:46:38 +09:00
47ffc01524 feat(docking): add core enums and base classes 2026-03-28 21:43:30 +09:00
5f0eea49cf docs: add docking layout implementation plan 2026-03-28 21:34:00 +09:00
51398f29d2 docs: add docking layout design spec 2026-03-28 21:23:25 +09:00
17588439fa Clean up code 2026-03-28 20:47:00 +09:00
668e66937b Fix docking layout 2026-03-28 20:45:23 +09:00
5845e7e9fb fix(dock): fix Element is already the child of another element exception 2026-03-28 19:38:32 +09:00
de71043be3 fix(dock): ensure exception-safe reentrancy guard and symmetrical event cleanup 2026-03-28 19:25:15 +09:00
3f6de84387 fix(dock): remove unused using and add event cleanup symmetry 2026-03-28 19:10:05 +09:00
975c359bf4 fix(dock): improve window lifecycle, size sync performance, and code style 2026-03-28 19:00:09 +09:00
71abd60a75 fix(dock): ensure persistent sizing capture and improve window close logic 2026-03-28 18:48:00 +09:00
777c4ef31d fix(dock): prevent render-feedback loop and improve drag state cleanup 2026-03-28 18:32:10 +09:00
3c9c95ad73 fix(dock): fix build breaks, handle size reordering, and add size change subscriptions 2026-03-28 18:25:45 +09:00
4713bfe7da fix(dock): migrate primary editor to DockLayout, add persistent sizing, and refactor DockLayout 2026-03-28 18:11:35 +09:00
9a1b8dcab0 fix(dock): migrate primary editor to DockLayout and add persistent sizing 2026-03-28 17:57:54 +09:00
ea7d3fad26 fix(dock): clean up unused variables and simplify event handling 2026-03-28 17:44:10 +09:00
c77592d479 fix(dock): fix build break and clean up logging/event patterns 2026-03-28 17:38:07 +09:00
287b3b303f fix(dock): ensure callback side-effect cleanup and improve tear-off diagnostics 2026-03-28 17:30:50 +09:00
7ac9a66110 fix(dock): complete TabTearOffService migration and restore transactional integrity 2026-03-28 17:23:49 +09:00
0a0359ec06 fix(dock): restore transactional integrity and fix build breaks 2026-03-28 17:12:17 +09:00
cda3b292b5 fix(dock): decouple DockLayout from window creation and remove redundant state 2026-03-28 17:08:24 +09:00
65a335fc1a fix(dock): make drag payload single source of truth and improve diagnostics 2026-03-28 17:03:27 +09:00
c1f7f3e14e fix(dock): use structured drag payload and namespaced property key 2026-03-28 16:58:20 +09:00
a409a93a10 fix(dock): strengthen drag payload validation and use event args in TabDroppedOutside 2026-03-28 16:52:15 +09:00
5ceb7c11ed fix(dock): add drag payload validation and ensure unconditional state cleanup 2026-03-28 16:47:08 +09:00
e80266f2bc fix(dock): restore core docking behavior and fix build breaks 2026-03-28 16:34:04 +09:00
04a3b924ab fix(dock): fix build breaks and finalize TabTearOffService 2026-03-28 16:29:34 +09:00
10bc76a654 fix(dock): centralize tear-off transaction in TabTearOffService and fix build breaks 2026-03-28 16:21:48 +09:00
c4c0b5cd87 fix(dock): centralize transactional tear-off logic and fix build break 2026-03-28 16:15:27 +09:00
08e4d3311a fix(dock): centralize tear-off logic and ensure transactional integrity 2026-03-28 16:10:25 +09:00
299bcf520c fix(dock): ensure transactional tear-off and wire main window tabs 2026-03-28 16:05:51 +09:00
304df0a381 fix(dock): complete tear-off flow and add rollback on failure 2026-03-28 16:01:42 +09:00
8c136709ff fix(dock): decouple DockLayout from App and fix multi-window shutdown 2026-03-28 15:56:53 +09:00
e83555498a fix(dock): address reviewer feedback on window tear-off 2026-03-28 15:48:56 +09:00
07274b6699 feat(dock): implement tab tear-off to new window 2026-03-28 15:41:39 +09:00
095fcc87a7 docs: generated api docs for graphics 2026-03-28 15:35:54 +09:00
419552439d fix(dock): improve mutation engine safety and revert public surface expansion 2026-03-28 15:07:39 +09:00
5efd0c8aee fix(dock): extract mutation engine to core and improve test coverage 2026-03-28 15:03:08 +09:00
b3d753fd08 fix(dock): fix InsertChild move semantics and improve test quality 2026-03-28 14:57:52 +09:00
e69e071ce2 fix(dock): refactor mutation logic and fix AddChild regression 2026-03-28 14:54:40 +09:00
231756006e test(dock): add model-level mutation and cleanup tests 2026-03-28 14:50:35 +09:00
98405cb8ec fix(dock): ensure drop safety and consistent reordering semantics 2026-03-28 14:50:11 +09:00
4aeaecfe81 fix(dock): improve drop mutation safety and tree cleanup 2026-03-28 14:46:58 +09:00
c6a71e599b fix(dock): prevent tab loss on invalid drops and improve tree cleanup 2026-03-28 14:39:26 +09:00
b194b57e4e docs: refactor document folder structure. 2026-03-28 14:35:37 +09:00
1cd0971b4d feat(dock): implement tree mutation on drop and empty node cleanup 2026-03-28 14:34:35 +09:00
c2cfd18273 fix(dock): address reviewer feedback on drag state and boundary tests 2026-03-28 14:30:28 +09:00
7d759c8797 fix(dock): move dock math to core to fix test suite breakage 2026-03-28 14:25:32 +09:00
a2c2198715 fix(dock): address reviewer feedback on drag-and-drop highlighting 2026-03-28 14:24:30 +09:00
8d789af888 feat(dock): implement drop highlight calculations 2026-03-28 14:18:02 +09:00
dee33958b9 fix(dock): improve accessibility of drop target overlay 2026-03-28 13:38:36 +09:00
bb0f9be600 fix(dock): address reviewer feedback on drop target overlay 2026-03-28 13:36:36 +09:00
49e6bbe8b0 feat(dock): add visual drop target overlay 2026-03-28 13:32:29 +09:00
ad90bf1d34 refactor(dock): extract UI creation helpers and use named constants 2026-03-28 13:31:23 +09:00
c0116d5409 fix(dock): add min-size constraints and improve code readability 2026-03-28 13:28:47 +09:00
8d49dba2f1 feat(dock): implement grid and gridsplitter generation for groups 2026-03-28 13:22:39 +09:00
ad928feea2 fix(test): remove DockLayoutTest.cs 2026-03-28 13:18:53 +09:00
17090eaa0d fix(test): remove failing UI tests and project reference 2026-03-28 13:18:40 +09:00
56b84effb6 fix(dock): address minor reviewer feedback and add unit tests 2026-03-28 13:16:04 +09:00
944687848e fix(dock): address subscription leaks and selection rerender issues 2026-03-28 13:10:08 +09:00
038a13bbe0 fix(dock): implement group layout and selection binding fixes 2026-03-28 13:05:35 +09:00
efc9e8862d fix(dock): address reviewer feedback on tree renderer 2026-03-28 12:57:20 +09:00
979f1d64a7 feat(dock): implement basic recursive tree renderer 2026-03-28 12:50:16 +09:00
87217337b7 fix(dock): encapsulate Children collection and polish tests 2026-03-28 12:45:55 +09:00
3ea4260405 fix(dock): robust selection sync, internal parent setter, and XML docs 2026-03-28 12:42:42 +09:00
4052ffb854 fix(dock): enforce tree invariants, sync selection, and fix AOT warnings 2026-03-28 12:38:29 +09:00
8ba976b0ba feat(dock): add core data models for docking system 2026-03-28 12:31:52 +09:00
f38ad04c4f docs: add dock layout implementation plan 2026-03-28 12:14:51 +09:00
dd41cafd64 docs: add dock layout system design spec 2026-03-28 12:13:38 +09:00
d8a7b07624 feat(graphics): improve rendering pipeline and docs
- Refactor D3D12 backend and RenderGraph module
- Update graphics RHI and core rendering components
- Add Random.hlsl shader include
- Regenerate API documentation and update user guides
2026-03-27 22:23:44 +09:00
0a2eb619eb Add document 2026-03-26 12:51:07 +09:00
447a4e6904 feat(render): add meshlet rendering and ECS query ref API
Introduces meshlet-based rendering pipeline with new HLSL structures and push constant layouts. Refactors meshlet upload/cooking, updates RenderGraphContext for global/view/instance data, and enhances ECS QueryBuilder with ref returns and [UnscopedRef] for fluent chaining. Improves resource management and disposal patterns, updates D3D12 interop for compatibility, and refines test/app infrastructure. Includes dependency updates, bug fixes, and code cleanups.
2026-03-25 20:27:46 +09:00
b729ca86f5 feat(meshlet): refactor meshlet pipeline and add render pass
Refactor meshlet data structures to use packed uint triangle indices, update meshlet cooking and upload logic, and align HLSL mesh shader. Add MeshRenderPass with bindless rendering and blit support. Improve RenderExtractionSystem, RootSignatureLayout, and TestRenderPipeline. Update GraphicsTestWindow for new pipeline and meshlet logic. Includes code cleanups and comments.
2026-03-25 13:13:03 +09:00
7860e5e341 feat(render): add ECS-based test render pipeline
Introduce TestRenderPipeline and settings, replacing MeshRenderPass.
The new pipeline manages per-frame instance, view, and global data buffers,
and uploads them for each render request. Refactor GraphicsTestWindow to use
ECS World, setting up camera and mesh entities. Remove MeshRenderPass and
related demo code. Add TotalRecordCount to RenderList, new data structs for
buffer uploads, and static masks to RenderingLayerMask. Update project
references and InternalsVisibleTo for Ghost.Graphics.Test access.
2026-03-24 20:14:26 +09:00
92e3d33361 feat(render): per-frame render requests & thread safety
Refactor RenderSystem to store render requests per-frame within FrameResource, improving encapsulation and resource management. Update render loop and AddRenderRequest to use the new structure, ensuring proper disposal and clearing of requests to prevent memory leaks. Remove the old global renderRequests array and update Dispose logic accordingly.

Add spin lock-based thread safety to D3D12ResourceDatabase for AddResource/AddAllocation, and introduce EnterParallelRead/ExitParallelRead methods for explicit locking.

Enhance RenderExtractionSystem and Material to support transparent render lists and a MaterialRenderType property, preparing for advanced rendering features. Includes minor code cleanups and comment improvements.
2026-03-24 16:46:30 +09:00
d44ec0be31 feat(d3d12): unify resource mgmt & add pooling system
Refactored D3D12 resource and command management with a new D3D12Object<T> base class for unified lifetime and naming of COM objects. Introduced pooled command buffer and resource management in D3D12GraphicsEngine and ResourceManager, using frame-based return queues for safe reuse. Updated RenderSystem to use pooled command buffers and render requests, and to properly dispose of per-frame resources. Changed frame synchronization and resource release logic to use ulong fence/frame values for improved robustness. Refactored swap chain to DXGISwapChain and improved error handling and code clarity. Removed renderer management from IGraphicsEngine. Changed ResourceDesc, TextureDesc, and BufferDesc to record structs with equality and hashing for pooling.

BREAKING CHANGE: Renderer management APIs removed from IGraphicsEngine. Frame and resource synchronization now use ulong instead of uint. Resource pooling and command buffer pooling are now required for correct usage.
2026-03-23 20:48:08 +09:00
2b3bf21a74 feat(engine): refactor resource mgmt & render pipeline
Refactors engine infrastructure for improved resource/service
management and render pipeline extensibility. Replaces World’s
resource API with a service-based API. Splits IGraphicsEngine’s
RenderFrame into BeginFrame/EndFrame. Adds support for pluggable
render pipelines in RenderSystem. Replaces disposed checks with
Debug.Assert in performance paths. Updates RenderExtractionSystem
and render loop for new APIs. Improves diagnostics and code clarity.

BREAKING CHANGE: Resource API replaced with service API; render
pipeline and frame lifecycle interfaces changed.
2026-03-22 21:04:05 +09:00
37f4795b4f feat(engine)!: refactor graphics, ECS, and logging APIs
Major refactor of graphics and ECS infrastructure:
- Removed IResourceManager, IRenderSystem, IFenceSynchronizer interfaces; ResourceManager and RenderSystem are now concrete classes.
- Updated all render graph, pipeline, and context code to use concrete ResourceManager.
- Refactored camera/frustum math and render extraction for clarity and correctness; frustum now uses inline arrays.
- RenderingLayerMask is now an immutable struct with bitwise operators.
- Meshlet and meshlet group data structures improved; meshlet build callback signature updated.
- Logging system overhauled: LogMessage is now a class, LogCollection supports change events, and Logger is used directly in the debug console.
- ECS query API: ChunkView.Count renamed to EntityCount; query builder/iterators use VirtualStack.Scope.
- Updated render pipeline and passes for new resource manager and render list APIs.
- Cleaned up obsolete files, improved code style, and updated documentation.
- HLSL meshlet shader updated for new struct layout.
- Debug console now uses new logger and log collection.

BREAKING CHANGE: Public APIs for resource management, rendering, ECS queries, and logging have changed. Interfaces removed; use new concrete types and updated method signatures.
2026-03-21 22:10:28 +09:00
793df1af4f Merge pull request 'feat: implement CPU meshlet baking and update pipeline shaders' (#4) from Julian/GhostEngine:feature/meshlet-pipeline into develop
Reviewed-on: #4
2026-03-20 08:09:20 +00:00
a35321df89 feat: implement CPU meshlet baking and update pipeline shaders 2026-03-20 07:53:23 +00:00
db0be367ef feat(meshopt): add typed enums and improve naming logic
Introduce SimplifyOptions and SimplifyVertexOptions enums for mesh simplification, replacing magic numbers with type-safe flags. Update MeshOptApi with strongly-typed wrapper methods. Refactor MeshletUtility to use new enums and nullable delegates, and fix stride calculation for pointer arithmetic.

Rename NamingConventions.GetMethodName to GetName, update name removal logic to use "$TBare", and add ALL_CAPS style for constants. Update config files to match new naming conventions and add ALL_CAPS constant rule for meshopt. Refactor BindingParser and related classes to support constant member kind. Apply minor bug fixes and code style improvements throughout.
2026-03-20 15:17:38 +09:00
4a98e44630 feat(meshlet)!: consolidate and modernize Cluster LOD logic
Refactored Cluster LOD mesh generation by merging ClodBounds, ClodConfig, ClodMesh, ClodGroup, ClodCluster, Cluster, and related logic into a new MeshletUtility.cs under Ghost.Graphics.Utilities.
Removed legacy Clod* files and updated to use improved memory management (UnsafeArray, Allocator.FreeList) and more idiomatic C# patterns.
Updated .csproj package versions for compatibility.
Minor code style improvements in RenderGraphResourcePool.cs.

BREAKING CHANGE: Cluster LOD API has been consolidated and refactored; previous Clod* types and entry points have been removed or replaced. Callers must update to use MeshletUtility.cs.
2026-03-18 21:18:41 +09:00
9cf03e0b6f Merge pull request 'docs: add XML summary comments to public Meshlet types and methods' (#3) from Julian/GhostEngine:feature/meshlet-docs into develop
Reviewed-on: #3
2026-03-17 04:10:16 +00:00
bc78c8fbee docs: add XML summary comments to public Meshlet types and methods 2026-03-17 04:02:28 +00:00
fe49e57330 Merge pull request 'feat: implement ClusterLOD C# bindings in Ghost.Graphics.Meshlet' (#2) from Julian/GhostEngine:develop into develop
Reviewed-on: #2
Reviewed-by: Misaki <misaki_39@outlook.com>
2026-03-17 03:52:12 +00:00
2376fc9414 fix: resolve all build errors in Meshlet LOD pipeline
- Use correct UnsafeList constructor (int capacity, Allocator)
- Use .Count instead of .Length for UnsafeList
- Cast GetUnsafePtr() to typed pointers explicitly
- Use Api.meshopt_* constants (not MeshOptApi) for simplify flags
- Use meshopt_Meshlet instance methods BuildsFlex/BuildsSpatial
- Use correct meshopt_Meshlet field names (vertex_offset, triangle_offset, etc.)
- Fix byte constant overflow with unchecked cast in LockBoundary
- Add Ghost.MeshOptimizer project reference to Ghost.Graphics.csproj
2026-03-17 03:14:09 +00:00
0a3502b858 refactor: optimize memory for mega-meshes and fix collection usage
- Switch large/dynamic collections from StackScope to Allocator.Temp/Persistent to prevent stack overflow
- Remove redundant Resize calls; use capacity in constructor + Add or AsSpan().Fill for initialization
- Correctly propagate Allocator parameter for returned collections
- Ensure all temporary collections are properly disposed before returning
- Refine ClodBuilder, ClodPartition, ClodBoundsHelper, and ClodSimplify for high-scale mesh processing
2026-03-17 02:34:42 +00:00
22fdae1061 cleanup: remove obsolete Clod.cs file 2026-03-17 02:21:01 +00:00
92c503b253 refactor: address PR review feedback on meshlet LOD system
- Remove WORK_SUMMARY.md
- Use Debug.Assert for stride validation in ClodBuilder
- Fix ClodBuilder.Build return value after cluster disposal
- Update ClodPartition to accept AllocationHandle for return collections
- Standardize on camelCase for public fields in ClodConfig, ClodMesh, ClodBounds, etc.
- Remove redundant Resize calls where capacity suffices or Add is used
- Enforce stack allocator usage for internal temporary collections
- Ensure proper allocator propagation for collections returned from methods
2026-03-17 02:18:37 +00:00
f7fb7da496 fix: correct API calls and cleanup documentation
- Replace .Ptr with .GetUnsafePtr() for UnsafeList access
- Use proper MeshOptApi method names (CamelCase): ComputeClusterBounds, BuildMeshletsBound, PartitionClusters, etc.
- Fix SimplifyVertex_Protect constant access
- Remove IMPLEMENTATION_COMPLETE.md
- Rename AGENT_GUIDELINES.md to AGENT.md
2026-03-17 00:23:42 +00:00
2ba60c4bae refactor: improve unsafe collection API usage per review
- Replace float[3] with Vector3 (System.Numerics)
- Use UnsafeList.GetUnsafePtr() instead of fixed blocks
- Use AllocationManager.CreateStackScope() for temporary collections
- Remove redundant properties in ClodConfig (public fields suffice)
- Use MeshOptApi directly instead of Api alias
- Fix method signatures to not require allocator for stack-scoped collections
2026-03-16 23:55:19 +00:00
f2b68955b1 docs: comprehensive implementation summary 2026-03-16 16:12:33 +00:00
85a000e5c4 docs: add work summary for clusterlod translation 2026-03-16 16:05:02 +00:00
301a6d1c45 feat: translate clusterlod to C# and restructure to Ghost.Graphics.Meshlet 2026-03-16 16:01:57 +00:00
e831b71a79 feat(bindings): update C# wrappers for meshopt, nvtt, ufbx
Refactor and regenerate native C# bindings for Ghost.MeshOptimizer, Ghost.Nvtt, and Ghost.Ufbx to match updated native APIs and improve usability.
- Replace meshoptimizer.dll with newer version.
- Move meshoptimizer functions to static methods in partial class; add new meshlet, simplification, quantization features.
- Remove enum wrappers in favor of constants; delete meshopt_Allocator.cs.
- Regenerate native wrappers with PascalCase naming, XML doc comments, and aggressive inlining.
- Implement IDisposable for resource structs; update configs for naming, documentation, and method mapping.
- Update user code to use new wrapper classes and method names.
- Improve documentation and comments for clarity.

BREAKING CHANGE: API surface changes, wrapper class and method names updated, enum wrappers removed, custom allocator deleted.
2026-03-17 00:19:54 +09:00
9bae3e647e docs(README): rewrite and expand documentation
Major rewrite of the README for Ghost.NativeWrapperGen:
- Clarifies project purpose, design, and intended usage.
- Adds concrete code examples of generated wrappers.
- Updates CLI usage and provides validated command lines.
- Replaces old config schema docs with detailed tables for all config fields, including new `remaps` and `actions`.
- Documents the new config-driven routing and remapping system.
- Explains the action-based method routing with conditions and apply steps.
- Details naming conventions and method name derivation.
- Provides a full, modern config example for nvtt.
- Updates recommended workflow for wrapper regeneration and build.
- Emphasizes the design principle of keeping policy in config.
- Removes outdated sections and limitations.
- Improves formatting and accessibility for new users.
2026-03-15 21:15:52 +09:00
6cadd8edeb feat(nativegen)!: refactor to struct-based native wrappers
Major overhaul of native wrapper generation for ufbx and nvtt.
Replaces all hand-written and class-based wrappers with auto-generated partial struct wrappers that directly expose native API methods via pointers. Introduces a new JSON-driven configuration system using "remaps" and "actions" for flexible parameter/return mapping and method routing. Removes legacy config sections and helper classes, focusing solely on method wrappers. Updates all usages and tests to use the new pointer-based API. Cleans up obsolete code and ensures resource management is handled via struct Dispose methods. The result is a thinner, more direct, and maintainable interop layer.

BREAKING CHANGE: All managed wrapper classes and helpers are removed in favor of struct-based pointer wrappers. API usage and resource management patterns have changed.
2026-03-15 20:48:54 +09:00
3e4084c42a Added ufbx warper 2026-03-15 02:19:40 +09:00
cce1cf7256 Added Ufbx 2026-03-14 18:29:18 +09:00
254b08bc81 Added doc folder 2026-03-14 12:33:12 +09:00
912b320d8f Fixed compilation errors;
Added MaterialPalette
2026-03-14 12:27:49 +09:00
8a3b40b4f8 Refactor MeshInstance 2026-03-13 15:10:25 +09:00
619720feee Render extraction system & ECS/graphics refactor
Introduced RenderExtractionSystem for entity-based render data extraction. Added MeshInstance and MeshPalette components with shadow casting support. Refactored QueryBuilder API, SharedComponentStore, and component registration for clarity and flexibility. Updated SystemManager and SystemGroup to use SystemAPI. Replaced RenderingConfig with GraphicsEngineDesc/RenderSystemDesc. RenderFrame now uses CPU/GPU fence values for sync. Removed Camera.cs in favor of ECS-based rendering. Improved Material, RenderingLayerMask, Mesh, and RenderList APIs. Updated package references and fixed naming, error handling, and disposal issues.
2026-03-08 22:51:03 +09:00
bfe8588d76 Refactor render pipeline and resource management APIs
Split IFenceSynchronizer/IRenderSystem interfaces for clarity. Refactor D3D12GraphicsEngine to use IFenceSynchronizer. Update RenderGraph and context to use explicit resource manager/database/allocator references. Add multi-buffering methods to IRGBuilder (stub). Support history access for multi-frame resources. Remove legacy RenderPipelineBase; introduce IRenderPipelineSettings and sealed GhostRenderPipeline. Clean up resource aliasing and pool logic. Improve modularity and future extensibility.
2026-03-03 20:14:22 +09:00
b8af6e8c3a Added RenderPipelineBase 2026-03-02 19:06:19 +09:00
5e42d699c3 Added RenderList and RenderReques.
Added IRenderPipeline.
2026-02-28 21:31:38 +09:00
6f802ac12b Added CopyTexture support in ICommandBuffer 2026-02-26 21:40:07 +09:00
162b71f309 Refactor render graph error handling and resource APIs
- RenderGraph.Compile/Execute now return Error for better failure detection; error handling is propagated throughout compiler and executor.
- Renamed ScheduleReleaseResource to ReleaseResource for clarity; updated all usages.
- ResourceManager now calls ReleaseResource directly on Mesh, Material, and Shader types.
- Camera exposes Actual/Virtual size properties and Render returns Error.
- RenderingContext now uses IResourceManager for mesh/resource ops.
- Replaced custom BinaryWriter with BufferWriter in RenderGraphHasher.
- Improved variable naming, interface signatures, and code formatting.
- Added Error extension for IsSuccess/IsFailure.
- Minor FMOD/native interop and test code cleanups.
- No breaking API changes except for new Error return values on some methods.
2026-02-25 19:08:54 +09:00
30090f84ab Refactor rendering projects 2026-02-24 20:08:26 +09:00
93c58fa7fb Add Ghost.Nvtt C# wrapper and integrate nvtt texture pipeline
- Introduce full managed C# wrapper for NVIDIA Texture Tools (nvtt) with safe handle classes, idiomatic APIs, and managed callback support.
- Integrate Ghost.Nvtt into Ghost.Editor.Core and Ghost.MicroTest; update TextureAssetHandler to use the new nvtt wrapper for texture compression.
- Add comprehensive end-to-end binding test (NvttBindingTest).
- Refactor D3D12 resource management: add deferred/immediate release APIs, update allocator/database usage, and ensure proper resource cleanup.
- Update project files for new native DLL layout and dependency versions.
- Minor API cleanups: EditorApplication properties, D3D12 input layout, and removal of obsolete code.
- Update shaders, tests, and documentation for new APIs and usage patterns.
2026-02-23 17:13:10 +09:00
78e3b4ef31 Merge branch 'develop' of https://github.com/misakieku/GhostEngine into develop 2026-02-18 00:52:23 +09:00
db8ca971a8 Refactor folder structure 2026-02-18 00:52:18 +09:00
638417d4f0 Refactor folder structure 2026-02-18 00:50:46 +09:00
426786397c Modify AssetService 2026-02-05 19:25:48 +09:00
9bbccfc8f8 Update ContextFlyout 2026-02-05 13:52:53 +09:00
eadd13931f Updating ProjectBrowser 2026-02-04 19:08:18 +09:00
59991f47d5 Update editor 2026-02-03 21:49:14 +09:00
1428 changed files with 229124 additions and 20202 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.dll filter=lfs diff=lfs merge=lfs -text

9
.gitignore vendored
View File

@@ -10,6 +10,15 @@
*.userosscache
*.sln.docstates
AGENTS.md
.opencode/
.code-review-graph/
.antigravitycli/
ref/
docfx/
NUL
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

297
AGENTS.md
View File

@@ -1,297 +0,0 @@
# GhostEngine - Agent Development Guide
This guide provides essential information for AI coding agents working on the GhostEngine codebase.
## Project Overview
- **Type**: Game Engine
- **Language**: C#
- **Target Framework**: .NET 10.0
- **Special Features**: ECS architecture, D3D12 rendering, AOT compilation, WinUI 3 editor
- **Platform**: Windows (net10.0-windows10.0.22621.0 for editor projects)
## Build Commands
### Build Entire Solution
```bash
dotnet build GhostEngine.slnx
```
### Build Specific Project
```bash
dotnet build Ghost.Entities/Ghost.Entities.csproj
dotnet build Ghost.Editor/Ghost.Editor.csproj
```
### Build with Configuration
```bash
dotnet build GhostEngine.slnx -c Release
dotnet build GhostEngine.slnx -c Debug
```
### Clean Build
```bash
dotnet clean GhostEngine.slnx
dotnet build GhostEngine.slnx
```
## Test Commands
### Run All Tests (Custom Framework)
Tests use a custom test framework (not xUnit/NUnit/MSTest). Each test project is an executable.
```bash
# Run entity tests
dotnet run --project Ghost.Entities.Test/Ghost.Entities.Test.csproj
# Run shader tests
dotnet run --project Ghost.Shader.Test/Ghost.Shader.Test.csproj
```
### Run Single Test
Tests implement `ITest` interface. To run a specific test, modify the test project's `Program.cs`:
```csharp
// In Ghost.Entities.Test/Program.cs
TestRunner.Run<EntityQueryTest>(); // Run specific test
TestRunner.Run<EntityQueryTest>(10); // Run with 10 iterations
```
### Visual Tests (Graphics)
Graphics tests use WinUI 3 and require running as packaged apps:
```bash
dotnet run --project Ghost.Graphics.Test/Ghost.Graphics.Test.csproj
```
## Code Style Guidelines
### Formatting (from .editorconfig)
- **Braces**: Allman style - all opening braces on new lines
- **Line Length**: Max 400 characters (very permissive)
- **Single-line statements**: Preserved (allowed)
- **Single-line blocks**: Preserved (allowed)
```csharp
// Correct brace style
public void Method()
{
if (condition)
{
DoSomething();
}
}
```
### Imports
- **System directives**: NOT sorted first (dotnet_sort_system_directives_first = false)
- **No grouping**: Import directives not separated by blank lines
- **Order**: Organize by project convention, not alphabetically
```csharp
using Ghost.Core;
using Ghost.Entities;
using Misaki.HighPerformance.Collections;
using System.Diagnostics;
using TerraFX.Interop.DirectX;
```
### Types and Nullability
- **Nullable**: Enabled for all projects
- **Implicit usings**: Enabled
- **Unsafe code**: Allowed in most projects (AllowUnsafeBlocks = True)
- **Primary constructors**: NOT preferred (csharp_style_prefer_primary_constructors = false)
### Naming Conventions
- **Classes/Interfaces**: PascalCase (`EntityManager`, `ICommandBuffer`)
- **Methods**: PascalCase (`CreateEntity`, `GetComponent`)
- **Properties**: PascalCase (`IsSuccess`, `Value`)
- **Fields (private)**: Camel case with underscore prefix (`_entityLocations`, `_world`)
- **Fields (public/internal)**: Camel case, no prefix for struct fields (`archetypeID`, `chunkIndex`)
- **Type parameters**: Single letter or PascalCase (`T`, `TComponent`)
- **Constants**: PascalCase (no SCREAMING_SNAKE_CASE)
```csharp
public class EntityManager
{
private readonly World _world; // Private field
private UnsafeSlotMap<EntityLocation> _entityLocations;
public World World => _world; // Property
public Entity CreateEntity() { } // Method
}
internal struct EntityLocation // Struct
{
public int archetypeID; // Public struct field
public int chunkIndex;
}
```
### Error Handling
**Use Result Types** - Railway-oriented programming pattern:
```csharp
// Custom result types defined in Ghost.Core
public ErrorStatus DoOperation()
{
return ErrorStatus.None; // or ErrorStatus.NotFound, etc.
}
public Result<T> GetValue()
{
if (success)
return Result<T>.Success(value);
else
return Result<T>.Failure("Error message");
}
public Result<T, ErrorStatus> GetValueWithStatus()
{
if (success)
return value; // Implicit conversion
else
return ErrorStatus.NotFound; // Implicit conversion
}
// Extension methods for checking results
result.ThrowIfFailed();
var value = result.GetValueOrThrow();
var value = result.GetValueOrDefault(defaultValue);
if (result.TryGetValue(out var value)) { }
```
**Error Status Values**: None, NotFound, InvalidArgument, InvalidState, InternalError, PermissionDenied, NotSupported, OutOfMemory, Timeout, Cancelled, UnknownError
### Memory and Performance
- **Use unsafe code** when needed for performance-critical paths
- **Span<T> and stackalloc**: Prefer for temporary allocations
- **ref returns**: Use for zero-copy access to internal data
- **Allocator patterns**: Use `Allocator.Persistent` for long-lived allocations
- **AllocationManager**: Create stack scopes for temporary allocations
```csharp
// Stack allocation pattern
var entities = (Span<Entity>)stackalloc Entity[1];
// Using allocation scope
using var scope = AllocationManager.CreateStackScope();
var batchDestroy = new UnsafeList<EntityLocation>(entities.Length, scope.AllocationHandle);
// Ref returns for zero-copy access
public ref T GetSingleton<T>() where T : unmanaged, IComponent
{
var ptr = GetSingleton(ComponentTypeID<T>.Value);
return ref *(T*)ptr;
}
```
### Type Safety Patterns
**Strongly-typed identifiers**:
```csharp
Identifier<IComponent> componentID;
Identifier<Archetype> archetypeID;
Handle<T> resourceHandle;
```
**Generic constraints**:
```csharp
public void Method<T>() where T : unmanaged, IComponent
public void Method<T, E>() where E : struct, Enum
```
### Documentation
- **XML comments**: Required for public APIs
- **Summary tags**: Describe what, not how
- **Remarks**: Add for complex behavior, thread-safety warnings, structural changes
```csharp
/// <summary>
/// Create an entity with specified components.
/// </summary>
/// <param name="set">A set of component space IDs to add to the entities.</param>
/// <returns>The created entity.</returns>
/// <remarks>
/// This method causes structural changes and is not thread-safe.
/// Use <see cref="EntityCommandBuffer"/> to defer changes.
/// </remarks>
public Entity CreateEntity(ComponentSet set) { }
```
### Common Patterns
**ECS Component Registration**:
```csharp
// Type-safe component ID
ComponentTypeID<Transform>.Value
// Component sets for archetypes
var set = new ComponentSet(ComponentTypeID<Transform>.Value, ComponentTypeID<Velocity>.Value);
```
**Disposal Pattern**:
```csharp
private bool _disposed;
~MyClass()
{
Dispose();
}
public void Dispose()
{
if (_disposed) return;
// Cleanup code
_disposed = true;
GC.SuppressFinalize(this);
}
```
**Debug-only validation**:
```csharp
#if DEBUG || GHOST_EDITOR
if (!_isSuccess)
{
throw new InvalidOperationException($"Error: {_message}");
}
#endif
```
## Architecture Notes
### Entity Component System (ECS)
- Archetype-based storage (similar to Unity DOTS)
- Component data stored in chunks
- Queries use bitset signatures for fast matching
- Structural changes move entities between archetypes
### Graphics (D3D12)
- Hardware abstraction via `ICommandBuffer`
- Resource lifetime managed via handles
- Pipeline state objects (PSO) cached in library
- Native interop via TerraFX.Interop
### Custom Dependencies
- `Misaki.HighPerformance.*`: High-performance collections and utilities
- `TerraFX.Interop.*`: Native Windows/DirectX interop
- Custom source generators in `Ghost.Generator`
## Important Rules
1. **Never disable nullable warnings** - fix the root cause
2. **Use Result types** instead of throwing exceptions for expected failures
3. **Document thread-safety** in XML comments for public APIs
4. **AllowUnsafeBlocks** is enabled - use unsafe code when it improves performance
5. **Avoid collection expressions/initializers** (disabled in .editorconfig)
6. **Prefer explicit over implicit** - clarity over brevity
7. **Test changes** by running the appropriate test project executable

View File

@@ -1,11 +0,0 @@
namespace Ghost.Core.Contracts;
public interface ICloneable
{
object Clone();
}
public interface ICloneable<T>
{
T Clone();
}

View File

@@ -1,6 +0,0 @@
namespace Ghost.Core.Contracts;
internal interface IReleasable
{
void InternalRelease();
}

View File

@@ -1,32 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
<DefineConstants>$(DefineConstants);PLATEFORME_WIN64</DefineConstants>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
<DefineConstants>$(DefineConstants);PLATEFORME_WIN64</DefineConstants>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.4" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.2.2" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.3.3" />
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.1" />
<PackageReference Include="System.IO.Hashing" Version="10.0.1" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
<PackageReference Include="ZLinq" Version="1.5.4" />
</ItemGroup>
</Project>

View File

@@ -1,100 +0,0 @@
namespace Ghost.Core.Graphics;
public enum KeywordSpace
{
Local,
Global,
}
public enum ShaderPropertyType
{
None,
Float, Float2, Float3, Float4,
Float4x4,
Int, Int2, Int3, Int4,
UInt, UInt2, UInt3, UInt4,
Bool, Bool2, Bool3, Bool4,
Texture2D, Texture3D, TextureCube,
Texture2DArray, TextureCubeArray,
Sampler
}
public struct ShaderEntryPoint
{
public string entry;
public string shader;
public readonly bool IsCreated => !string.IsNullOrEmpty(entry) && !string.IsNullOrEmpty(shader);
}
public struct KeywordsGroup
{
public KeywordSpace space;
public List<string> keywords;
}
public struct PropertyDescriptor
{
public ShaderPropertyType type;
public string name;
public object? defaultValue;
}
public struct PassDescriptor
{
public string identifier;
public string name;
public ShaderEntryPoint taskShader;
public ShaderEntryPoint meshShader;
public ShaderEntryPoint pixelShader;
public string[] defines;
public string[] includes;
public KeywordsGroup[] keywords;
public PipelineState localPipeline;
public string? hlsl;
}
public class ShaderDescriptor
{
public string name = string.Empty;
public uint cbufferSize;
public PropertyDescriptor[] globalProperties = null!;
public PropertyDescriptor[] properties = null!;
public PassDescriptor[] passes = null!;
public string? hlsl;
}
public static class ShaderDescriptorExtensions
{
public static uint GetSize(this ShaderPropertyType type)
{
return type switch
{
ShaderPropertyType.Float => 4,
ShaderPropertyType.Float2 => 8,
ShaderPropertyType.Float3 => 12,
ShaderPropertyType.Float4 => 16,
ShaderPropertyType.Float4x4 => 64,
ShaderPropertyType.Int => 4,
ShaderPropertyType.Int2 => 8,
ShaderPropertyType.Int3 => 12,
ShaderPropertyType.Int4 => 16,
ShaderPropertyType.UInt => 4,
ShaderPropertyType.UInt2 => 8,
ShaderPropertyType.UInt3 => 12,
ShaderPropertyType.UInt4 => 16,
ShaderPropertyType.Bool => 4,
ShaderPropertyType.Bool2 => 8,
ShaderPropertyType.Bool3 => 12,
ShaderPropertyType.Bool4 => 16,
ShaderPropertyType.Texture2D => 4, // Bindless resource use uint32
ShaderPropertyType.Texture3D => 4,
ShaderPropertyType.TextureCube => 4,
ShaderPropertyType.Texture2DArray => 4,
ShaderPropertyType.TextureCubeArray => 4,
ShaderPropertyType.Sampler => 4,
_ => 0,
};
}
}

View File

@@ -1,214 +0,0 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
namespace Ghost.Core;
public enum LogLevel
{
Info,
Warning,
Error
}
public readonly struct LogMessage
{
public LogLevel Level
{
get;
}
public string Message
{
get;
}
public string? StackTrace
{
get;
}
public DateTime Timestamp
{
get;
}
public LogMessage(LogLevel level, string message, string? stackTrace = null)
{
Level = level;
Message = message;
StackTrace = stackTrace;
Timestamp = DateTime.Now;
}
public override string ToString()
{
if (StackTrace != null)
{
return $"{Timestamp:HH:mm:ss} [{Level}] {Message}\n{StackTrace}";
}
return $"{Timestamp:HH:mm:ss} [{Level}] {Message}";
}
}
public interface ILogger
{
ReadOnlyObservableCollection<LogMessage> Logs
{
get;
}
void Log(string message, LogLevel level);
void Log(Exception exception);
void Assert(bool condition, string message);
void Clear();
}
public static class Logger
{
// TODO: Add file logging.
private class LoggerImpl : ILogger
{
private readonly ObservableCollection<LogMessage> _logs = new();
private readonly ReadOnlyObservableCollection<LogMessage> _readOnly;
private readonly Lock _lock = new();
public ReadOnlyObservableCollection<LogMessage> Logs => _readOnly;
public LoggerImpl()
{
_readOnly = new ReadOnlyObservableCollection<LogMessage>(_logs);
}
[StackTraceHidden]
public void Log(string message, LogLevel level)
{
lock (_lock)
{
_logs.Add(new LogMessage(level, message));
}
}
[StackTraceHidden]
public void Log(Exception exception)
{
lock (_lock)
{
_logs.Add(new LogMessage(LogLevel.Error, exception.Message, exception.StackTrace));
}
}
[StackTraceHidden]
public void Assert(bool condition, string message)
{
lock (_lock)
{
if (!condition)
{
Log(message, LogLevel.Error);
}
}
}
public void Clear()
{
lock (_lock)
{
_logs.Clear();
}
}
}
private static readonly ILogger s_logger = new LoggerImpl();
public static ReadOnlyObservableCollection<LogMessage> Logs => s_logger.Logs;
[StackTraceHidden]
public static void Log(LogLevel level, object? message)
{
s_logger.Log(message?.ToString() ?? "null", level);
}
[StackTraceHidden]
public static void Log(LogLevel level, string message)
{
s_logger.Log(message, level);
}
[StackTraceHidden]
public static void Log(LogLevel level, string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), level);
}
[StackTraceHidden]
public static void LogInfo(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Info);
}
[StackTraceHidden]
public static void LogInfo(string message)
{
s_logger.Log(message, LogLevel.Info);
}
[StackTraceHidden]
public static void LogInfo(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Info);
}
[StackTraceHidden]
public static void LogWarning(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning);
}
[StackTraceHidden]
public static void LogWarning(string message)
{
s_logger.Log(message, LogLevel.Warning);
}
[StackTraceHidden]
public static void LogWarning(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Warning);
}
[StackTraceHidden]
public static void LogError(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Error);
}
[StackTraceHidden]
public static void LogError(string message)
{
s_logger.Log(message, LogLevel.Error);
}
[StackTraceHidden]
public static void LogError(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Error);
}
[StackTraceHidden]
public static void LogError(Exception ex)
{
s_logger.Log(ex);
}
[StackTraceHidden]
public static void Assert(bool condition, string message)
{
s_logger.Assert(condition, message);
}
public static void Clear()
{
s_logger.Clear();
}
}

View File

@@ -1,67 +0,0 @@
using System.Runtime.CompilerServices;
namespace Ghost.Core;
public readonly struct TypeHandle
{
public readonly IntPtr Value
{
get;
}
private TypeHandle(IntPtr value)
{
Value = value;
}
/// <summary>
/// Gets the space handle for the specified space.
/// </summary>
/// <param name="type">The space to get the handle for.</param>
/// <returns>The space handle as a nint.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TypeHandle Get(Type type) => new TypeHandle(type.TypeHandle.Value);
/// <summary>
/// Gets the space handle for the specified space.
/// </summary>
/// <typeparam name="T">The space to get the handle for.</typeparam>
/// <returns>The space handle as a nint.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TypeHandle Get<T>() => Get(typeof(T));
/// <summary>
/// Converts a TypeHandle to a Type.
/// </summary>
/// <param name="handle">The TypeHandle to convert.</param>
/// <returns>The corresponding Type.</returns>
public Type? ToType()
{
return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(Value));
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static implicit operator TypeHandle(IntPtr value)
{
return new TypeHandle(value);
}
public static implicit operator IntPtr(TypeHandle handle)
{
return handle.Value;
}
public static implicit operator TypeHandle(Type type)
{
return Get(type);
}
public static implicit operator Type?(TypeHandle handle)
{
return handle.ToType();
}
}

View File

@@ -1,9 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ghost.Core.Utilities;
internal class EnumUtility
{
}

View File

@@ -1,12 +0,0 @@
using Ghost.Core.Contracts;
namespace Ghost.Core.Utilities;
internal static class InternalResource
{
public static void Release<T>(ref T? resource)
where T : IReleasable
{
resource?.InternalRelease();
}
}

View File

@@ -1,115 +0,0 @@
using Misaki.HighPerformance.LowLevel;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using TerraFX.Interop.Windows;
namespace Ghost.Core.Utilities;
[SupportedOSPlatform("windows10.0.19041.0")]
internal static unsafe partial class Win32Utility
{
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly ref struct IID_PPV
{
public readonly Guid* iid;
public readonly void** ppv;
public IID_PPV(Guid* iid, void** ppv)
{
this.iid = iid;
this.ppv = ppv;
}
public void Deconstruct(out Guid* iid, out void** ppv)
{
iid = this.iid;
ppv = this.ppv;
}
}
public static Guid* IID_NULL
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IID.IID_NULL));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IID_PPV IID_PPV_ARGS<T>(ComPtr<T>* comPtr)
where T : unmanaged, IUnknown.Interface
{
return new IID_PPV(Windows.__uuidof<T>(), (void**)comPtr);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Attach<T>(ref this UniquePtr<T> uPtr, T* other)
where T : unmanaged, IUnknown.Interface
{
var ptr = uPtr.Get();
if (ptr != null)
{
var refCount = ptr->Release();
Debug.Assert((refCount != 0) || (ptr != other));
}
uPtr = new UniquePtr<T>(other);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Dispose<T>(ref this UniquePtr<T> uPtr)
where T : unmanaged, IUnknown.Interface
{
var ptr = uPtr.Detach();
if (ptr != null)
{
ptr->Release();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result ToResult(this HRESULT hr, [CallerArgumentExpression(nameof(hr))] string? op = null)
{
if (hr.SUCCEEDED)
{
return Result.Success();
}
return Result.Failure($"{op} failed with code {hr}");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void** ReleaseAndGetVoidAddressOf<T>(ref this ComPtr<T> comPtr)
where T : unmanaged, IUnknown.Interface
{
return (void**)comPtr.ReleaseAndGetAddressOf();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ComPtr<T> Move<T>(ref this ComPtr<T> comPtr)
where T : unmanaged, IUnknown.Interface
{
var copy = default(ComPtr<T>);
comPtr.Swap(ref copy);
return copy;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasFlag<T>(this uint flags, T flag)
where T : Enum
{
return (flags & Unsafe.As<T, uint>(ref flag)) != 0;
}
extension(MemoryLeakException)
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfRefCountNonZero(uint count)
{
if (count != 0)
{
throw new MemoryLeakException($"Reference count is not zero: {count}");
}
}
}
}

View File

@@ -1,323 +0,0 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderParser;
using System.Text;
namespace Ghost.DSL.ShaderCompiler;
public struct DSLShaderError
{
public string message;
public int line;
public int column;
public override readonly string ToString()
{
return $"Error at {line}:{column} - {message}";
}
}
internal static class DSLShaderCompiler
{
private const string _GLOBAL_PROPERTY_FILE_NAME = "GlobalData.g.hlsl";
private const string _GENERATED_FILE_HEADER = "// Auto-generated shader file. Please do not edit this file directly.";
private static string GetPassUniqueId(DSLShaderSemantics shader, PassSemantic pass)
{
return $"{shader.name}_{pass.name}";
}
private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent)
{
if (semantic == null)
{
return parent;
}
return new PipelineState
{
ZTest = semantic.zTest ?? parent.ZTest,
ZWrite = semantic.zWrite ?? parent.ZWrite,
Cull = semantic.cull ?? parent.Cull,
Blend = semantic.blend ?? parent.Blend,
ColorMask = semantic.colorMask ?? parent.ColorMask
};
}
private static uint CalculateCBufferSize(ReadOnlySpan<PropertyDescriptor> properties)
{
if (properties.IsEmpty)
{
return 0;
}
var currentOffset = 0u;
foreach (var prop in properties)
{
var size = prop.type.GetSize();
if ((currentOffset % 16) + size > 16)
{
currentOffset = (currentOffset + 15u) & ~15u;
}
currentOffset += size;
}
return (currentOffset + 15u) & ~15u;
}
// TODO: Implement shader inheritance resolution, including property and pass merging.
// Currently, we just ignore inheritance.
public static ShaderDescriptor ResolveShader(DSLShaderSemantics semantics)
{
var descriptor = new ShaderDescriptor
{
name = semantics.name,
hlsl = semantics.hlsl
};
var shaderGlobalProperties = semantics.properties?
.Where(p => p.scope == PropertyScope.Global)
.Select(p => new PropertyDescriptor
{
name = p.name,
type = p.type,
defaultValue = p.defaultValue
}).ToArray();
var shaderLocalProperties = semantics.properties?
.Where(p => p.scope == PropertyScope.Local)
.Select(p => new PropertyDescriptor
{
name = p.name,
type = p.type,
defaultValue = p.defaultValue
}).ToArray();
descriptor.globalProperties = shaderGlobalProperties ?? Array.Empty<PropertyDescriptor>();
descriptor.properties = shaderLocalProperties ?? Array.Empty<PropertyDescriptor>();
descriptor.cbufferSize = CalculateCBufferSize(descriptor.properties);
if (semantics.passes != null)
{
descriptor.passes = new PassDescriptor[semantics.passes.Count];
for (int i = 0; i < semantics.passes.Count; i++)
{
var pass = semantics.passes[i];
var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default);
descriptor.passes[i] = new PassDescriptor
{
identifier = GetPassUniqueId(semantics, pass),
name = pass.name,
taskShader = pass.taskShader,
meshShader = pass.meshShader,
pixelShader = pass.pixelShader,
localPipeline = localPipeline,
defines = pass.defines?.ToArray() ?? Array.Empty<string>(),
includes = pass.includes?.ToArray() ?? Array.Empty<string>(),
keywords = pass.keywords?.ToArray() ?? Array.Empty<KeywordsGroup>(),
hlsl = pass.hlsl
};
}
}
else
{
descriptor.passes = Array.Empty<PassDescriptor>();
}
return descriptor;
}
public static Result<ShaderDescriptor> CompileShader(string shaderPath, string generatedOutputDirectory)
{
try
{
var source = File.ReadAllText(shaderPath);
// Use ANTLR4 parser
var shaderModels = AntlrShaderCompiler.ParseShaders(source, out var parseErrors);
if (parseErrors.Count != 0)
{
var errorMessages = new StringBuilder();
foreach (var error in parseErrors)
{
errorMessages.AppendLine(error.ToString());
}
return Result.Failure("Failed to parse shader due to errors:\n" + errorMessages.ToString());
}
if (shaderModels.Count == 0)
{
return Result.Failure("No shader found in the provided file.");
}
// Convert to semantics
var model = AntlrShaderCompiler.ConvertToSemantics(shaderModels[0], out var errors);
if (errors.Count != 0 || model == null)
{
var errorMessages = new StringBuilder();
foreach (var error in errors)
{
errorMessages.AppendLine(error.ToString());
}
return Result.Failure("Failed to compile shader due to errors:\n" + errorMessages.ToString());
}
var desc = ResolveShader(model);
var globalPropResult = GenerateGlobalProperties(desc.globalProperties, generatedOutputDirectory);
if (globalPropResult.IsFailure)
{
return Result.Failure("Failed to generate global properties: " + globalPropResult.Message);
}
var generatedResult = GenerateShaderCode(desc, generatedOutputDirectory);
if (generatedResult.IsFailure)
{
return Result.Failure("Failed to generate pass files: " + generatedResult.Message);
}
foreach (ref var pass in desc.passes.AsSpan())
{
if (pass.includes == null)
{
pass.includes = new string[2];
}
else
{
Array.Resize(ref pass.includes, pass.includes.Length + 2);
// Shift existing includes to make room for the two new includes at the front.
pass.includes.AsSpan(0, pass.includes.Length - 2).CopyTo(pass.includes.AsSpan(2));
}
pass.includes[0] = globalPropResult.Value;
pass.includes[1] = generatedResult.Value;
}
return desc;
}
catch (Exception ex)
{
return Result.Failure("Failed to compile shader: " + ex.Message);
}
}
private static string ShaderPropertyTypeToHLSLType(ShaderPropertyType type)
{
return type switch
{
ShaderPropertyType.Float => "float",
ShaderPropertyType.Float2 => "float2",
ShaderPropertyType.Float3 => "float3",
ShaderPropertyType.Float4 => "float4",
ShaderPropertyType.Int => "int",
ShaderPropertyType.Int2 => "int2",
ShaderPropertyType.Int3 => "int3",
ShaderPropertyType.Int4 => "int4",
ShaderPropertyType.UInt => "uint",
ShaderPropertyType.UInt2 => "uint2",
ShaderPropertyType.UInt3 => "uint3",
ShaderPropertyType.UInt4 => "uint4",
ShaderPropertyType.Bool => "bool",
ShaderPropertyType.Bool2 => "bool2",
ShaderPropertyType.Bool3 => "bool3",
ShaderPropertyType.Bool4 => "bool4",
// NOTE: Textures here are bindless, represented as uint (descriptor index).
ShaderPropertyType.Texture2D => "TEXTURE2D",
ShaderPropertyType.Texture3D => "TEXTURE3D",
ShaderPropertyType.TextureCube => "TEXTURECUBE",
ShaderPropertyType.Texture2DArray => "TEXTURE2D_ARRAY",
ShaderPropertyType.TextureCubeArray => "TEXTURECUBE_ARRAY",
ShaderPropertyType.Sampler => "SAMPLER",
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported shader property type: {type}")
};
}
public static Result<string> GenerateShaderCode(ShaderDescriptor descriptor, string targetDirectory)
{
if (!Directory.Exists(targetDirectory))
{
return Result.Failure("Target directory does not exist.");
}
var outputFileName = descriptor.name.Replace('/', '_');
var outputFilePath = Path.Combine(targetDirectory, outputFileName + ".g.hlsl");
var outputDirectory = Path.GetDirectoryName(outputFilePath);
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory!);
}
using var fileStream = File.CreateText(outputFilePath);
var fileDefine = outputFileName.Replace('/', '_').ToUpperInvariant() + "_G_HLSL";
var sb = new StringBuilder();
sb.AppendLine(_GENERATED_FILE_HEADER);
sb.AppendLine(@$"
#ifndef {fileDefine}
#define {fileDefine}
#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl""");
sb.Append(@"
struct PerMaterialData
{");
foreach (var prop in descriptor.properties)
{
sb.Append($@"
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
}
sb.Append(@"
};");
sb.AppendLine();
sb.AppendLine(@$"
#endif // {fileDefine}");
fileStream.Write(sb.ToString());
return outputFilePath;
}
public static Result<string> GenerateGlobalProperties(ReadOnlySpan<PropertyDescriptor> globalProperties, string targetDirectory)
{
if (!Directory.Exists(targetDirectory))
{
return Result.Failure("Target directory does not exist.");
}
var globalFilePath = Path.Combine(targetDirectory, _GLOBAL_PROPERTY_FILE_NAME);
using var globalFileStream = File.CreateText(globalFilePath);
var sb = new StringBuilder();
sb.AppendLine(_GENERATED_FILE_HEADER);
sb.Append(@"
#ifndef GLOBALDATA_G_HLSL
#define GLOBALDATA_G_HLSL
#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl""
struct GlobalData
{");
foreach (var prop in globalProperties)
{
sb.Append($@"
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
}
sb.AppendLine(@"
};
#endif // GLOBALDATA_G_HLSL");
globalFileStream.Write(sb.ToString());
return globalFilePath;
}
}

View File

@@ -1,6 +0,0 @@
namespace Ghost.Data.Repository;
internal class AssetsRepository
{
}

View File

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

View File

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

View File

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

View File

@@ -1,134 +0,0 @@
# Asset Database Plan
AssetDB is a core component of the Ghost Editor that manages the storage, retrieval, and organization of various assets used within the editor.
This document outlines the plan for implementing the AssetDB, including its structure, functionality, and integration with other components of the Ghost Editor.
## Data Structure
- Asset Metadata: Each asset will have associated metadata, including:
- Unique Identifier (GUID)
- Version (Version of the asset pipeline, not the asset. This is primarily for migration when we redesign the asset pipeline in the future)
- Tags
- Importer Settings
An simplified example of metadata file (filename.png.gmeta):
```json
{
"Guid": "123e4567-e89b-12d3-a456-426614174000",
"Version": 1,
"Tags": ["Environment", "Texture"],
"ImporterSettings": [
"TextureImporter": {
"Version": 1,
"MaxSize": 2048,
"MipLevels": 1
},
"OtherImporter": {
}
]
}
```
- Asset: The base class for all assets.
- Asset Database: A centralized database that stores and manages all assets. It will handle:
- Asset registration and deregistration
- Asset lookup by GUID
- Asset lookup by path
- Automatic handle file creation, remove, rename, move, etc.
- Asset dependency management
- Automatic asset re-importing when source files change.
- Asset tagging.
- Add type specific default importer settings for new asset.
- SQLite (`Microsoft.Data.Sqlite`) for persistent storage and efficient querying.
An simplified data model example in SQLite:
```sql
CREATE TABLE Assets (
Guid TEXT PRIMARY KEY,
Path TEXT NOT NULL,
Type INTEGER,
Version INTEGER,
Tags TEXT,
DependencyGuids TEXT
);
```
## Simplified Workflow
### New Asset Addition
1. A file is added to the project directory.
2. Generates the metadata for the asset with name filename.etx + ".gmeta" (You can get the extension from Ghost.Editor.Core.Utilities.FileExtensions.META_FILE_EXTENSION)
3. Add the asset to the database.
### File Removal
1. A file is removed from the project directory.
2. Deletes the corresponding asset metadata.
3. Remove the asset from the database.
4. Mark dependent assets as dirty for re-importing.
### File Renaming/Moving
1. A file is renamed or moved within the project directory.
2. Check if the new path has an existing metadata file (just in case user move the file with the metadata file together).
- If exists, validate the data and update the database accordingly.
- if not, regenerate the metadata file for the new path and update the database.
3. Delete the old metadata file if exists.
### File Modification
1. A file is modified in the project directory.
2. Check the file hash to see if it has changed.
- If changed, mark the asset as dirty for re-importing.
- If not, do nothing.
### Asset Importing (You don't need to write any assets importer right now, just write the framework and a simple test importer if it's needed for unit test)
1. An asset is marked as dirty.
2. The asset importer for that type processes the asset based on its importer settings.
3. Validate the references and dependencies, report errors if any (for example, missing dependencies).
## Features Checklist
### In Code (API)
- [ ] Find GUID by path
- [ ] Find path by GUID
- [ ] Load asset by GUID (May need special asset loader, not included in this plan, leave an API and TODO comment)
- [ ] API for adding/removing/moving/copying assets
- [ ] API for opening asset in editor or external program
- [ ] API to set asset dirty and save all assets if dirty
- [ ] Get and set asset tags.
- [ ] Refresh asset database (re-scan project directory for changes)
### In Editor (API Only, I will handle the UI part)
- [ ] Asset Browser window to view and manage assets (automatically refresh when assets change)
- [ ] Skip meta file in asset browser view
- [ ] Search assets by name, tag, type, etc.
### In Background
- [ ] File system watcher to monitor changes in the project directory and update the AssetDB accordingly.
- [ ] Automatic asset re-importing when source files change (detect changes via file system watcher and quick hash comparison).
- [ ] Asset dependency management.
- [ ] Validate and fix AssetDB on project load (check for missing/corrupted assets if user add/delete/rename/move files when the editor is not running, etc.)
- [ ] Asset importer system to handle different asset types and their import settings.
## Testing
Make sure everything builds correctly at first.
Write unit tests and integration tests to ensure the AssetDB functions correctly inside the `Ghost.UnitTests` project.
## Critical Considerations
- Performance: Ensure that the AssetDB operations are efficient, especially for large projects with many assets.
- Stability: The meta data files should be the only source of truth for asset information.
The AssetDB should be able to recover from inconsistencies by re-generating data from the meta files.
We still need to trust the meta data files even if they are corrupted or missing by regenerating them when necessary.
Database is used for caching and quick lookup only.
- Asynchronous patterns: Consider using asynchronous operations for file I/O and database operations to avoid blocking the main thread.
Packages like `Microsoft.Data.Sqlite` support async operations.

View File

@@ -1,355 +0,0 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
/// <summary>
/// Create a new asset at the specified path.
/// Generates metadata and adds it to the database.
/// </summary>
/// <param name="assetPath">Path to create the asset at.</param>
/// <param name="content">Content to write to the asset file.</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> CreateAssetAsync(string assetPath, ReadOnlyMemory<byte> content, CancellationToken token = default)
{
if (AssetsDirectory == null)
{
return Result.Failure("AssetsDirectory not initialized");
}
if (!assetPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result.Failure("Asset path must be within the Assets directory");
}
if (File.Exists(assetPath))
{
return Result.Failure("Asset already exists");
}
try
{
var directory = Path.GetDirectoryName(assetPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using var fs = File.Create(assetPath);
await fs.WriteAsync(content, token);
// GenerateMetaFileAsync will be called automatically by the file watcher
// But we'll call it directly to ensure it's created immediately
await GenerateMetaFileAsync(assetPath, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to create asset: {ex.Message}");
}
}
/// <summary>
/// Create an empty asset at the specified path.
/// Generates metadata and adds it to the database.
/// </summary>
/// <param name="assetPath">Path to create the asset at.</param>
/// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> CreateAssetAsync(string assetPath, CancellationToken token = default)
{
return CreateAssetAsync(assetPath, ReadOnlyMemory<byte>.Empty, token);
}
/// <summary>
/// Delete an asset and its metadata.
/// </summary>
/// <param name="guid">GUID of the asset to delete.</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> DeleteAssetAsync(Guid guid, CancellationToken token = default)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
return Result.Failure(pathResult.Message);
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
return Result.Failure(fullPathResult.Message);
}
try
{
var assetPath = fullPathResult.Value;
// Delete the asset file
if (File.Exists(assetPath))
{
File.Delete(assetPath);
}
// Delete the .gmeta file
var metaPath = assetPath + Utilities.FileExtensions.META_FILE_EXTENSION;
if (File.Exists(metaPath))
{
File.Delete(metaPath);
}
// Remove from database
await RemoveAssetFromDatabaseAsync(guid, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to delete asset: {ex.Message}");
}
}
/// <summary>
/// Delete an asset and its metadata by path.
/// </summary>
/// <param name="assetPath">Path to the asset to delete.</param>
/// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> DeleteAssetAsync(string assetPath, CancellationToken token = default)
{
var guidResult = PathToGuid(assetPath);
if (guidResult.IsFailure)
{
return new ValueTask<Result>(Task.FromResult(Result.Failure(guidResult.Message)));
}
return DeleteAssetAsync(guidResult.Value, token);
}
/// <summary>
/// Move an asset to a new location.
/// </summary>
/// <param name="guid">GUID of the asset to move.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default)
{
var oldPathResult = GuidToPath(guid);
if (oldPathResult.IsFailure)
{
return Result.Failure(oldPathResult.Message);
}
var oldFullPathResult = GetFullPath(oldPathResult.Value);
if (oldFullPathResult.IsFailure)
{
return Result.Failure(oldFullPathResult.Message);
}
if (AssetsDirectory == null)
{
return Result.Failure("AssetsDirectory not initialized");
}
// Ensure new path is absolute and within assets directory
if (!Path.IsPathRooted(newPath))
{
newPath = Path.Combine(AssetsDirectory.FullName, newPath);
}
if (!newPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result.Failure("New path must be within the Assets directory");
}
if (File.Exists(newPath))
{
return Result.Failure("A file already exists at the new path");
}
try
{
var directory = Path.GetDirectoryName(newPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Read metadata and calculate hash before moving
var metaResult = await ReadMetaFileAsync(oldFullPathResult.Value, token);
if (metaResult.IsFailure)
{
return Result.Failure(metaResult.Message);
}
var fileHash = await CalculateFileHashAsync(oldFullPathResult.Value, token);
// Move the asset file
File.Move(oldFullPathResult.Value, newPath);
// Move the .gmeta file
var oldMetaPath = oldFullPathResult.Value + Utilities.FileExtensions.META_FILE_EXTENSION;
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
if (File.Exists(oldMetaPath))
{
File.Move(oldMetaPath, newMetaPath);
}
// Update database directly (bypassing file watcher)
await UpsertAssetAsync(newPath, metaResult.Value, fileHash, null, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to move asset: {ex.Message}");
}
}
/// <summary>
/// Move an asset to a new location by path.
/// </summary>
/// <param name="oldPath">Current path of the asset.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default)
{
var guidResult = PathToGuid(oldPath);
if (guidResult.IsFailure)
{
return ValueTask.FromResult(Result.Failure(guidResult.Message));
}
return MoveAssetAsync(guidResult.Value, newPath, token);
}
/// <summary>
/// Copy an asset to a new location with a new GUID.
/// </summary>
/// <param name="guid">GUID of the asset to copy.</param>
/// <param name="newPath">New path for the copied asset (relative or absolute).</param>
/// <returns>Result containing the new asset's GUID.</returns>
public static async ValueTask<Result<Guid>> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default)
{
var oldPathResult = GuidToPath(guid);
if (oldPathResult.IsFailure)
{
return Result<Guid>.Failure(oldPathResult.Message);
}
var oldFullPathResult = GetFullPath(oldPathResult.Value);
if (oldFullPathResult.IsFailure)
{
return Result<Guid>.Failure(oldFullPathResult.Message);
}
if (AssetsDirectory == null)
{
return Result<Guid>.Failure("AssetsDirectory not initialized");
}
// Ensure new path is absolute and within assets directory
if (!Path.IsPathRooted(newPath))
{
newPath = Path.Combine(AssetsDirectory.FullName, newPath);
}
if (!newPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result<Guid>.Failure("New path must be within the Assets directory");
}
if (File.Exists(newPath))
{
return Result<Guid>.Failure("A file already exists at the new path");
}
try
{
var directory = Path.GetDirectoryName(newPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
await using var oldFs = File.OpenRead(oldFullPathResult.Value);
await using var newFs = File.Create(newPath);
await oldFs.CopyToAsync(newFs, token);
// Generate new metadata with new GUID
await GenerateMetaFileAsync(newPath, token);
// Get the new GUID
var newGuidResult = PathToGuid(newPath);
if (newGuidResult.IsFailure)
{
return Result<Guid>.Failure(newGuidResult.Message);
}
return newGuidResult.Value;
}
catch (Exception ex)
{
return Result<Guid>.Failure($"Failed to copy asset: {ex.Message}");
}
}
/// <summary>
/// Copy an asset to a new location by path.
/// </summary>
/// <param name="sourcePath">Path of the asset to copy.</param>
/// <param name="destPath">New path for the copied asset (relative or absolute).</param>
/// <returns>Result containing the new asset's GUID.</returns>
public static ValueTask<Result<Guid>> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default)
{
var guidResult = PathToGuid(sourcePath);
if (guidResult.IsFailure)
{
return new ValueTask<Result<Guid>>(Task.FromResult(Result<Guid>.Failure(guidResult.Message)));
}
return CopyAssetAsync(guidResult.Value, destPath, token);
}
/// <summary>
/// Mark an asset as dirty for re-importing (in-memory only).
/// </summary>
/// <param name="guid">GUID of the asset to mark dirty.</param>
/// <returns>Result indicating success or failure.</returns>
public static Result MarkDirtyAsync(Guid guid, CancellationToken token = default)
{
MarkDirty(guid);
return Result.Success();
}
/// <summary>
/// Import all dirty assets.
/// </summary>
/// <returns>Result indicating success or failure.</returns>
public static async Task<Result> ImportDirtyAssetsAsync(CancellationToken token = default)
{
var dirtyGuids = GetDirtyAssets();
foreach (var guid in dirtyGuids)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
continue;
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
continue;
}
var result = await ImportAssetAsync(fullPathResult.Value, token);
if (result.IsSuccess)
{
ClearDirty(guid);
}
}
return Result.Success();
}
}

View File

@@ -1,128 +0,0 @@
using Ghost.Core;
using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static readonly Dictionary<Type, AssetImporter> s_importerInstances = new();
/// <summary>
/// Import an asset at the specified path.
/// </summary>
/// <param name="assetPath">Full path to the asset file.</param>
/// <returns>Result indicating success or failure.</returns>
private static async ValueTask<Result> ImportAssetAsync(string assetPath, CancellationToken token = default)
{
var extension = Path.GetExtension(assetPath);
if (!s_importerTypeLookup.TryGetValue(extension, out var importerType))
{
// No importer registered for this file type
return Result.Success();
}
// Get or create importer instance
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
{
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
if (importerInstance is null)
{
return Result.Failure($"Failed to create importer instance for type {importerType.Name}");
}
s_importerInstances[importerType] = importerInstance;
}
// Read metadata
var metaResult = await ReadMetaFileAsync(assetPath, token);
if (metaResult.IsFailure)
{
return Result.Failure($"Failed to read asset metadata: {metaResult.Message}");
}
return await importerInstance.ImportAsync(assetPath, metaResult.Value, token);
}
/// <summary>
/// Get the importer type for a specific file extension.
/// </summary>
/// <param name="extension">File extension (e.g., ".png").</param>
/// <returns>The importer type if found, otherwise null.</returns>
public static Type? GetImporterType(string extension)
{
s_importerTypeLookup.TryGetValue(extension, out var importerType);
return importerType;
}
/// <summary>
/// Get all registered importer types and their supported extensions.
/// </summary>
/// <returns>Dictionary mapping extensions to importer types.</returns>
public static Dictionary<string, Type> GetAllImporters()
{
return new Dictionary<string, Type>(s_importerTypeLookup);
}
/// <summary>
/// Export in-memory asset data to disk.
/// The importer will serialize the data into a format it can later import.
/// </summary>
/// <typeparam name="T">Type of asset data to export.</typeparam>
/// <param name="assetPath">Full path where the asset should be saved.</param>
/// <param name="assetData">In-memory asset data to export.</param>
/// <returns>Result with the GUID of the exported asset.</returns>
public static async ValueTask<Result<Guid>> ExportAssetAsync<T>(string assetPath, T assetData, CancellationToken token = default) where T : class
{
var extension = Path.GetExtension(assetPath);
if (!s_importerTypeLookup.TryGetValue(extension, out var importerType))
{
return Result<Guid>.Failure($"No importer registered for extension {extension}");
}
// Get or create importer instance
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
{
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
if (importerInstance is null)
{
return Result<Guid>.Failure($"Failed to create importer instance for type {importerType.Name}");
}
s_importerInstances[importerType] = importerInstance;
}
// Find and invoke the ExportAsync method
var exportMethod = importerType.GetMethod("ExportAsync", BindingFlags.Public | BindingFlags.Instance);
if (exportMethod == null)
{
return Result<Guid>.Failure($"ExportAsync method not found on importer {importerType.Name}. This importer does not support exporting.");
}
// Generate metadata for the new asset
var result = await GenerateMetaFileAsync(assetPath, token);
if (result.IsFailure)
{
return Result<Guid>.Failure($"Failed to generate metadata: {result.Message}");
}
var metaResult = await ReadMetaFileAsync(assetPath, token);
if (metaResult.IsFailure)
{
return Result<Guid>.Failure($"Failed to read metadata: {metaResult.Message}");
}
result = await importerInstance.ExportAsync(assetPath, assetData, metaResult.Value, token);
if (result.IsFailure)
{
return Result<Guid>.Failure(result.Message);
}
// Calculate file hash and update database
var fileHash = await CalculateFileHashAsync(assetPath, token);
await UpsertAssetAsync(assetPath, metaResult.Value, fileHash, null, token);
return metaResult.Value.Guid;
}
}

View File

@@ -1,212 +0,0 @@
using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
// Asset cache - stores loaded assets by GUID
private static readonly ConcurrentDictionary<Guid, Asset> s_assetCache = new();
// LRU tracking - stores access time for each cached asset
private static readonly ConcurrentDictionary<Guid, DateTime> s_assetAccessTime = new();
// Maximum number of cached assets before eviction starts
private const int MAX_CACHED_ASSETS = 1000;
// Percentage of cache to evict when limit is reached (evict oldest 20%)
private const float _CACHE_EVICTION_PERCENTAGE = 0.2f;
private static Result<string> GetImportedAssetsDirectory()
{
if (AssetsDirectory == null)
{
return Result<string>.Failure("AssetsDirectory not initialized");
}
var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "ImportedAssets");
if (!Directory.Exists(cacheDir))
{
Directory.CreateDirectory(cacheDir);
}
return cacheDir;
}
private static Result<string> GetImportedAssetPath(Guid guid)
{
var importedDirResult = GetImportedAssetsDirectory();
if (importedDirResult.IsFailure)
{
return Result<string>.Failure(importedDirResult.Message);
}
// Store imported assets as {GUID}.asset
var assetDataPath = Path.Combine(importedDirResult.Value, $"{guid}.asset");
return assetDataPath;
}
private static Result<T> LoadAssetInternal<T>(Guid guid) where T : Asset
{
// Check cache first
if (s_assetCache.TryGetValue(guid, out var cachedAsset))
{
// Update access time for LRU
s_assetAccessTime[guid] = DateTime.UtcNow;
if (cachedAsset is T typedAsset)
{
return typedAsset;
}
else
{
return Result<T>.Failure($"Cached asset is of type {cachedAsset.GetType().Name}, expected {typeof(T).Name}");
}
}
// Asset not in cache, load from disk
var assetPathResult = GetImportedAssetPath(guid);
if (assetPathResult.IsFailure)
{
return Result<T>.Failure(assetPathResult.Message);
}
var assetDataPath = assetPathResult.Value;
if (!File.Exists(assetDataPath))
{
return Result<T>.Failure($"Imported asset data not found at {assetDataPath}. Asset may not have been imported yet.");
}
try
{
// Read and deserialize asset data
var json = File.ReadAllText(assetDataPath);
var asset = JsonSerializer.Deserialize<T>(json);
if (asset == null)
{
return Result<T>.Failure("Failed to deserialize asset data");
}
// Add to cache
CacheAsset(guid, asset);
return asset;
}
catch (Exception ex)
{
return Result<T>.Failure($"Failed to load asset: {ex.Message}");
}
}
public static Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset
{
var guidResult = PathToGuid(assetPath);
if (guidResult.IsFailure)
{
return Result<T>.Failure(guidResult.Message);
}
return LoadAsset<T>(guidResult.Value);
}
private static void CacheAsset(Guid guid, Asset asset)
{
// Check if we need to evict old assets
if (s_assetCache.Count >= MAX_CACHED_ASSETS)
{
EvictOldestAssets();
}
s_assetCache[guid] = asset;
s_assetAccessTime[guid] = DateTime.UtcNow;
}
private static void EvictOldestAssets()
{
var evictionCount = (int)(MAX_CACHED_ASSETS * _CACHE_EVICTION_PERCENTAGE);
// Sort by access time and remove oldest entries
var oldestAssets = s_assetAccessTime
.OrderBy(kvp => kvp.Value)
.Take(evictionCount)
.Select(kvp => kvp.Key)
.ToList();
foreach (var guid in oldestAssets)
{
s_assetCache.TryRemove(guid, out _);
s_assetAccessTime.TryRemove(guid, out _);
}
}
/// <summary>
/// Unload a specific asset from cache.
/// </summary>
/// <param name="guid">GUID of the asset to unload.</param>
public static void UnloadAsset(Guid guid)
{
s_assetCache.TryRemove(guid, out _);
s_assetAccessTime.TryRemove(guid, out _);
}
/// <summary>
/// Unload all assets from cache.
/// </summary>
public static void UnloadAllAssets()
{
s_assetCache.Clear();
s_assetAccessTime.Clear();
}
/// <summary>
/// Check if an asset is currently loaded in cache.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <returns>True if the asset is in cache.</returns>
public static bool IsAssetLoaded(Guid guid)
{
return s_assetCache.ContainsKey(guid);
}
/// <summary>
/// Get cache statistics.
/// </summary>
/// <returns>Tuple of (current cache size, max cache size).</returns>
public static (int currentSize, int maxSize) GetCacheStats()
{
return (s_assetCache.Count, MAX_CACHED_ASSETS);
}
/// <summary>
/// Save an imported asset to disk for later loading.
/// This should be called by importers after processing the source file.
/// </summary>
/// <typeparam name="T">Type of asset data.</typeparam>
/// <param name="guid">GUID of the asset.</param>
/// <param name="assetData">Processed asset data to save.</param>
/// <returns>Result indicating success or failure.</returns>
public static Result SaveImportedAsset<T>(Guid guid, T assetData)
where T : Asset
{
var assetPathResult = GetImportedAssetPath(guid);
if (assetPathResult.IsFailure)
{
return Result.Failure(assetPathResult.Message);
}
try
{
var json = JsonSerializer.Serialize(assetData, s_defaultJsonOptions);
File.WriteAllText(assetPathResult.Value, json);
// Invalidate cache for this asset so it gets reloaded next time
UnloadAsset(guid);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to save imported asset: {ex.Message}");
}
}
}

View File

@@ -1,203 +0,0 @@
using Ghost.Core;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
/// <summary>
/// Get the relative path from the assets directory.
/// </summary>
private static Result<string> GetRelativePath(string fullPath)
{
if (AssetsDirectory == null)
{
return Result<string>.Failure("AssetsDirectory not initialized");
}
if (!fullPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result<string>.Failure("Path is not within assets directory");
}
return Path.GetRelativePath(AssetsDirectory.FullName, fullPath);
}
/// <summary>
/// Get the full path from a relative path.
/// </summary>
private static Result<string> GetFullPath(string relativePath)
{
if (AssetsDirectory == null)
{
return Result<string>.Failure("AssetsDirectory not initialized");
}
return Path.Combine(AssetsDirectory.FullName, relativePath);
}
/// <summary>
/// Find GUID by asset path.
/// </summary>
/// <param name="assetPath">Full or relative path to the asset.</param>
/// <returns>The GUID of the asset if found.</returns>
public static Result<Guid> PathToGuid(string assetPath)
{
var relativePath = assetPath;
// Convert to relative path if it's a full path
if (Path.IsPathRooted(assetPath))
{
var relResult = GetRelativePath(assetPath);
if (relResult.IsFailure)
{
return Result<Guid>.Failure(relResult.Message);
}
relativePath = relResult.Value;
}
// Normalize path separators
relativePath = relativePath.Replace('\\', '/');
lock (s_dbLock)
{
if (s_pathAssetLookup.TryGetValue(relativePath, out var guid))
{
return guid;
}
}
return Result<Guid>.Failure("Asset not found in database");
}
/// <summary>
/// Find path by GUID.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <returns>The relative path to the asset if found.</returns>
public static Result<string> GuidToPath(Guid guid)
{
lock (s_dbLock)
{
if (s_assetPathLookup.TryGetValue(guid, out var path))
{
return path;
}
}
return Result<string>.Failure("Asset GUID not found in database");
}
/// <summary>
/// Load asset by GUID with caching.
/// </summary>
/// <typeparam name="T">Type of asset to load.</typeparam>
/// <param name="guid">GUID of the asset.</param>
/// <returns>The loaded asset.</returns>
public static Result<T> LoadAsset<T>(Guid guid) where T : Asset
{
// Implemented in AssetDatabase.Loader.cs
return LoadAssetInternal<T>(guid);
}
/// <summary>
/// Get asset tags by GUID.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <returns>List of tags associated with the asset.</returns>
public static async ValueTask<Result<List<string>>> GetAssetTagsAsync(Guid guid, CancellationToken token = default)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
return Result<List<string>>.Failure(pathResult.Message);
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
return Result<List<string>>.Failure(fullPathResult.Message);
}
var metaResult = await ReadMetaFileAsync(fullPathResult.Value, token);
if (metaResult.IsFailure)
{
return Result<List<string>>.Failure(metaResult.Message);
}
return metaResult.Value.Tags;
}
/// <summary>
/// Set asset tags by GUID.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <param name="tags">New tags for the asset.</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> SetAssetTagsAsync(Guid guid, List<string> tags, CancellationToken token = default)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
return Result.Failure(pathResult.Message);
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
return Result.Failure(fullPathResult.Message);
}
var metaResult = await ReadMetaFileAsync(fullPathResult.Value, token);
if (metaResult.IsFailure)
{
return Result.Failure(metaResult.Message);
}
metaResult.Value.Tags = tags;
// Write updated metadata to .gmeta file
var writeResult = await WriteMetaFileAsync(fullPathResult.Value + Utilities.FileExtensions.META_FILE_EXTENSION, metaResult.Value, token);
if (writeResult.IsFailure)
{
return writeResult;
}
// Update database with new tags
var fileHash = await CalculateFileHashAsync(fullPathResult.Value, token);
return await UpsertAssetAsync(fullPathResult.Value, metaResult.Value, fileHash, null, token);
}
/// <summary>
/// Search assets by name pattern.
/// Supports SQL LIKE wildcards: * (any characters) and ? (single character).
/// </summary>
/// <param name="namePattern">Search pattern (e.g., "*.txt", "player?", "test*").</param>
/// <returns>List of matching asset GUIDs.</returns>
public static async Task<List<Guid>> FindAssetsByNameAsync(string namePattern, CancellationToken token = default)
{
return await GetAssetsByNameAsync(namePattern, token);
}
/// <summary>
/// Find assets by tag.
/// </summary>
/// <param name="tag">Tag to search for.</param>
/// <returns>List of asset GUIDs with the specified tag.</returns>
public static async Task<List<Guid>> FindAssetsByTagAsync(string tag, CancellationToken token = default)
{
return await GetAssetsByTagAsync(tag, token);
}
/// <summary>
/// Get all assets in the database.
/// </summary>
/// <returns>Dictionary mapping GUIDs to relative paths.</returns>
public static IReadOnlyDictionary<Guid, string> GetAllAssets()
{
lock (s_dbLock)
{
return s_assetPathLookup.AsReadOnly();
}
}
}

View File

@@ -1,251 +0,0 @@
using Ghost.Core;
using Ghost.Editor.Core.Utilities;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static readonly Dictionary<string, Type> s_importerTypeLookup = new();
private static void InitializeMetaData()
{
if (s_watcher == null)
{
throw new InvalidOperationException("AssetDatabase is not initialized. Ensure that Initialize() is called before registering asset importers.");
}
var importerTypes = TypeCache.GetTypes().Where(t => t.GetCustomAttribute<AssetImporterAttribute>() != null);
foreach (var type in importerTypes)
{
var attribute = type.GetCustomAttribute<AssetImporterAttribute>()!;
foreach (var extension in attribute.SupportedExtensions)
{
s_importerTypeLookup[extension] = type;
}
}
s_watcher.Created += OnFSEvent;
s_watcher.Deleted += OnFSEvent;
s_watcher.Changed += OnFSEvent;
s_watcher.Renamed += OnAssetRenamed;
}
private static Result<string> GetMetaFilePath(string assetPath)
{
if (Directory.Exists(assetPath))
{
return Result<string>.Failure("Cannot create metadata for directories");
}
if (Path.GetExtension(assetPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
return Result<string>.Failure("Cannot create metadata for metadata files");
}
return assetPath + FileExtensions.META_FILE_EXTENSION;
}
private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath)
{
var extension = Path.GetExtension(assetPath);
if (s_importerTypeLookup.TryGetValue(extension, out var importerType))
{
var settingsType = importerType.BaseType?.GetGenericArguments()[0];
if (settingsType == null || !typeof(ImporterSettings).IsAssignableFrom(settingsType))
{
return null;
}
return (ImporterSettings?)Activator.CreateInstance(settingsType);
}
return null;
}
/// <summary>
/// Calculate SHA256 hash of a file for change detection.
/// </summary>
private static async Task<string> CalculateFileHashAsync(string filePath, CancellationToken token = default)
{
try
{
await using var stream = File.OpenRead(filePath);
var hash = await SHA256.HashDataAsync(stream, token);
return Convert.ToHexString(hash);
}
catch
{
return string.Empty;
}
}
private static async Task<Result> WriteMetaFileAsync(string metaFilePath, AssetMeta metaData, CancellationToken token = default)
{
try
{
await using var fileStream = File.Create(metaFilePath);
await JsonSerializer.SerializeAsync(fileStream, metaData, s_defaultJsonOptions, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
/// <summary>
/// Read metadata from a .gmeta file.
/// </summary>
private static async ValueTask<Result<AssetMeta>> ReadMetaFileAsync(string assetPath, CancellationToken token = default)
{
var metaFileResult = GetMetaFilePath(assetPath);
if (metaFileResult.IsFailure)
{
return Result<AssetMeta>.Failure(metaFileResult.Message);
}
if (!File.Exists(metaFileResult.Value))
{
return Result<AssetMeta>.Failure("Metadata file does not exist");
}
try
{
await using var fileStream = File.OpenRead(metaFileResult.Value);
var meta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream, s_defaultJsonOptions, token);
if (meta == null)
{
return Result<AssetMeta>.Failure("Failed to deserialize metadata");
}
return meta;
}
catch (Exception ex)
{
return Result<AssetMeta>.Failure($"Failed to read metadata: {ex.Message}");
}
}
internal static async ValueTask<Result> GenerateMetaFileAsync(string assetPath, CancellationToken token = default)
{
Result r;
var metaFileResult = GetMetaFilePath(assetPath);
if (metaFileResult.IsFailure)
{
return Result.Failure(metaFileResult.Message);
}
if (File.Exists(metaFileResult.Value))
{
var existingMetaResult = await ReadMetaFileAsync(assetPath, token);
if (existingMetaResult.IsSuccess)
{
var existingMeta = existingMetaResult.Value;
if (s_assetPathLookup.TryGetValue(existingMeta.Guid, out var path))
{
var relResult = GetRelativePath(assetPath);
if (relResult.IsSuccess && assetPath != path)
{
// GUID conflict - regenerate
existingMeta.Guid = Guid.NewGuid();
r = await WriteMetaFileAsync(metaFileResult.Value, existingMeta, token);
if (r.IsFailure)
{
return r;
}
}
}
// Calculate file hash and update database
var fileHash = await CalculateFileHashAsync(assetPath, token);
await UpsertAssetAsync(assetPath, existingMeta, fileHash, null, token);
return Result.Success();
}
}
// Calculate initial file hash
var fileHash2 = await CalculateFileHashAsync(assetPath, token);
var defaultSettings = GetDefaultSettingsForAsset(assetPath);
var metaData = new AssetMeta
{
Guid = Guid.NewGuid()
};
if (defaultSettings != null)
{
metaData.SetImporterSettings(defaultSettings.GetType().Name, defaultSettings);
}
r = await WriteMetaFileAsync(metaFileResult.Value, metaData, token);
if (r.IsFailure)
{
return r;
}
// Add to database
await UpsertAssetAsync(assetPath, metaData, fileHash2, null, token);
return r;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsMetaFile(string path)
{
return Path.GetExtension(path).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase);
}
private static async void OnFSEvent(object sender, FileSystemEventArgs e)
{
if (IsMetaFile(e.FullPath))
{
return;
}
var type = e.ChangeType switch
{
WatcherChangeTypes.Created => AssetCommandType.FileCreated,
WatcherChangeTypes.Deleted => AssetCommandType.FileDeleted,
WatcherChangeTypes.Changed => AssetCommandType.FileModified,
_ => throw new InvalidOperationException("Unsupported file system event type")
};
await PostCommandAsync(new AssetCommand(type, e.FullPath, Timestamp: DateTime.UtcNow));
}
private static async void OnAssetRenamed(object sender, RenamedEventArgs e)
{
if (IsMetaFile(e.FullPath))
{
return;
}
await PostCommandAsync(new AssetCommand(AssetCommandType.FileRenamed, e.FullPath, e.OldFullPath, DateTime.UtcNow));
}
/// <summary>
/// Mark all assets that depend on the specified asset as dirty.
/// </summary>
private static async Task MarkDependentAssetsDirtyAsync(Guid assetGuid)
{
// TODO: We should have a reverse dependency lookup in the database to avoid scanning all assets.
// Query database for all assets and check their dependencies
var allAssets = GetAllAssets();
foreach (var kvp in allAssets)
{
var dependencies = await GetDependenciesAsync(kvp.Key, CancellationToken.None);
if (dependencies.Contains(assetGuid))
{
MarkDirty(kvp.Key);
}
}
}
}

View File

@@ -1,51 +0,0 @@
using Ghost.Core;
using Ghost.Editor.Core.Utilities;
using System.Diagnostics;
using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static readonly Dictionary<string, Action<string>> s_assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase);
private static void InitializeAssetHandle()
{
var methods = TypeCache.GetTypes()
.SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
.Where(m => m.GetCustomAttribute<AssetOpenHandlerAttribute>() != null &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == typeof(string));
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<AssetOpenHandlerAttribute>()!;
var del = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), method);
foreach (var ext in attr.Extensions)
{
if (s_assetOpenHandlers.ContainsKey(ext))
{
Logger.LogError($"Duplicate asset open handler for extension '{ext}' found in method '{method.Name}'. Existing handler will be overwritten.");
}
s_assetOpenHandlers[ext] = del;
}
}
}
public static void OpenAsset(string path)
{
var extension = Path.GetExtension(path);
if (s_assetOpenHandlers.TryGetValue(extension, out var handler))
{
handler(path);
}
else
{
Process.Start(new ProcessStartInfo(path)
{
UseShellExecute = true
});
}
}
}

View File

@@ -1,390 +0,0 @@
using Ghost.Core;
using Ghost.Data.Services;
using Microsoft.Data.Sqlite;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static SqliteConnection? s_dbConnection;
/// <summary>
/// Initialize the SQLite database for asset caching.
/// </summary>
private static async Task InitializeDatabaseAsync(CancellationToken token = default)
{
if (AssetsDirectory == null)
{
throw new InvalidOperationException("AssetsDirectory is not set. Initialize() must be called first.");
}
var dbPath = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "AssetDatabase.db");
var cacheDir = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(cacheDir))
{
Directory.CreateDirectory(cacheDir!);
}
var connectionString = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadWriteCreate,
Cache = SqliteCacheMode.Shared
}.ToString();
s_dbConnection = new SqliteConnection(connectionString);
await s_dbConnection.OpenAsync(token);
// Create tables
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Assets (
Guid TEXT PRIMARY KEY,
Path TEXT NOT NULL,
Version INTEGER NOT NULL,
Tags TEXT,
FileHash TEXT,
DependencyGuids TEXT,
LastModified INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_path ON Assets(Path);
";
await cmd.ExecuteNonQueryAsync(token);
}
/// <summary>
/// Add or update an asset in the database.
/// </summary>
/// <param name="assetPath">Full path to the asset file.</param>
/// <param name="meta">Asset metadata from .gmeta file.</param>
/// <param name="fileHash">SHA256 hash of the asset file content.</param>
/// <param name="dependencies">List of GUIDs this asset depends on (extracted during import).</param>
private static async ValueTask<Result> UpsertAssetAsync(string assetPath, AssetMeta meta, string fileHash, List<Guid>? dependencies = null, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return Result.Failure("Database not initialized");
}
var relativePath = GetRelativePath(assetPath);
if (relativePath.IsFailure)
{
return Result.Failure(relativePath.Message);
}
try
{
lock (s_dbLock)
{
// If this GUID already exists with a different path, remove the old path mapping
if (s_assetPathLookup.TryGetValue(meta.Guid, out var oldPath) && oldPath != relativePath.Value)
{
s_pathAssetLookup.Remove(oldPath);
}
// Update lookups with new path (normalize path separators for consistency)
var normalizedPath = relativePath.Value.Replace('\\', '/');
s_assetPathLookup[meta.Guid] = normalizedPath;
s_pathAssetLookup[normalizedPath] = meta.Guid;
}
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = @"
INSERT OR REPLACE INTO Assets (Guid, Path, Version, Tags, FileHash, DependencyGuids, LastModified)
VALUES (@guid, @path, @version, @tags, @fileHash, @deps, @modified)
";
cmd.Parameters.AddWithValue("@guid", meta.Guid.ToString());
cmd.Parameters.AddWithValue("@path", relativePath.Value);
cmd.Parameters.AddWithValue("@version", meta.Version);
cmd.Parameters.AddWithValue("@tags", JsonSerializer.Serialize(meta.Tags));
cmd.Parameters.AddWithValue("@fileHash", fileHash);
cmd.Parameters.AddWithValue("@deps", JsonSerializer.Serialize(dependencies ?? new List<Guid>()));
cmd.Parameters.AddWithValue("@modified", DateTimeOffset.UtcNow.ToUnixTimeSeconds());
await cmd.ExecuteNonQueryAsync(token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to upsert asset: {ex.Message}");
}
}
/// <summary>
/// Remove an asset from the database.
/// </summary>
private static async Task<Result> RemoveAssetFromDatabaseAsync(Guid guid, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return Result.Failure("Database not initialized");
}
try
{
lock (s_dbLock)
{
if (s_assetPathLookup.TryGetValue(guid, out var path))
{
s_assetPathLookup.Remove(guid);
s_pathAssetLookup.Remove(path);
}
}
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "DELETE FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString());
await cmd.ExecuteNonQueryAsync(token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to remove asset: {ex.Message}");
}
}
/// <summary>
/// Load all assets from the database into memory cache.
/// </summary>
private static async Task LoadAssetCacheFromDatabaseAsync(CancellationToken token = default)
{
if (s_dbConnection == null)
{
return;
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Path FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var path = reader.GetString(1);
if (Guid.TryParse(guidStr, out var guid))
{
lock (s_dbLock)
{
s_assetPathLookup[guid] = path;
s_pathAssetLookup[path] = guid;
}
}
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to load asset cache: {ex.Message}");
}
}
/// <summary>
/// Get assets by tag.
/// </summary>
private static async Task<List<Guid>> GetAssetsByTagAsync(string tag, CancellationToken token = default)
{
var result = new List<Guid>();
if (s_dbConnection == null)
{
return result;
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Tags FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var tagsJson = reader.GetString(1);
if (Guid.TryParse(guidStr, out var guid))
{
var tags = JsonSerializer.Deserialize<List<string>>(tagsJson);
if (tags != null && tags.Contains(tag, StringComparer.OrdinalIgnoreCase))
{
result.Add(guid);
}
}
}
}
catch
{
// Silently fail
}
return result;
}
/// <summary>
/// Get the file hash for an asset from the database.
/// </summary>
private static async Task<string?> GetFileHashAsync(Guid guid, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return null;
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT FileHash FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString());
var result = await cmd.ExecuteScalarAsync(token);
return result?.ToString();
}
catch
{
return null;
}
}
/// <summary>
/// Get the dependencies for an asset from the database.
/// </summary>
private static async Task<List<Guid>> GetDependenciesAsync(Guid guid, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return new List<Guid>();
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT DependencyGuids FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString());
var result = await cmd.ExecuteScalarAsync(token);
if (result != null)
{
var json = result.ToString();
return JsonSerializer.Deserialize<List<Guid>>(json ?? "[]") ?? new List<Guid>();
}
}
catch
{
// Silently fail
}
return new List<Guid>();
}
/// <summary>
/// Find assets by name pattern using database query with wildcards.
/// </summary>
/// <param name="namePattern">Pattern supporting * (any chars) and ? (single char).</param>
private static async Task<List<Guid>> GetAssetsByNameAsync(string namePattern, CancellationToken token = default)
{
var results = new List<Guid>();
if (s_dbConnection == null)
{
return results;
}
try
{
// Convert wildcard pattern to SQL LIKE pattern
var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_');
await using var cmd = s_dbConnection.CreateCommand();
// Extract just the filename from the path for matching
// SQLite doesn't have a built-in path manipulation, so we search in the full path
// and filter by checking if the pattern matches the filename part
cmd.CommandText = @"
SELECT Guid, Path FROM Assets
WHERE Path LIKE '%' || @pattern || '%'
";
cmd.Parameters.AddWithValue("@pattern", sqlPattern);
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var path = reader.GetString(1);
// Extract filename and check if it matches the pattern
var fileName = Path.GetFileName(path);
// Convert pattern to regex for proper matching
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(namePattern)
.Replace("\\*", ".*")
.Replace("\\?", ".") + "$";
if (System.Text.RegularExpressions.Regex.IsMatch(fileName, regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
if (Guid.TryParse(guidStr, out var guid))
{
results.Add(guid);
}
}
}
}
catch
{
// Silently fail
}
return results;
}
/// <summary>
/// Remove orphaned entries from database (assets that no longer exist on disk).
/// </summary>
private static async Task RemoveOrphanedEntriesAsync(CancellationToken token = default)
{
if (s_dbConnection == null || AssetsDirectory == null)
{
return;
}
try
{
var orphanedGuids = new List<Guid>();
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Path FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var path = reader.GetString(1);
if (Guid.TryParse(guidStr, out var guid))
{
// Check if file exists
var fullPath = Path.Combine(AssetsDirectory.FullName, path);
if (!File.Exists(fullPath))
{
orphanedGuids.Add(guid);
}
}
}
// Remove orphaned entries
foreach (var guid in orphanedGuids)
{
await RemoveAssetFromDatabaseAsync(guid, token);
}
}
catch
{
// Silently fail - cleanup is best effort
}
}
}

View File

@@ -1,531 +0,0 @@
using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// Command types for asset database operations.
/// </summary>
internal enum AssetCommandType
{
FileCreated,
FileModified,
FileDeleted,
FileRenamed,
ManualRefresh
}
/// <summary>
/// Represents a command to process an asset operation.
/// </summary>
internal readonly record struct AssetCommand(
AssetCommandType Type,
string Path,
string? OldPath = null,
DateTime Timestamp = default
);
/// <summary>
/// Centralized asset database that manages all assets in the project.
/// Handles asset registration, lookup, importing, and dependency management.
/// Uses SQLite for persistent storage and efficient querying.
/// </summary>
public static partial class AssetDatabase
{
private static FileSystemWatcher? s_watcher;
private static readonly Lock s_dbLock = new();
private static readonly Dictionary<Guid, string> s_assetPathLookup = new();
private static readonly Dictionary<string, Guid> s_pathAssetLookup = new();
// In-memory dirty asset tracking (for runtime modifications only)
// TODO: We do not handle the reimporting of dirty assets yet
private static readonly HashSet<Guid> s_dirtyAssets = new();
// Command buffer pattern - Channel for file system event commands
private static Channel<AssetCommand>? s_commandChannel;
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 bool s_autoRefreshEnabled = true;
// Initialization guard
private static readonly Lock s_initializationLock = new();
private static bool s_initialized = false;
private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100);
private static ManualResetEventSlim s_resetEventSlim = new(false);
private static readonly JsonSerializerOptions s_defaultJsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
public static DirectoryInfo? AssetsDirectory
{
get;
private set;
}
/// <summary>
/// Initialize the asset database.
/// Must be called after project is loaded.
/// </summary>
internal static async Task Initialize(CancellationToken token = default)
{
lock (s_initializationLock)
{
if (s_initialized)
{
return;
}
s_initialized = true;
}
if (ProjectService.CurrentProject.Metadata == null)
{
throw new InvalidOperationException("Project metadata is not initialized. Ensure that the project is loaded before accessing the AssetDatabase.");
}
AssetsDirectory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER));
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
{
SingleReader = false,
SingleWriter = false
});
// Initialize command processor timer (starts disabled, triggered by events)
s_commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite);
await InitializeDatabaseAsync(token);
await LoadAssetCacheFromDatabaseAsync(token);
s_watcher = new FileSystemWatcher
{
Path = AssetsDirectory.FullName,
IncludeSubdirectories = true,
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite
};
InitializeAssetHandle();
InitializeMetaData();
// TODO: Timestamp fake instead of full scan.
await ValidateAndFixDatabaseAsync(token);
}
/// <summary>
/// Validate the asset database and fix any inconsistencies.
/// Checks for missing/corrupted assets and regenerates metadata as needed.
/// </summary>
private static async Task<Result> ValidateAndFixDatabaseAsync(CancellationToken token = default)
{
if (AssetsDirectory == null)
{
return Result.Failure("AssetsDirectory not initialized");
}
try
{
// Scan all files in assets directory
var allFiles = Directory.EnumerateFiles(AssetsDirectory.FullName, "*.*", SearchOption.AllDirectories)
.Where(f => !f.EndsWith(Utilities.FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase));
// Ensure all files have metadata
foreach (var file in allFiles)
{
var metaPath = file + Utilities.FileExtensions.META_FILE_EXTENSION;
if (!File.Exists(metaPath))
{
await GenerateMetaFileAsync(file, token);
}
else
{
// Validate and update database
var metaResult = await ReadMetaFileAsync(file, token);
if (metaResult.IsSuccess)
{
var fileHash = await CalculateFileHashAsync(file, token);
await UpsertAssetAsync(file, metaResult.Value, fileHash, null, token);
}
else
{
// Corrupted meta file - regenerate
await GenerateMetaFileAsync(file, token);
}
}
}
// Remove orphaned entries from database (files that no longer exist)
await RemoveOrphanedEntriesAsync(token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to validate database: {ex.Message}");
}
}
/// <summary>
/// Refresh the asset database manually.
/// Scans the project directory for changes and processes any queued file system events.
/// </summary>
public static async Task<Result> RefreshAsync(CancellationToken token = default)
{
// Flush waiting commands to channel
while (s_waitingCommands.TryDequeue(out var cmd))
{
s_commandChannel?.Writer.TryWrite(cmd);
}
s_resetEventSlim.Reset();
s_commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty));
s_commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
await Task.Run(s_resetEventSlim.Wait, token);
return Result.Success();
}
/// <summary>
/// Mark an asset as dirty (modified in memory but not yet saved).
/// This state is NOT persisted and will be lost on application restart.
/// </summary>
public static void MarkDirty(Guid assetGuid)
{
lock (s_dbLock)
{
s_dirtyAssets.Add(assetGuid);
}
}
/// <summary>
/// Check if an asset is marked as dirty.
/// </summary>
public static bool IsDirty(Guid assetGuid)
{
lock (s_dbLock)
{
return s_dirtyAssets.Contains(assetGuid);
}
}
/// <summary>
/// Get all dirty assets.
/// </summary>
public static Guid[] GetDirtyAssets()
{
lock (s_dbLock)
{
return s_dirtyAssets.ToArray();
}
}
/// <summary>
/// Clear dirty flag for an asset (typically after saving).
/// </summary>
public static void ClearDirty(Guid assetGuid)
{
lock (s_dbLock)
{
s_dirtyAssets.Remove(assetGuid);
}
}
/// <summary>
/// Clear all dirty flags.
/// </summary>
public static void ClearAllDirty()
{
lock (s_dbLock)
{
s_dirtyAssets.Clear();
}
}
/// <summary>
/// Enable or disable automatic asset database refresh.
/// When disabled, file system events are queued and processed only when RefreshAsync() is called.
/// </summary>
public static void SetAutoRefresh(bool enabled)
{
s_autoRefreshEnabled = enabled;
}
internal static void FlushPendingCommands()
{
// Stop timer temporarily
s_commandProcessorTimer?.Change(Timeout.Infinite, Timeout.Infinite);
// Give a tiny bit of time for any in-flight file watcher events to post to channel
Thread.Sleep(50);
// Process all commands now
ProcessPendingCommands(null);
}
private static async ValueTask PostCommandAsync(AssetCommand command, CancellationToken token = default)
{
if (s_commandChannel == null)
{
return;
}
if (s_autoRefreshEnabled)
{
await s_commandChannel.Writer.WriteAsync(command, token);
s_commandProcessorTimer?.Change(s_debounceDelay, Timeout.InfiniteTimeSpan);
}
else
{
s_waitingCommands.Enqueue(command);
}
}
private static async void ProcessPendingCommands(object? state)
{
if (s_commandChannel == null)
{
return;
}
try
{
// // Collect all pending commands
// var commands = new List<AssetCommand>();
//
// while (s_commandChannel.Reader.TryRead(out var cmd))
// {
// commands.Add(cmd);
// }
// // Group commands by path (last command wins)
// var commandsByPath = new Dictionary<string, AssetCommand>();
// foreach (var cmd in commands)
// {
// commandsByPath[cmd.Path] = cmd;
// }
// NOTE: We handle the temp file filtering in each command handler now
// We should able to remove this allocation heavy code
// Filter out temp files (files that were created then deleted)
// lock (s_commandLock)
// {
// var pathsToProcess = commandsByPath.Keys.ToList();
// foreach (var path in pathsToProcess)
// {
// // If file was created/modified but doesn't exist anymore, skip
// if (!File.Exists(path) && commandsByPath[path].Type != AssetCommandType.FileDeleted)
// {
// commandsByPath.Remove(path);
// }
// }
//
// // Clear pending paths
// s_pendingCommandPaths.Clear();
// }
// Execute commands
// NOTE: We many don't need to collect all commands first, just process as we read.
// Channel in c# is thread-safe for multiple readers/writers.
//await foreach (var cmd in s_commandChannel.Reader.ReadAllAsync())
//{
// await ExecuteCommandAsync(cmd);
//}
while (s_commandChannel.Reader.TryRead(out var cmd))
{
await ExecuteCommandAsync(cmd);
}
await ImportDirtyAssetsAsync();
}
catch (Exception ex)
{
Logger.LogError($"Error processing commands: {ex.Message}");
}
finally
{
s_resetEventSlim.Set();
}
}
private static async ValueTask ExecuteCommandAsync(AssetCommand command)
{
switch (command.Type)
{
case AssetCommandType.FileCreated:
await HandleFileCreatedAsync(command.Path);
break;
case AssetCommandType.FileModified:
await HandleFileModifiedAsync(command.Path);
break;
case AssetCommandType.FileDeleted:
await HandleFileDeletedAsync(command.Path);
break;
case AssetCommandType.FileRenamed:
if (command.OldPath != null)
{
await HandleFileRenamedAsync(command.OldPath, command.Path);
}
break;
case AssetCommandType.ManualRefresh:
await ValidateAndFixDatabaseAsync(CancellationToken.None);
break;
}
}
private static async ValueTask HandleFileCreatedAsync(string path)
{
if (!File.Exists(path))
{
return;
}
await GenerateMetaFileAsync(path, CancellationToken.None);
}
private static async ValueTask HandleFileModifiedAsync(string path)
{
if (!File.Exists(path))
{
return;
}
// Check if file hash changed
var metaResult = await ReadMetaFileAsync(path, CancellationToken.None);
if (metaResult.IsFailure)
{
// No .gmeta file - treat this as a new file creation
await HandleFileCreatedAsync(path);
return;
}
var newHash = await CalculateFileHashAsync(path, CancellationToken.None);
var oldHash = await GetFileHashAsync(metaResult.Value.Guid, CancellationToken.None);
if (oldHash != newHash)
{
// File changed - update database and mark as dirty
await UpsertAssetAsync(path, metaResult.Value, newHash, null, CancellationToken.None);
MarkDirty(metaResult.Value.Guid);
}
}
private static async ValueTask HandleFileDeletedAsync(string path)
{
var metaFileResult = GetMetaFilePath(path);
if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value))
{
try
{
var metaResult = await ReadMetaFileAsync(path, CancellationToken.None);
if (metaResult.IsSuccess)
{
var meta = metaResult.Value;
// Remove from database
await RemoveAssetFromDatabaseAsync(meta.Guid, CancellationToken.None);
// Mark dependent assets as dirty
await MarkDependentAssetsDirtyAsync(meta.Guid);
}
File.Delete(metaFileResult.Value);
}
catch (Exception ex)
{
Logger.LogError($"Error deleting asset metadata: {ex.Message}");
}
}
}
private static async ValueTask HandleFileRenamedAsync(string oldPath, string newPath)
{
var oldMetaPath = oldPath + Utilities.FileExtensions.META_FILE_EXTENSION;
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
if (File.Exists(newMetaPath))
{
// Validate and update
await GenerateMetaFileAsync(newPath, CancellationToken.None);
}
else if (File.Exists(oldMetaPath))
{
// Move meta file
File.Move(oldMetaPath, newMetaPath);
// Update database with new path and recalculated hash
var metaResult = await ReadMetaFileAsync(newPath, CancellationToken.None);
if (metaResult.IsSuccess)
{
var fileHash = await CalculateFileHashAsync(newPath, CancellationToken.None);
await UpsertAssetAsync(newPath, metaResult.Value, fileHash, null, CancellationToken.None);
}
}
else
{
// Generate new meta file
await GenerateMetaFileAsync(newPath, CancellationToken.None);
}
// Delete old meta if it still exists
if (File.Exists(oldMetaPath) && oldMetaPath != newMetaPath)
{
try
{
File.Delete(oldMetaPath);
}
catch
{
}
}
}
internal static void Shutdown()
{
lock (s_initializationLock)
{
if (!s_initialized)
{
return;
}
s_watcher?.Dispose();
s_watcher = null;
s_commandProcessorTimer?.Dispose();
s_commandProcessorTimer = null;
s_dbConnection?.Close();
s_dbConnection?.Dispose();
s_dbConnection = null;
s_assetPathLookup.Clear();
s_pathAssetLookup.Clear();
s_dirtyAssets.Clear();
s_waitingCommands.Clear();
s_importerInstances.Clear();
s_importerTypeLookup.Clear();
s_initialized = false;
}
}
}

View File

@@ -1,115 +0,0 @@
# Asset Database Architecture
This document details the architectural design and data flow of the `AssetHandle` module in Ghost Editor.
## System Overview
The Asset Database acts as the bridge between the raw file system (Source Assets) and the runtime engine (Imported Assets). It maintains a consistent state using a dual-storage approach:
1. **File System**: The source of truth. Contains source files (e.g., `.png`, `.fbx`) and metadata files (`.gmeta`).
2. **SQLite Database**: An acceleration layer (cache) for fast lookups, dependency tracking, and searching.
## Data Flow
### 1. Asset Discovery & Registration
When the editor starts or a file changes:
1. **FileSystemWatcher** detects the change (Create/Delete/Modify/Rename).
2. **Event Handler** queues an `AssetCommand` (debounce mechanism prevents event storms).
3. **Command Processor** executes the command:
* **New File**: Generates a `.gmeta` file with a new GUID and default settings. Adds to SQLite.
* **Modified File**: Checks hash. If changed, marks asset as "Dirty" and updates SQLite.
* **Deleted File**: Removes from SQLite and marks dependents as "Dirty".
### 2. Import Pipeline
The import process converts source formats into engine-ready data.
**Flow:**
1. `AssetDatabase.ImportDirtyAssetsAsync()` or direct `ImportAssetAsync` is called.
2. System looks up the registered `AssetImporter` for the file extension.
3. `AssetImporter.ImportAsync` is invoked with the source path and metadata.
4. Importer reads source file and settings from metadata.
5. Importer processes data (e.g., compiles shaders, compresses textures).
6. Importer calls `AssetDatabase.SaveImportedAsset(guid, data)`.
7. Data is serialized to JSON (or binary) in the `Cache/ImportedAssets` directory as `{GUID}.asset`.
### 3. Loading Pipeline
When the engine requests an asset:
**Flow:**
1. `AssetDatabase.LoadAsset<T>(guid)` is called.
2. **Memory Cache Check**:
* Checks `s_assetCache` (ConcurrentDictionary).
* If found: Updates LRU timestamp and returns object.
* If not found: Proceeds to disk load.
3. **Disk Load**:
* Locates `{GUID}.asset` in `Cache/ImportedAssets`.
* Deserializes the data into the target runtime type (e.g., `TextureAsset`).
4. **Cache Update**:
* Adds new object to `s_assetCache`.
* If cache size > `MAX_CACHED_ASSETS` (1000), evicts oldest 20% based on access time.
## Key Components Diagram
```mermaid
graph TD
User[Editor / User] -->|File Ops| API[AssetDatabase API]
FS[File System] -->|Events| Watcher[FileSystemWatcher]
subgraph AssetDatabase
API --> DB[SQLite Database]
API --> Meta[Meta Handler]
API --> Loader[Asset Loader]
API --> Importer[Import System]
Watcher -->|Queue| Cmd[Command Processor]
Cmd --> Meta
Cmd --> DB
Importer -->|Read| FS
Importer -->|Write| Cache[Imported Assets Cache]
Loader -->|Read| Cache
Loader -->|Check| MemCache[Memory LRU Cache]
end
Meta -->|Read/Write| FS
DB -->|Index| FS
```
## Database Schema (SQLite)
The `AssetDatabase.db` contains a single `Assets` table:
| Column | Type | Description |
|--------|------|-------------|
| **Guid** | TEXT (PK) | The unique identifier of the asset. |
| **Path** | TEXT | Relative path from `Assets/` folder. Indexed for fast lookup. |
| **Version** | INTEGER | Importer version for migration support. |
| **Tags** | TEXT | JSON array of string tags. |
| **FileHash** | TEXT | SHA256 hash of the source file content. |
| **DependencyGuids** | TEXT | JSON array of GUIDs this asset depends on. |
| **LastModified** | INTEGER | Unix timestamp of last modification. |
## Detailed Subsystems
### Metadata System (`.gmeta`)
* **Format**: JSON.
* **Content**: GUID, Version, Tags, ImporterSettings (per importer type).
* **Strategy**: The `.gmeta` file is the *only* place the persistent GUID lives. If the database is corrupted, it can be rebuilt entirely by scanning the file system and reading `.gmeta` files.
### Threading & Safety
* **Locks**:
* `s_dbLock`: Protects in-memory dictionaries (`s_assetPathLookup`) and dirty tracking.
* `s_commandLock`: Protects the command queue for file events.
* **Async**: Heavy I/O operations (DB access, File I/O) are async.
* **Channels**: Uses `System.Threading.Channels` to decouple high-frequency file system events from database processing.
### Importer Registry
* Uses `TypeCache` and reflection to find classes with `[AssetImporter]`.
* Mappings are stored in `s_importerTypeLookup` (Extension -> Type).
* Importers are stateless (instantiated on demand or cached as singletons depending on implementation, currently cached in `s_importerInstances`).
## Future Improvements / Known Limitations
1. **Binary Formats**: Currently, imported assets are stored as JSON. For large assets (textures, models), a binary format is required for performance.
2. **Dependency Graph**: While dependencies are stored, a full graph traversal for complex invalidation (e.g., if A changes, re-import B which depends on A) is partial.
3. **Cross-Process Locking**: SQLite is file-based; concurrent access from multiple editor instances needs careful file locking mode configuration.

View File

@@ -1,131 +0,0 @@
# Asset Database Documentation
The Asset Database is a core component of the Ghost Editor responsible for managing the lifecycle, storage, import, and retrieval of project assets. It provides a unified API for interacting with assets, ensuring that metadata (GUIDs, tags, settings) stays synchronized with files on disk.
## Key Features
- **GUID-based Asset Identification**: Every asset is uniquely identified by a stable GUID, stored in a sidecar `.gmeta` file.
- **Automatic Importing**: Monitors the file system for changes and automatically imports assets using registered importers.
- **Dependency Tracking**: Tracks dependencies between assets to ensure validity and trigger re-imports when dependencies change.
- **Caching**: Implements an LRU (Least Recently Used) cache for loaded assets to optimize performance.
- **SQLite Backed**: Uses a local SQLite database for fast lookups (Path <-> GUID) and metadata queries.
- **Metadata Management**: Handles `.gmeta` files automatically, including generation, validation, and cleanup.
## usage
### Initialization
The Asset Database must be initialized after the project is loaded.
```csharp
await AssetDatabase.Initialize(cancellationToken);
```
### Loading Assets
Assets can be loaded by GUID or by Path.
```csharp
// Load by Path
var result = AssetDatabase.LoadAssetAtPath<TextureAsset>("Assets/Textures/my_texture.png");
if (result.IsSuccess)
{
var texture = result.Value;
}
// Load by GUID
var guid = ...;
var result = AssetDatabase.LoadAsset<TextureAsset>(guid);
```
### File Operations
Always use the `AssetDatabase` API for file operations to ensure metadata is preserved.
```csharp
// Create
await AssetDatabase.CreateAssetAsync("Assets/Data/config.json", dataBytes);
// Move
await AssetDatabase.MoveAssetAsync("Assets/Old/file.txt", "Assets/New/file.txt");
// Copy
await AssetDatabase.CopyAssetAsync("Assets/template.txt", "Assets/instance.txt");
// Delete
await AssetDatabase.DeleteAssetAsync("Assets/garbage.tmp");
```
### Searching
Find assets using wildcards or tags.
```csharp
// Find all PNGs
var guids = await AssetDatabase.FindAssetsByNameAsync("*.png");
// Find assets with a specific tag
var enemyAssets = await AssetDatabase.FindAssetsByTagAsync("Enemy");
```
### Tags
Manage asset tags for organization.
```csharp
// Get tags
var tagsResult = await AssetDatabase.GetAssetTagsAsync(guid);
// Set tags
await AssetDatabase.SetAssetTagsAsync(guid, new List<string> { "Level1", "Prop" });
```
### Opening Assets
Open an asset using its registered handler or the system default.
```csharp
AssetDatabase.OpenAsset("Assets/Docs/readme.txt");
```
## Extending the Asset Database
### Creating a New Importer
To support a new file type, create a class that inherits from `AssetImporter<T>` and decorate it with the `[AssetImporter]` attribute.
```csharp
[AssetImporter(".myfmt")]
internal class MyFormatImporter : AssetImporter<MyFormatSettings>
{
public override async Task<Result> ImportAsync(string assetPath, AssetMeta meta)
{
var settings = GetSettings(meta);
// 1. Read source file
// 2. Process data
// 3. Save imported data using AssetDatabase.SaveImportedAsset
var myAsset = new MyAsset(meta.Guid) { ... };
return AssetDatabase.SaveImportedAsset(meta.Guid, myAsset);
}
}
internal class MyFormatSettings : ImporterSettings
{
public float Scale { get; set; } = 1.0f;
}
```
### Creating an Open Handler
To define custom behavior when an asset is opened (e.g., double-clicked in the editor), use the `[AssetOpenHandler]` attribute.
```csharp
internal static class MyHandlers
{
[AssetOpenHandler(".myfmt")]
private static void OpenMyFormat(string path)
{
// Open custom editor window
}
}
```
## Internal Architecture
- **AssetDatabase.cs**: Core initialization and event coordination.
- **AssetDatabase.SQLite.cs**: Database table management and queries.
- **AssetDatabase.Meta.cs**: `.gmeta` file handling and file system watcher events.
- **AssetDatabase.Importer.cs**: Importer discovery and execution.
- **AssetDatabase.Loader.cs**: Asset loading and caching logic.

View File

@@ -1,81 +0,0 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle;
public abstract class AssetImporter
{
/// <summary>
/// Import the asset at the specified path with the given settings.
/// </summary>
/// <param name="assetPath">Full path to the source asset file.</param>
/// <param name="meta">Metadata for the asset.</param>
/// <param name="token">Cancellation token.</param>
/// <returns>Result indicating success or failure.</returns>
public abstract ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default);
/// <summary>
/// Export in-memory asset data to disk.
/// Override this method to support creating assets from code.
/// </summary>
/// <typeparam name="T">Type of asset data to export.</typeparam>
/// <param name="assetPath">Full path where the asset should be saved.</param>
/// <param name="assetData">In-memory asset data to serialize.</param>
/// <param name="meta">Metadata for the asset.</param>
/// <param name="token">Cancellation token.</param>
/// <returns>Result indicating success or failure.</returns>
public virtual ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
where T : class
{
return ValueTask.FromResult(Result.Failure("This importer does not support exporting assets."));
}
/// <summary>
/// Validate dependencies referenced by this asset.
/// Dependencies are extracted from asset content during import and stored in the database.
/// </summary>
/// <param name="dependencies">List of dependency GUIDs extracted from the asset.</param>
/// <returns>Result indicating if all dependencies are valid.</returns>
protected virtual ValueTask<Result> ValidateDependenciesAsync(List<Guid> dependencies, CancellationToken token = default)
{
foreach (var dependencyGuid in dependencies)
{
var path = AssetDatabase.GuidToPath(dependencyGuid);
if (path.IsFailure)
{
return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}"));
}
if (!File.Exists(path.Value))
{
return ValueTask.FromResult(Result.Failure($"Dependency file does not exist: {path.Value}"));
}
}
return ValueTask.FromResult(Result.Success());
}
}
public abstract class AssetImporter<TSettings> : AssetImporter
where TSettings : ImporterSettings, new()
{
/// <summary>
/// Get the settings for this importer from the metadata.
/// Creates default settings if none exist.
/// </summary>
/// <param name="meta">Asset metadata.</param>
/// <returns>The importer settings.</returns>
protected TSettings GetSettings(AssetMeta meta)
{
var typeName = GetType().Name;
var settings = meta.GetImporterSettings<TSettings>(typeName);
if (settings != null)
{
return settings;
}
var defaultSettings = new TSettings();
meta.SetImporterSettings(typeName, defaultSettings);
return defaultSettings;
}
}

View File

@@ -1,15 +0,0 @@
namespace Ghost.Editor.Core.AssetHandle;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class AssetImporterAttribute : Attribute
{
public string[] SupportedExtensions
{
get;
}
public AssetImporterAttribute(params string[] supportedExtensions)
{
SupportedExtensions = supportedExtensions;
}
}

View File

@@ -1,85 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// Metadata for an asset, stored in .gmeta files.
/// Contains GUID, version, tags, and importer settings.
/// FileHash and Dependencies are stored in the database only, not in .gmeta files.
/// </summary>
public class AssetMeta
{
/// <summary>
/// Unique identifier for the asset.
/// </summary>
[JsonPropertyName("Guid")]
public Guid Guid
{
get;
set;
}
/// <summary>
/// Version of the asset pipeline (not the asset itself).
/// Used for migration when the asset pipeline is redesigned.
/// </summary>
[JsonPropertyName("Version")]
public int Version
{
get;
set;
} = 1;
/// <summary>
/// Tags for categorizing and searching assets.
/// </summary>
[JsonPropertyName("Tags")]
public List<string> Tags
{
get;
set;
} = new();
/// <summary>
/// Importer settings specific to this asset.
/// The key is the importer type name, and the value is a JSON element containing the settings.
/// Use GetImporterSettings&lt;T&gt;() and SetImporterSettings&lt;T&gt;() to work with strongly-typed settings.
/// </summary>
[JsonPropertyName("ImporterSettings")]
public Dictionary<string, JsonElement> ImporterSettings
{
get;
set;
} = new();
/// <summary>
/// Get importer settings of a specific type.
/// </summary>
public T? GetImporterSettings<T>(string importerName) where T : ImporterSettings
{
if (ImporterSettings.TryGetValue(importerName, out var element))
{
return element.Deserialize<T>();
}
return null;
}
/// <summary>
/// Set importer settings.
/// </summary>
public void SetImporterSettings<T>(string importerName, T settings) where T : ImporterSettings
{
var element = JsonSerializer.SerializeToElement(settings);
ImporterSettings[importerName] = element;
}
/// <summary>
/// Set importer settings (non-generic overload).
/// </summary>
internal void SetImporterSettings(string importerName, ImporterSettings settings)
{
var element = JsonSerializer.SerializeToElement(settings, settings.GetType());
ImporterSettings[importerName] = element;
}
}

View File

@@ -1,15 +0,0 @@
namespace Ghost.Editor.Core.AssetHandle;
[AttributeUsage(AttributeTargets.Method)]
public class AssetOpenHandlerAttribute : Attribute
{
public string[] Extensions
{
get;
}
public AssetOpenHandlerAttribute(params string[] extensions)
{
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
}
}

View File

@@ -1,5 +0,0 @@
namespace Ghost.Editor.Core.AssetHandle;
public abstract class ImporterSettings
{
}

View File

@@ -1,70 +0,0 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle.Importers;
/// <summary>
/// Example importer settings for text assets.
/// </summary>
internal class TextImporterSettings : ImporterSettings
{
public string Encoding
{
get;
set;
} = "UTF-8";
public bool TrimWhitespace
{
get;
set;
} = false;
}
/// <summary>
/// Example importer for text files (.txt, .md).
/// This is a simple test importer to demonstrate the asset import system.
/// </summary>
[AssetImporter(".txt", ".md")]
internal class TextImporter : AssetImporter<TextImporterSettings>
{
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
{
var settings = GetSettings(meta);
// Text files typically don't have dependencies
// If they did, you would extract them from the content here
var dependencies = new List<Guid>();
// Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies);
if (depResult.IsFailure)
{
return depResult;
}
try
{
// Read the file
var content = await File.ReadAllTextAsync(assetPath, token);
if (settings.TrimWhitespace)
{
content = content.Trim();
}
// TODO: Process the text content
// For example:
// - Convert to a specific format
// - Extract metadata
// - Generate assets
// - Save to output folder
// For now, just report success
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to import text asset: {ex.Message}");
}
}
}

View File

@@ -1,279 +0,0 @@
using Ghost.Core;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle.Importers;
/// <summary>
/// Importer settings for texture assets.
/// </summary>
internal class TextureImporterSettings : ImporterSettings
{
/// <summary>
/// Whether to generate mipmaps for the texture.
/// </summary>
public bool GenerateMipmaps
{
get;
set;
} = true;
/// <summary>
/// Whether the texture uses sRGB color space.
/// </summary>
public bool SRGB
{
get;
set;
} = true;
/// <summary>
/// Maximum texture size. Images larger than this will be downscaled.
/// </summary>
public uint MaxSize
{
get;
set;
} = 2048;
/// <summary>
/// Texture compression format.
/// Options: "None", "BC1", "BC3", "BC7"
/// </summary>
public string CompressionFormat
{
get;
set;
} = "None";
/// <summary>
/// Texture filter mode.
/// Options: "Point", "Bilinear", "Trilinear"
/// </summary>
public string FilterMode
{
get;
set;
} = "Bilinear";
/// <summary>
/// Texture wrap mode.
/// Options: "Repeat", "Clamp", "Mirror"
/// </summary>
public string WrapMode
{
get;
set;
} = "Repeat";
}
/// <summary>
/// Importer for texture files (.png, .jpg, .jpeg, .dds, .tga, .bmp).
/// Processes image files and converts them into engine-ready texture assets.
/// </summary>
[AssetImporter(".png", ".jpg", ".jpeg", ".dds", ".tga", ".bmp")]
internal class TextureImporter : AssetImporter<TextureImporterSettings>
{
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
{
var settings = GetSettings(meta);
// Textures typically don't reference other assets as dependencies
// If they did (e.g., normal maps referencing base textures), extract here
var dependencies = new List<Guid>();
// Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies, token);
if (depResult.IsFailure)
{
return depResult;
}
try
{
// Check if file exists
if (!File.Exists(assetPath))
{
return Result.Failure($"Source texture file not found: {assetPath}");
}
// Get image dimensions (simplified - in real implementation would use image library)
var (width, height) = GetImageDimensions(assetPath);
if (width == 0 || height == 0)
{
return Result.Failure("Failed to read image dimensions");
}
// Apply max size constraint
if (width > settings.MaxSize || height > settings.MaxSize)
{
var scale = Math.Min(settings.MaxSize / (float)width, settings.MaxSize / (float)height);
width = (uint)(width * scale);
height = (uint)(height * scale);
}
// Calculate mipmap count
uint mipLevels = 1;
if (settings.GenerateMipmaps)
{
mipLevels = CalculateMipLevels(width, height);
}
// Determine format
var format = settings.CompressionFormat == "None" ? "RGBA8" : settings.CompressionFormat;
// Create texture asset
var textureAsset = new TextureAsset(meta.Guid, Path.GetFileNameWithoutExtension(assetPath))
{
Width = width,
Height = height,
MipLevels = mipLevels,
Format = format,
IsSRGB = settings.SRGB,
SourcePath = assetPath
};
// Save the imported asset data
var saveResult = AssetDatabase.SaveImportedAsset(meta.Guid, textureAsset);
if (saveResult.IsFailure)
{
return Result.Failure($"Failed to save texture asset: {saveResult.Message}");
}
// In a real implementation, you would:
// 1. Load the image using a library like ImageSharp or StbImageSharp
// 2. Resize if needed
// 3. Generate mipmaps
// 4. Compress if needed
// 5. Save the processed texture data to the ImportedAssets folder
// 6. Update the hash in database
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to import texture: {ex.Message}");
}
}
/// <summary>
/// Get image dimensions from file.
/// Simplified implementation - in production, use an image library.
/// </summary>
private static (uint width, uint height) GetImageDimensions(string imagePath)
{
// This is a placeholder implementation
// In a real implementation, you would use a library like:
// - ImageSharp
// - StbImageSharp
// - DirectXTex (for DDS files)
var extension = Path.GetExtension(imagePath).ToLowerInvariant();
if (extension == ".dds")
{
// For DDS files, read the header
// DDS header format: https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header
return ReadDDSHeader(imagePath);
}
else
{
// For PNG/JPG/etc, we would use an image library
// For now, return placeholder values
return (1024, 1024);
}
}
/// <summary>
/// Read DDS file header to get dimensions.
/// </summary>
private static (uint width, uint height) ReadDDSHeader(string ddsPath)
{
try
{
using var stream = File.OpenRead(ddsPath);
using var reader = new BinaryReader(stream);
// Read magic number (should be "DDS ")
var magic = reader.ReadUInt32();
if (magic != 0x20534444) // "DDS " in little-endian
{
return (0, 0);
}
// Read header size (should be 124)
var headerSize = reader.ReadUInt32();
if (headerSize != 124)
{
return (0, 0);
}
// Skip flags
reader.ReadUInt32();
// Read height and width
var height = reader.ReadUInt32();
var width = reader.ReadUInt32();
return (width, height);
}
catch
{
return (0, 0);
}
}
/// <summary>
/// Export a texture asset from memory to disk.
/// </summary>
public override async ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
{
if (assetData is not TextureAsset textureAsset)
{
return Result.Failure($"Asset data is not a TextureAsset, got {typeof(T).Name}");
}
try
{
// In a real implementation, you would:
// 1. Convert the texture data to the appropriate format
// 2. Write the image file (PNG, DDS, etc.)
// 3. Save metadata
// For now, just save metadata as JSON
var json = JsonSerializer.Serialize(textureAsset, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(assetPath, json, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to export texture: {ex.Message}");
}
}
/// <summary>
/// Calculate number of mipmap levels for a given texture size.
/// </summary>
private static uint CalculateMipLevels(uint width, uint height)
{
if (width == 0 || height == 0)
{
return 0;
}
uint count = 1;
while (width > 1 || height > 1)
{
width >>= 1;
height >>= 1;
count++;
}
return count;
}
}

View File

@@ -1,22 +0,0 @@
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// The base class for all asset types in the Ghost Editor.
/// </summary>
public abstract class Asset
{
public abstract string Name
{
get; set;
}
public Guid ID
{
get;
}
protected Asset(Guid id)
{
ID = id;
}
}

View File

@@ -1,75 +0,0 @@
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// Represents a texture asset.
/// </summary>
public class TextureAsset : Asset
{
public override string Name
{
get;
set;
}
/// <summary>
/// Width of the texture in pixels.
/// </summary>
public uint Width
{
get;
set;
}
/// <summary>
/// Height of the texture in pixels.
/// </summary>
public uint Height
{
get;
set;
}
/// <summary>
/// Number of mipmap levels.
/// </summary>
public uint MipLevels
{
get;
set;
}
/// <summary>
/// Texture format (e.g., "RGBA8", "BC1", "BC7").
/// </summary>
public string Format
{
get;
set;
}
/// <summary>
/// Whether the texture uses sRGB color space.
/// </summary>
public bool IsSRGB
{
get;
set;
}
/// <summary>
/// Relative path to the source image file.
/// </summary>
public string SourcePath
{
get;
set;
}
public TextureAsset(Guid id, string name) : base(id)
{
Name = name;
Format = "RGBA8";
IsSRGB = true;
SourcePath = string.Empty;
}
}

View File

@@ -1,7 +0,0 @@
namespace Ghost.Editor.Core.Contracts;
public interface INavigationAware
{
public void OnNavigatedTo(object? parameter);
public void OnNavigatedFrom();
}

View File

@@ -1,27 +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"
xmlns:local="using:Ghost.Editor.Core.Controls.Internal">
<Style TargetType="local:ComponentView">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ComponentView">
<StackPanel Margin="0,0,0,16">
<Border
Padding="8"
HorizontalAlignment="Stretch"
Background="{ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{TemplateBinding HeaderText}" />
</Border>
<StackPanel
x:Name="ContentContainer"
Margin="8,2,2,0"
Spacing="2" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,157 +0,0 @@
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Resources;
using Ghost.Editor.Core.Utilities;
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Controls.Internal;
internal sealed unsafe partial class ComponentView : Control
{
private delegate void EditorUpdate();
private StackPanel? _contentContainer;
private readonly World? _world;
private readonly Entity _entity = Entity.Invalid;
private readonly Type? _componentType;
private readonly ComponentInfo _componentInfo;
private object? _managedInstance;
private void* _pComponentData;
private ComponentEditor? _customEditor;
private PropertyField[]? _propertyFields;
private EditorUpdate? _editorUpdate;
public string HeaderText
{
get => (string)GetValue(HeaderTextProperty);
set => SetValue(HeaderTextProperty, value);
}
public static readonly DependencyProperty HeaderTextProperty =
DependencyProperty.Register(nameof(HeaderText), typeof(string), typeof(ComponentView), new PropertyMetadata(string.Empty));
internal ComponentView()
{
DefaultStyleKey = typeof(ComponentView);
Unloaded += (s, e) =>
{
_customEditor?.Destroy();
_contentContainer = null;
_customEditor = null;
_propertyFields = null;
};
}
public ComponentView(string header, World world, Entity entity, Type componentType) : this()
{
HeaderText = header;
_world = world;
_entity = entity;
_componentType = componentType;
_componentInfo = ComponentRegistry.GetComponentInfo(componentType);
}
protected override void OnApplyTemplate()
{
_contentContainer = (StackPanel)GetTemplateChild("ContentContainer");
base.OnApplyTemplate();
ReBuild();
}
private void ReflectionUpdate()
{
if (_propertyFields == null)
{
return;
}
foreach (var propertyField in _propertyFields)
{
propertyField.UpdateValue();
}
}
private void CustomEditorUpdate()
{
_customEditor?.Update();
}
public void ReBuild()
{
if (_contentContainer == null)
{
return;
}
_contentContainer.Children.Clear();
if (_world == null || _componentType == null || _entity == Entity.Invalid)
{
return;
}
if (_propertyFields != null)
{
foreach (var propertyField in _propertyFields)
{
propertyField.OnValueChanged -= OnPropertyValueChanged;
}
}
var componentObject = new ComponentObject(_world, _entity);
var editorType = TypeCache.GetTypes().FirstOrDefault(t =>
typeof(ComponentEditor).IsAssignableFrom(t) &&
t.GetCustomAttribute<CustomEditorAttribute>()?.TargetType.IsAssignableFrom(_componentType) == true);
if (editorType != null)
{
_customEditor = (ComponentEditor)Activator.CreateInstance(editorType)!;
_customEditor.Initialize(componentObject);
_customEditor.Create(_contentContainer);
}
else
{
var fields = _componentType.GetFields(StaticResource.ComponentPropertyBindingFlags);
_propertyFields = new PropertyField[fields.Length];
_pComponentData = _world.EntityManager.GetComponent(_entity, _componentInfo.id);
_managedInstance = Marshal.PtrToStructure((nint)_pComponentData, _componentType);
if (_managedInstance == null)
{
return;
}
for (var i = 0; i < fields.Length; i++)
{
var field = fields[i];
var propertyField = PropertyField.Create(field.Name, field, _managedInstance);
propertyField.OnValueChanged += OnPropertyValueChanged;
_propertyFields[i] = propertyField;
_contentContainer.Children.Add(propertyField);
}
}
_editorUpdate = _customEditor == null ? ReflectionUpdate : CustomEditorUpdate;
_editorUpdate();
}
private void OnPropertyValueChanged(PropertyField field)
{
if (_managedInstance == null || _pComponentData == null)
{
return;
}
Marshal.StructureToPtr(_managedInstance, (nint)_pComponentData, false);
}
}

View File

@@ -1,5 +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"
xmlns:local="using:Ghost.Editor.Controls.Internal" />

View File

@@ -1,46 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Editor.Core</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<!-- in .net 10, field keyword is not preview anymore, but we are still waiting roslyn team to update their code analyzer packages -->
<langversion>preview</langversion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Data\Ghost.Data.csproj" />
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\BasicInput\PropertyField.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\BasicInput\Vector3Field.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\ControlsDictionary.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\Internal\ComponentDataView.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\Internal\NavigationTabView.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>

View File

@@ -1,40 +0,0 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
public abstract class ComponentEditor
{
private ComponentObject _componentObject;
/// <summary>
/// Represents the underlying component object used by this class to manage its functionality.
/// </summary>
protected ComponentObject ComponentObject => _componentObject;
internal void Initialize(ComponentObject componentObject)
{
_componentObject = componentObject;
}
/// <summary>
/// Called when the component editor is created.
/// </summary>
/// <param name="container">The container to add the editor controls to.</param>
public virtual void Create(StackPanel container)
{
}
/// <summary>
/// Called when the component editor needs to update its UI based on the current state of the component data.
/// </summary>
public virtual void Update()
{
}
/// <summary>
/// Called when the component editor is destroyed.
/// </summary>
public virtual void Destroy()
{
}
}

View File

@@ -1,10 +0,0 @@
namespace Ghost.Editor.Core.Inspector;
[AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute(Type targetType) : Attribute
{
internal Type TargetType
{
get;
} = targetType;
}

View File

@@ -1,13 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
public interface IInspectable
{
public IconSource? CreateIcon();
public UIElement? CreateHeader();
public UIElement? CreateInspector();
}

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
using CommunityToolkit.WinUI.Behaviors;
namespace Ghost.Editor.Core.Notifications;
public interface INotificationService
{
public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null);
public void ShowNotification(Notification notification);
}

View File

@@ -1,9 +0,0 @@
namespace Ghost.Editor.Core.Progress;
public interface IProgressService
{
public void ShowProgress(string message, double progress = 0.0);
public void ShowIndeterminateProgress(string message);
public void SetProgress(double progress);
public void HideProgress();
}

View File

@@ -1,18 +0,0 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Resources;
public static class EditorIconSource
{
public static readonly IconSource scene_24 = new FontIconSource
{
Glyph = "\uF159",
FontSize = 24
};
public static readonly IconSource entity_24 = new FontIconSource
{
Glyph = "\uF158",
FontSize = 24
};
}

View File

@@ -1,45 +0,0 @@
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class EntityNode : SceneGraphNode
{
private readonly Entity _entity;
public Entity Entity => _entity;
public override IconSource? CreateIcon()
{
return new FontIconSource
{
Glyph = "\uF158"
};
}
public override UIElement? CreateHeader()
{
return null;
}
public override UIElement? CreateInspector()
{
throw new NotImplementedException();
}
public override DataTemplate GetSceneHierarchyTemplate()
{
var template = @"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:sg=""using:Ghost.Editor.Core.SceneGraph"" x:Key=""EntityTemplate"" x:DataType=""sg:SceneGraphNode"">
<TreeViewItem AutomationProperties.Name=""{x:Bind Name, Mode=OneWay}"" ItemsSource=""{x:Bind Children, Mode=OneWay}"">
<StackPanel Margin=""10,0"" Orientation=""Horizontal"">
<FontIcon FontSize=""14"" Glyph=""&#xF158;"" />
<TextBlock Margin=""5,0,0,0"" Text=""{x:Bind Name, Mode=OneWay}"" />
</StackPanel>
</TreeViewItem>
</DataTemplate>";
return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
}
}

View File

@@ -1,87 +0,0 @@
# Architecture Plan: Scene Graph and Scene Representation
The Scene Graph is a hierarchical structure that represents all the objects and entities within a 3D scene in the Ghost Editor.
## Scene Graph (Editor representation of runtime data)
There should be three main types of nodes in the Scene Graph for now:
1. **Scene Graph Node**: The base class for all nodes in the Scene Graph.
2. **Entity Node**: Represents an individual entity within a scene. Name stored here, not runtime component.
3. **Scene Node**: Represents a Scene object, which can contain multiple entities. Name stored here not runtime data.
### Editor World
Editor contains a different world compares to the runtime world. When user click the Play button, we will create a runtime world and load the scene data from the editor world to the runtime world.
This allows us to
1. Unload the runtime only systems like physics, rendering, etc when user stop playing.
2. Load editor only systems like gizmos, debug, etc when user stop playing.
3. Allow editor only entities like editor camera, editor lights, etc to exist in the editor world without affecting the runtime world.
### Editor Hierarchy
The Scene Graph should be represented as a tree structure in the editor (TreeView in WinUI 3), where:
- The top level nodes represents the loaded Scenes in the editor world.
- Levels below the Scene nodes represents the Entity nodes that belong to that scene.
- Each Entity node can have child Entity nodes representing parent-child relationships between entities.
An example hierarchy could look like this:
```
- Scene 1
- Entity A
- Entity B
- Entity C
- Scene 2
- Entity D
```
## Scene (The runtime representation)
A Scene is a collection of entities with SceneID component from a world that are grouped together. There can be multiple scenes in a world.
### Save a Scene
When save a scene, all entities with the SceneID component matching the scene's ID should be included in the saved data.
When an Entity references another Entity in the same scene, we should store the file local id instead of the global entity id.
For example, if Entity A (id: 10, 5th in scene) references Entity B (id: 20, 50th in scene) in the same scene, in the saved data for Entity A,
we should store 50 (the file local id) as the reference to Entity B instead of 20 (the global entity id).
> We does not allow cross-scene references for now because ideally it's not a good practice to have cross-scene references.
> We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
### Load a Scene
When loading a scene, we need to reconstruct the entities and their relationships based on the saved data.
1. We allocate the entities in the world and assign them new global entity IDs.
2. We remap the file local IDs to the new global entity IDs and change the references accordingly.
For example if Entity A (file local id: 5) references Entity B (file local id: 50) in the saved data,
we need to find the new global entity IDs for both entities after loading and update the reference in Entity A to point to the new global entity ID of Entity B.
### Data format
The scene data should be stored in a structured format (JSON and binary) that includes:
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
- References between entities using file local IDs
> The name of the saved scene file should match the name of the scene node in the editor.
JSON should only be used in the editor and JSON serialization/deserialization logic should also only exist in the editor codebase (Ghost.Editor.Core). Reflection is allowed here.
Binary format should be used in the runtime for better performance. The runtime codebase (Ghost.Engine) must be aot compatible.
Currently we strict the IComponent to must be unmanaged and blittable types.
However, we also support ManagedEntity and ManagedEntityRef with ScriptComponent to allow OOP like logic for common gameplay logic that DOD pattern is not suitable for.
Serializing/deserializing with those components will be tricky. We can use MemoryPack (already installed) for binary serialization/deserialization because it supports both unmanaged and managed types.
## What need to implement
- [ ] Scene type for the runtime representation if needed
- [ ] Scene Graph data structures (SceneNode, EntityNode)
- [ ] Editor World management (loading/unloading scenes, managing entities)
- [ ] Scene saving/loading logic with file local ID remapping
- [ ] Serialization/deserialization logic for scene data (JSON for editor, binary for runtime)
- [ ] UI integration for displaying and managing the Scene Graph in the editor with WinUI 3 TreeView

View File

@@ -1,45 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class SceneNode : SceneGraphNode
{
public override IconSource? CreateIcon()
{
return new FontIconSource
{
Glyph = "\uF156"
};
}
// TODO: Implement custom header and inspector UI for the SceneNode
public override UIElement? CreateHeader()
{
return null;
}
public override UIElement? CreateInspector()
{
return null;
}
public override DataTemplate GetSceneHierarchyTemplate()
{
var template = @"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:sg=""using:Ghost.Editor.Core.SceneGraph"" x:DataType=""sg:SceneGraphNode"">
<TreeViewItem
AutomationProperties.Name=""{x:Bind Name, Mode=OneWay}""
Background=""{ThemeResource ControlSolidFillColorDefaultBrush}""
IsExpanded=""True""
ItemsSource=""{ x:Bind Children, Mode=OneWay}"" >
<StackPanel Orientation=""Horizontal"" >
<FontIcon FontSize=""14"" Glyph=""&#xF156;""/>
<TextBlock Margin=""10,0"" Text=""{ x:Bind Name, Mode=OneWay}""/>
</StackPanel>
</TreeViewItem>
</DataTemplate>";
return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
}
}

View File

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

View File

@@ -1,36 +0,0 @@
using Ghost.Core.Attributes;
using System.Reflection;
namespace Ghost.Editor.Core.Utilities;
public static class TypeCache
{
private static readonly TypeInfo[] s_types;
static TypeCache()
{
var loadableTypes = new List<Type>(512);
var assembliesToScan = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetCustomAttribute<EngineAssemblyAttribute>() != null);
foreach (var assembly in assembliesToScan)
{
try
{
loadableTypes.AddRange(assembly.GetTypes());
}
catch (ReflectionTypeLoadException ex)
{
var types = ex.Types.Where(t => t != null);
loadableTypes.AddRange(types!);
}
}
s_types = loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
}
public static Type[] GetTypes()
{
return s_types;
}
}

View File

@@ -1,30 +0,0 @@
using Ghost.Data.Resources;
using Ghost.Data.Services;
using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Xaml;
namespace Ghost.Editor;
internal static class ActivationHandler
{
private static void FolderInitialization()
{
if (!Directory.Exists(DataPath.s_applicationDataFolder))
{
Directory.CreateDirectory(DataPath.s_applicationDataFolder);
}
if (!Directory.Exists(DataPath.s_projectTemplateFolder))
{
Directory.CreateDirectory(DataPath.s_projectTemplateFolder);
}
}
public static void Handle(LaunchActivatedEventArgs args)
{
FolderInitialization();
ProjectService.EnsureDefaultTemplate();
EditorApplication.Initialize(((App)(Application.Current)).Host.Services);
}
}

View File

@@ -1,112 +0,0 @@
using Ghost.Core;
using Ghost.Editor.Core.AppState;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Notifications;
using Ghost.Editor.Core.Progress;
using Ghost.Editor.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml;
// 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;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
private Window? _window;
internal static Window? Window
{
get => (Current as App)!._window;
set
{
if (Current is App app)
{
// HACK: As far as I can tell, there is no proper application shutdown event in WinUI 3.
app._window?.Closed -= app.OnClosed;
app._window = value;
app._window?.Closed += app.OnClosed;
}
}
}
internal IHost Host
{
get;
}
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
internal App()
{
InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
{
HostHelper.AddLandingScope(context, services);
HostHelper.AddEngineScope(context, services);
services.AddSingleton<AppStateMachine>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IProgressService, ProgressService>();
services.AddSingleton<IInspectorService, InspectorService>();
})
.Build();
UnhandledException += App_UnhandledException;
}
internal static IServiceScope CreateScope()
{
return (Current as App)!.Host.Services.CreateScope();
}
public static T GetService<T>() where T : class
{
if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}
return service;
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
base.OnLaunched(args);
await Host.StartAsync();
ActivationHandler.Handle(args);
var stateMachine = GetService<AppStateMachine>();
stateMachine.RegisterState(StateKey.Landing, () => new LandingState());
stateMachine.RegisterState(StateKey.EngineEditor, () => new EditorState());
await stateMachine.TransitionToAsync(StateKey.Landing);
}
private void OnClosed(object? sender, WindowEventArgs args)
{
Host.StopAsync().GetAwaiter().GetResult();
Host.Dispose();
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError(e.Exception);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,20 +0,0 @@
using Ghost.Editor.Core.Inspector;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Components;
//[CustomEditor(typeof(Hierarchy))]
internal class HierarchyEditor : ComponentEditor
{
public void Create(ComponentObject componentObject, StackPanel container)
{
}
public void Update(ComponentObject componentObject)
{
}
public void Destroy(ComponentObject componentObject)
{
}
}

View File

@@ -1,65 +0,0 @@
using Ghost.Editor.Core.Controls;
using Ghost.Editor.Core.Inspector;
using Ghost.Engine.Components;
using Ghost.Engine.Utilities;
using Microsoft.UI.Xaml.Controls;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Editor.Components;
[CustomEditor(typeof(LocalToWorld))]
internal class LocalToWorldEditor : ComponentEditor
{
private Float3Field _translationField = null!;
private Float3Field _rotationField = null!;
private Float3Field _scaleField = null!;
public override void Create(StackPanel container)
{
_translationField = new Float3Field();
_rotationField = new Float3Field();
_scaleField = new Float3Field();
_translationField.OnValueChanged += (s, e) =>
{
ref var data = ref ComponentObject.GetData<LocalToWorld>();
data.matrix.c3.xyz = e.NewValue;
};
_rotationField.OnValueChanged += (s, e) =>
{
ref var data = ref ComponentObject.GetData<LocalToWorld>();
var newRotation = quaternion.EulerXYZ(e.NewValue * math.TORADIANS);
data.matrix.GetTRS(out var oldTranslation, out var _, out var oldScale);
data.matrix = float4x4.TRS(oldTranslation, newRotation, oldScale);
};
_scaleField.OnValueChanged += (s, e) =>
{
ref var data = ref ComponentObject.GetData<LocalToWorld>();
var newScale = e.NewValue;
data.matrix.GetTRS(out var oldTranslation, out var oldRotation, out var _);
data.matrix = float4x4.TRS(oldTranslation, oldRotation, newScale);
};
container.Children.Add(new PropertyField() { Label = "Position", Content = _translationField });
container.Children.Add(new PropertyField() { Label = "Rotation", Content = _rotationField });
container.Children.Add(new PropertyField() { Label = "Scale", Content = _scaleField });
}
public override void Update()
{
var data = ComponentObject.GetData<LocalToWorld>();
data.matrix.GetTRS(out var position, out var rotation, out var scale);
_translationField.Value = position;
_rotationField.Value = math.degrees(math.EulerXYZ(rotation));
_scaleField.Value = scale;
}
public override void Destroy()
{
}
}

View File

@@ -1,38 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.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();
}
}
}

View File

@@ -1,60 +0,0 @@
using Ghost.Core;
using Ghost.Data.Models;
using Ghost.Data.Services;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.View.Windows;
using Ghost.Engine;
namespace Ghost.Editor.Core.AppState;
internal class EditorState : IAppState
{
private EngineEditorWindow? _window;
private EngineCore? _engineCore;
public ValueTask<Result> OnExitingAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
_engineCore?.Dispose();
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnEnteringAsync(object? parameter)
{
if (parameter is not ProjectMetadataInfo metadataInfo)
{
return ValueTask.FromResult(Result.Failure("Invalid parameter for entering EditorState."));
}
ProjectService.CurrentProject = metadataInfo;
_engineCore = App.GetService<EngineCore>();
_engineCore.Init();
_window = App.GetService<EngineEditorWindow>();
_window.Activate();
App.Window = _window;
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnExitedAsync()
{
_window?.Close();
_window = null;
return ValueTask.FromResult(Result.Success());
}
public async ValueTask<Result> OnEnteredAsync(object? parameter)
{
await AssetDatabase.Initialize();
return Result.Success();
}
}

View File

@@ -1,42 +0,0 @@
using Ghost.Core;
using Ghost.Editor.View.Windows;
namespace Ghost.Editor.Core.AppState;
internal class LandingState : IAppState
{
private LandingWindow? _window;
public ValueTask<Result> OnExitingAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnEnteringAsync(object? parameter)
{
_window = App.GetService<LandingWindow>();
_window.Activate();
App.Window = _window;
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnExitedAsync()
{
_window?.Close();
_window = null;
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnEnteredAsync(object? parameter)
{
return ValueTask.FromResult(Result.Success());
}
}

View File

@@ -1,121 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<!-- in .net 10, field keyword is not preview anymore, but we are still waiting roslyn team to update their code analyzer packages -->
<langversion>preview</langversion>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Editor.Core\Ghost.Editor.Core.csproj" />
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Pages\Landing\CreateProjectPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Window\Landing.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Pages\Landing\OpenProjectPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Pages\EngineEditor\InspectorPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</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>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Pages\EngineEditor\ConsolePage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Override.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Windows\EngineEditorWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Pages\EngineEditor\ScenePage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<PropertyGroup Label="Globals" />
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<Nullable>enable</Nullable>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
<PublishAot>False</PublishAot>
<PublishTrimmed>False</PublishTrimmed>
<RootNamespace>Ghost.Editor</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -1,27 +0,0 @@
using System.Collections.ObjectModel;
namespace Ghost.Editor.Models;
internal class ExplorerItem(string name, string path, bool isDirectory)
{
public string Name
{
get;
} = name;
public string FullName
{
get;
} = path;
public bool IsDirectory
{
get;
} = isDirectory;
public ObservableCollection<ExplorerItem>? Children
{
get;
set;
}
}

View File

@@ -1,20 +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"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
xmlns:internal="using:Ghost.Editor.Controls.Internal">
<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>
<Style TargetType="internal:NavigationTabView">
<Setter Property="TabWidthMode" Value="Compact" />
</Style>
<Style TargetType="NumberBox" />
</ResourceDictionary>

View File

@@ -1,50 +0,0 @@
using Ghost.Data.Services;
using Ghost.Editor.View.Pages.EngineEditor;
using Ghost.Editor.View.Pages.Landing;
using Ghost.Editor.View.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;
namespace Ghost.Editor.Utilities;
internal static partial class HostHelper
{
public static void AddLandingScope(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<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<EngineCore>();
services.AddSingleton<EngineEditorWindow>();
services.AddSingleton<EngineEditorViewModel>();
services.AddTransient<ScenePage>();
services.AddTransient<HierarchyPage>();
services.AddTransient<HierarchyViewModel>();
services.AddTransient<ProjectPage>();
services.AddTransient<ProjectViewModel>();
services.AddTransient<ConsolePage>();
services.AddTransient<ConsoleViewModel>();
services.AddTransient<InspectorPage>();
services.AddTransient<InspectorViewModel>();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.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.Utilities.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" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Folder Tree View -->
<Grid
Grid.Column="0"
Padding="4"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,1,0">
<TreeView
x:Name="DirectoryTreeView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.SubDirectories}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectedItem="{x:Bind ViewModel.SelectedDirectory, Mode=TwoWay}">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="model:ExplorerItem">
<TreeViewItem ItemsSource="{x:Bind Children}">
<StackPanel Orientation="Horizontal">
<FontIcon
VerticalAlignment="Center"
FontSize="14"
Glyph="&#xE8B7;" />
<TextBlock
Margin="8,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
<!-- Files -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
Padding="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,0,1">
<BreadcrumbBar Height="15" />
</Grid>
<ScrollViewer
Grid.Row="1"
Padding="8"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<GridView
x:Name="AssetsGridView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.DirectoryAssets, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedAsset, Mode=TwoWay}">
<GridView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
<Setter Property="Margin" Value="2" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemTemplate>
<DataTemplate x:DataType="model:ExplorerItem">
<Grid
Width="100"
Height="100"
Padding="8"
DoubleTapped="GridViewItem_DoubleTapped"
IsDoubleTapEnabled="True">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="0.25*" />
</Grid.RowDefinitions>
<FontIcon FontSize="42" Glyph="{x:Bind FullName, Converter={StaticResource AssetPathToGlyphConverter}}" />
<TextBlock
Grid.Row="1"
Margin="8,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</ScrollViewer>
<Grid
Grid.Row="2"
Padding="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,1,0,0">
<TextBlock
VerticalAlignment="Center"
HorizontalTextAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedAsset.FullName, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</Grid>
</Grid>
</Grid>
</Page>

View File

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

View File

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

View File

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

View File

@@ -1,142 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.View.Pages.Landing.CreateProjectPage"
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:data="using:Ghost.Data.Models"
xmlns:editor="using:Ghost.Editor.Core.Controls"
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
NavigationCacheMode="Enabled"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Template Info -->
<Grid Grid.Column="0" Width="300">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="0,0,0,24"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="Template" />
<ListView
Grid.Row="1"
ItemsSource="{x:Bind ViewModel.templates}"
SelectedItem="{x:Bind ViewModel.SelectedTemplate, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="data:TemplateData">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ImageIcon
Grid.Column="0"
Width="24"
Height="24">
<ImageIcon.Source>
<BitmapImage UriSource="{x:Bind GetIconURI()}" />
</ImageIcon.Source>
</ImageIcon>
<TextBlock
Grid.Column="1"
Margin="8,0"
VerticalAlignment="Center"
Text="{x:Bind Info.Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<!-- Project Info -->
<Grid
Grid.Column="1"
Margin="16,0,0,0"
Padding="16"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="300" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" CornerRadius="4">
<Image VerticalAlignment="Center" Stretch="UniformToFill">
<Image.Source>
<BitmapImage UriSource="{x:Bind ViewModel.SelectedTemplate.Value.GetPreviewURI(), Mode=OneWay}" />
</Image.Source>
</Image>
<Grid
MaxHeight="100"
VerticalAlignment="Bottom"
Background="{ThemeResource ControlOnImageFillColorDefaultBrush}">
<TextBlock
Margin="16"
VerticalAlignment="Bottom"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Description, Mode=OneWay}" />
</Grid>
</Grid>
<StackPanel Grid.Row="1" Margin="8,0">
<TextBlock
Margin="0,16,0,8"
Style="{StaticResource TitleTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Name, Mode=OneWay}" />
<TextBlock
Margin="0,8,0,16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="Project Settings" />
<editor:PropertyField Label="Name">
<TextBox Text="{x:Bind ViewModel.ProjectName, Mode=TwoWay}" />
</editor:PropertyField>
<editor:PropertyField Label="Location">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
IsReadOnly="True"
Text="{x:Bind ViewModel.ProjectLocation, Mode=TwoWay}" />
<Button
Grid.Column="1"
Margin="4,0,0,0"
VerticalAlignment="Stretch"
Command="{x:Bind ViewModel.SelectionProjectLocationCommand}">
<FontIcon FontSize="16" Glyph="&#xE8DA;" />
</Button>
</Grid>
</editor:PropertyField>
</StackPanel>
<Grid Grid.Row="2">
<Button
Width="150"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.CreateProjectCommand}"
Content="Create"
Style="{ThemeResource AccentButtonStyle}" />
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -1,32 +0,0 @@
using Ghost.Editor.ViewModels.Pages.Landing;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Ghost.Editor.View.Pages.Landing;
internal sealed partial class CreateProjectPage : Page
{
public CreateProjectViewModel ViewModel
{
get;
}
public CreateProjectPage()
{
ViewModel = App.GetService<CreateProjectViewModel>();
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.OnNavigatedTo(e.Parameter);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
ViewModel.OnNavigatedFrom();
}
}

View File

@@ -1,165 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.View.Pages.Landing.OpenProjectPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Ghost.Editor.Utilities.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"
mc:Ignorable="d">
<Page.Resources>
<converters:GetDirectoryNameConverter x:Key="DirNameConverter" />
</Page.Resources>
<Grid x:Name="MainContainer">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="16,4">
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="Projects" />
<AutoSuggestBox
Width="300"
HorizontalAlignment="Right"
PlaceholderText="Search project by name"
QueryIcon="Find" />
</Grid>
<!-- Header for the ListView -->
<Grid Grid.Row="1" Margin="28,16,45,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="165" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="NAME" />
<TextBlock
Grid.Column="1"
HorizontalAlignment="Right"
Style="{StaticResource CaptionTextBlockStyle}"
Text="LAST OPEN" />
<TextBlock
Grid.Column="2"
HorizontalAlignment="Right"
Style="{StaticResource CaptionTextBlockStyle}"
Text="ENGINE VERSION" />
</Grid>
<!-- Project ListView -->
<Grid
Grid.Row="2"
Padding="8"
AllowDrop="True"
DragEnter="ProjectContainer_DragEnter"
DragLeave="ProjectContainer_DragLeave"
DragOver="ProjectContainer_DragOver"
Drop="ProjectContainer_Drop">
<ListView
Padding="4,8"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{StaticResource OverlayCornerRadius}"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind ViewModel.projects}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="data:ProjectMetadataInfo">
<Grid Height="64" Padding="4,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="65" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
VerticalAlignment="Center"
FontSize="16"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Metadata.Name}" />
<TextBlock
Grid.Row="1"
Margin="0,4,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Path, Converter={StaticResource DirNameConverter}}" />
</Grid>
<TextBlock
Grid.Column="1"
Margin="16,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Bind Metadata.LastOpened}" />
<TextBlock
Grid.Column="2"
Margin="16,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Bind Metadata.EngineVersion}" />
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Background="Transparent"
BorderThickness="0">
<FontIcon Glyph="&#xE712;" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Drag Visual -->
<Grid
x:Name="DragVisual"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardStrokeColorDefaultBrush}"
BorderBrush="{ThemeResource ControlStrongStrokeColorDefaultBrush}"
BorderThickness="2"
CornerRadius="{StaticResource OverlayCornerRadius}"
Visibility="{x:Bind ViewModel.DragVisibility, Mode=OneWay}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource TitleTextBlockStyle}"
Text="Drage Project Folder Here" />
</Grid>
</Grid>
<!-- Empty Place Holder -->
<Grid
x:Name="EmptyPlaceHolder"
Grid.Row="2"
Visibility="{x:Bind ViewModel.EmptyVisibility, Mode=OneWay}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}"
Text="No projects found" />
</Grid>
</Grid>
</Page>

View File

@@ -1,72 +0,0 @@
using Ghost.Data.Models;
using Ghost.Editor.ViewModels.Pages.Landing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using Windows.ApplicationModel.DataTransfer;
namespace Ghost.Editor.View.Pages.Landing;
internal sealed partial class OpenProjectPage : Page
{
public OpenProjectViewModel ViewModel
{
get;
}
public OpenProjectPage()
{
ViewModel = App.GetService<OpenProjectViewModel>();
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.OnNavigatedTo(e.Parameter);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
ViewModel.OnNavigatedFrom();
}
private void ProjectContainer_DragEnter(object sender, DragEventArgs e)
{
ViewModel.DragVisibility = Visibility.Visible;
ViewModel.EmptyVisibility = Visibility.Collapsed;
}
private void ProjectContainer_DragLeave(object sender, DragEventArgs e)
{
ViewModel.DragVisibility = Visibility.Collapsed;
ViewModel.UpdateEmptyPlaceHolderVisibility();
}
private void ProjectContainer_DragOver(object sender, DragEventArgs e)
{
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
e.AcceptedOperation = DataPackageOperation.Link;
}
else
{
e.AcceptedOperation = DataPackageOperation.None;
}
}
private async void ProjectContainer_Drop(object sender, DragEventArgs e)
{
await ViewModel.ContentDrop(e.DataView);
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is ProjectMetadataInfo project)
{
await Task.Yield();
await ViewModel.OpenProjectAsync(project);
}
}
}

View File

@@ -1,249 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<winex:WindowEx
x:Class="Ghost.Editor.View.Windows.EngineEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ee="using:Ghost.Editor.View.Pages.EngineEditor"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:internal="using:Ghost.Editor.Controls.Internal"
xmlns:local="using:Ghost.Editor.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx"
Activated="WindowEx_Activated"
Closed="WindowEx_Closed"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Titlebar -->
<StackPanel
Grid.Row="0"
Padding="8"
Orientation="Horizontal">
<ImageIcon
Width="24"
Height="24"
VerticalAlignment="Center"
Source="ms-appx:///Assets/Icon.targetsize-32.png" />
<TextBlock
Margin="8,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.engineVersionDescriptor}" />
<TextBlock
Margin="8,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.CurrentProject.Metadata.Name, Mode=OneWay}" />
</StackPanel>
<!-- Toolbar -->
<Grid Grid.Row="1" Margin="4,4">
<controls:TabbedCommandBar>
<controls:TabbedCommandBar.MenuItems>
<controls:TabbedCommandBarItem Header="Home">
<AppBarButton Label="Undo" />
<AppBarButton Label="Redo" />
<AppBarButton Label="Paste" />
</controls:TabbedCommandBarItem>
<controls:TabbedCommandBarItem Header="Home">
<AppBarButton Label="Undo" />
<AppBarButton Label="Redo" />
<AppBarButton Label="Paste" />
</controls:TabbedCommandBarItem>
<controls:TabbedCommandBarItem Header="Home">
<AppBarButton Label="Undo" />
<AppBarButton Label="Redo" />
<AppBarButton Label="Paste" />
</controls:TabbedCommandBarItem>
</controls:TabbedCommandBar.MenuItems>
</controls:TabbedCommandBar>
</Grid>
<!-- Editor -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<internal:NavigationTabView
Grid.Column="0"
Width="350"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<internal:NavigationTabView.TabItems>
<ee:HierarchyPage Header="Hierarchy">
<ee:HierarchyPage.IconSource>
<FontIconSource Glyph="&#xE8A4;" />
</ee:HierarchyPage.IconSource>
</ee:HierarchyPage>
</internal:NavigationTabView.TabItems>
</internal:NavigationTabView>
<internal:NavigationTabView Grid.Column="1">
<internal:NavigationTabView.TabItems>
<ee:ScenePage Header="Scene">
<ee:ScenePage.IconSource>
<FontIconSource Glyph="&#xF159;" />
</ee:ScenePage.IconSource>
</ee:ScenePage>
</internal:NavigationTabView.TabItems>
</internal:NavigationTabView>
<internal:NavigationTabView
Grid.Column="2"
Width="350"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<internal:NavigationTabView.TabItems>
<ee:InspectorPage Header="Inspector">
<ee:InspectorPage.IconSource>
<FontIconSource Glyph="&#xEC7A;" />
</ee:InspectorPage.IconSource>
</ee:InspectorPage>
</internal:NavigationTabView.TabItems>
</internal:NavigationTabView>
</Grid>
<internal:NavigationTabView Grid.Row="1" Height="350">
<internal:NavigationTabView.TabItems>
<TabViewItem Header="Project">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xEC50;" />
</TabViewItem.IconSource>
<ee:ProjectPage />
</TabViewItem>
<TabViewItem Header="Console">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xE756;" />
</TabViewItem.IconSource>
<ee:ConsolePage />
</TabViewItem>
</internal:NavigationTabView.TabItems>
</internal:NavigationTabView>
</Grid>
<!-- Status Bar -->
<Grid
Grid.Row="3"
Height="25"
Background="{ThemeResource SolidBackgroundFillColorBaseAltBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<FontIcon
Margin="8,0,0,0"
FontSize="16"
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
Glyph="&#xE930;"
Visibility="Visible" />
<StackPanel Orientation="Horizontal" Visibility="Collapsed">
<FontIcon
Margin="8,0,0,0"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource SystemFillColorAttentionBrush}"
Glyph="&#xE946;" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="0" />
<FontIcon
Margin="8,0,0,0"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource SystemFillColorCautionBrush}"
Glyph="&#xE7BA;" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="0" />
<FontIcon
Margin="8,0,0,0"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Glyph="&#xE783;" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="0" />
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="4">
<InfoBar
x:Name="InfoBar"
Margin="16"
HorizontalAlignment="Right"
VerticalAlignment="Bottom">
<interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
</interactivity:Interaction.Behaviors>
</InfoBar>
<Grid
x:Name="ProgressBarContainer"
Background="{ThemeResource SmokeFillColorDefaultBrush}"
Visibility="Collapsed">
<Grid
Height="100"
Padding="36,24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{ThemeResource SolidBackgroundFillColorBaseBrush}"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
x:Name="ProgressMessage"
Grid.Row="0"
Margin="0,0,0,12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}"
Text="Loading..." />
<ProgressBar
x:Name="ProgressBar"
Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsIndeterminate="True" />
</Grid>
</Grid>
</Grid>
</Grid>
</winex:WindowEx>

View File

@@ -1,54 +0,0 @@
using Ghost.Data.Resources;
using Ghost.Editor.Core.Notifications;
using Ghost.Editor.Core.Progress;
using Ghost.Editor.ViewModels.Windows;
using Ghost.Engine.Resources;
using WinUIEx;
// 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.Windows;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
internal sealed partial class EngineEditorWindow : WindowEx
{
private readonly NotificationService _notificationService;
private readonly ProgressService _progressService;
public EngineEditorViewModel ViewModel
{
get;
}
public EngineEditorWindow()
{
ViewModel = App.GetService<EngineEditorViewModel>();
_notificationService = (NotificationService)App.GetService<INotificationService>();
_progressService = (ProgressService)App.GetService<IProgressService>();
AppWindow.SetIcon(AssetsPath.s_appIconPath);
Title = EngineData.ENGINE_NAME;
ExtendsContentIntoTitleBar = true;
InitializeComponent();
this.CenterOnScreen();
}
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
{
Bindings.Update();
_notificationService.SetReference(InfoBar, NotificationQueue);
_progressService.SetReference(ProgressBarContainer);
}
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
{
_notificationService.ClearReference();
_progressService.ClearReference();
}
}

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<winex:WindowEx
x:Class="Ghost.Editor.View.Windows.LandingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:Ghost.Editor.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx"
Activated="WindowEx_Activated"
Closed="WindowEx_Closed"
IsResizable="False"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock
Margin="24,0,0,0"
VerticalAlignment="Bottom"
Style="{StaticResource BodyTextBlockStyle}"
Text="Ghost Engine" />
</Grid>
<Grid Grid.Row="1" Padding="24,0,24,18">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<SelectorBar
Grid.Row="0"
HorizontalAlignment="Right"
SelectionChanged="SelectorBar_SelectionChanged">
<SelectorBarItem IsSelected="True" Text="Open">
<SelectorBarItem.Icon>
<FontIcon Glyph="&#xE838;" />
</SelectorBarItem.Icon>
</SelectorBarItem>
<SelectorBarItem Text="Create">
<SelectorBarItem.Icon>
<FontIcon Glyph="&#xE8F4;" />
</SelectorBarItem.Icon>
</SelectorBarItem>
</SelectorBar>
<Frame
x:Name="ContentFrame"
Grid.Row="1"
Padding="8"
CacheMode="BitmapCache"
CacheSize="10" />
</Grid>
<Grid Grid.Row="1" Padding="16">
<InfoBar
x:Name="InfoBar"
HorizontalAlignment="Right"
VerticalAlignment="Bottom">
<interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
</interactivity:Interaction.Behaviors>
</InfoBar>
</Grid>
</Grid>
</winex:WindowEx>

View File

@@ -1,59 +0,0 @@
using Ghost.Data.Resources;
using Ghost.Editor.Core.Notifications;
using Ghost.Editor.View.Pages.Landing;
using Ghost.Engine.Resources;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using WinUIEx;
namespace Ghost.Editor.View.Windows;
internal sealed partial class LandingWindow : WindowEx
{
private readonly NotificationService _notificationService;
private int _previousSelectedIndex;
public LandingWindow()
{
_notificationService = (NotificationService)App.GetService<INotificationService>();
AppWindow.SetIcon(AssetsPath.s_appIconPath);
Title = EngineData.ENGINE_NAME;
InitializeComponent();
this.SetWindowSize(1000, 750);
this.CenterOnScreen();
ExtendsContentIntoTitleBar = true;
}
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
{
_notificationService.SetReference(InfoBar, NotificationQueue);
}
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
{
_notificationService.ClearReference();
}
private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e)
{
var selectedItem = sender.SelectedItem;
var currentSelectedIndex = sender.Items.IndexOf(selectedItem);
var pageType = currentSelectedIndex switch
{
1 => typeof(CreateProjectPage),
_ => typeof(OpenProjectPage),
};
var slideNavigationTransitionEffect = currentSelectedIndex - _previousSelectedIndex > 0 ?
SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft;
ContentFrame.Navigate(pageType, null, new SlideNavigationTransitionInfo() { Effect = slideNavigationTransitionEffect });
_previousSelectedIndex = currentSelectedIndex;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,146 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Data.Services;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Models;
using System.Collections.ObjectModel;
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 assetsRootItem = new ExplorerItem("Assets", Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER), true);
LoadSubFolderRecursive(ref assetsRootItem);
SubDirectories.Add(assetsRootItem);
}
private static void LoadSubFolderRecursive(ref ExplorerItem parentItem)
{
foreach (var directory in Directory.EnumerateDirectories(parentItem.FullName))
{
var item = new ExplorerItem(Path.GetFileName(directory), directory, true);
LoadSubFolderRecursive(ref item);
parentItem.Children ??= new();
parentItem.Children.Add(item);
}
}
public static Task<ExplorerItem?> FindNodeIterative(ExplorerItem root, Func<ExplorerItem, bool> predicate)
{
var stack = new Stack<ExplorerItem>();
stack.Push(root);
return Task.Run(() =>
{
while (stack.Count > 0)
{
var node = stack.Pop();
if (predicate(node))
{
return node;
}
if (node.Children == null || node.Children.Count == 0)
{
continue;
}
for (var i = node.Children.Count - 1; i >= 0; i--)
{
stack.Push(node.Children[i]);
}
}
return null;
});
}
private void NavigateToDirectory(string? path)
{
App.Window?.DispatcherQueue.TryEnqueue(async () =>
{
DirectoryAssets.Clear();
if (!Directory.Exists(path))
{
return;
}
foreach (var directory in Directory.EnumerateDirectories(path))
{
var directoryItem = new ExplorerItem(Path.GetFileName(directory), directory, true);
DirectoryAssets.Add(directoryItem);
}
foreach (var file in Directory.EnumerateFiles(path))
{
var fileItem = new ExplorerItem(Path.GetFileName(file), file, false);
DirectoryAssets.Add(fileItem);
}
SelectedDirectory = await FindNodeIterative(SubDirectories[0], x => x.FullName == path);
});
}
public void OpenSelected()
{
if (SelectedAsset == null)
{
return;
}
if (SelectedAsset.IsDirectory)
{
NavigateToDirectory(SelectedAsset.FullName);
}
else
{
AssetDatabase.OpenAsset(SelectedAsset.FullName);
}
}
partial void OnSelectedDirectoryChanged(ExplorerItem? value)
{
if (value == null)
{
return;
}
DirectoryAssets.Clear();
NavigateToDirectory(value.FullName);
}
}

View File

@@ -1,91 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ghost.Data.Models;
using Ghost.Data.Services;
using Ghost.Editor.Core.AppState;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Ghost.Editor.Utilities;
using Ghost.Engine.Resources;
using System.Collections.ObjectModel;
namespace Ghost.Editor.ViewModels.Pages.Landing;
internal partial class CreateProjectViewModel(INotificationService notificationService, ProjectService projectService, AppStateMachine stateService) : ObservableObject, INavigationAware
{
public ObservableCollection<TemplateData> templates = new();
[ObservableProperty]
public partial TemplateData? SelectedTemplate
{
get;
set;
}
[ObservableProperty]
public partial string? ProjectName
{
get;
set;
}
[ObservableProperty]
public partial string? ProjectLocation
{
get;
set;
}
public async void OnNavigatedTo(object? parameter)
{
templates.Clear();
await foreach (var (path, info) in ProjectService.GetProjectTemplatesAsync())
{
templates.Add(new(path, info));
}
SelectedTemplate = templates.FirstOrDefault();
}
public void OnNavigatedFrom()
{
}
[RelayCommand]
private async Task SelectionProjectLocation()
{
var folder = await SystemUtilities.OpenFolderPickerAsync();
if (folder != null)
{
ProjectLocation = folder.Path;
}
}
[RelayCommand]
private async Task CreateProject()
{
if (string.IsNullOrWhiteSpace(ProjectName)
|| !Directory.Exists(ProjectLocation)
|| !SelectedTemplate.HasValue)
{
notificationService.ShowNotification("Incorrect project info", MessageType.Error);
return;
}
var result = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, EngineData.EngineVersion, SelectedTemplate.Value.directory);
if (result.IsFailure)
{
notificationService.ShowNotification(result.Message, MessageType.Error);
return;
}
try
{
await stateService.TransitionToAsync(StateKey.EngineEditor, result.Value);
}
catch (Exception e)
{
notificationService.ShowNotification($"Failed to load project: {e.Message}", MessageType.Error);
}
}
}

View File

@@ -1,106 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Data.Models;
using Ghost.Data.Services;
using Ghost.Editor.Core.AppState;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Microsoft.UI.Xaml;
using System.Collections.ObjectModel;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
namespace Ghost.Editor.ViewModels.Pages.Landing;
internal partial class OpenProjectViewModel(ProjectService projectService, INotificationService _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)
{
await foreach (var projectInfo in projectService.GetAllProjectAsync())
{
var metadata = await ProjectService.LoadMetadataAsync(projectInfo.MetadataPath);
if (metadata == null)
{
continue;
}
projects.Add(new(projectInfo.MetadataPath, metadata));
}
UpdateEmptyPlaceHolderVisibility();
DragVisibility = Visibility.Collapsed;
}
public void OnNavigatedFrom()
{
projects.Clear();
}
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.IsSuccess)
{
projects.Add(result.Value);
goto CloseDropPanel;
}
else
{
errorMessage = result.Message;
}
}
}
else
{
errorMessage = "Unsupported data format. Please drop a folder containing a project.";
}
_notificationService.ShowNotification(errorMessage, MessageType.Error);
CloseDropPanel:
DragVisibility = Visibility.Collapsed;
UpdateEmptyPlaceHolderVisibility();
}
public async Task OpenProjectAsync(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}", MessageType.Error);
}
}
}

Some files were not shown because too many files have changed in this diff Show More