Files
GhostEngine/Ghost.Editor.Core/SceneGraph/EntityNode.cs
Misaki fdf831630b feat: implement complete scene graph system with hierarchical editor support
- Add SceneNode and EntityNode classes for editor-only metadata storage
- Implement SceneGraph view-model with O(1) entity lookup via internal caching
- Create IdRemapTable for file-local to global entity ID remapping on load
- Implement SceneSerializationContext for load/save operation tracking
- Add JSON-serializable SceneAssetData, EntityData, and ComponentData models
- Implement SceneSerializer for save/load with validation and reference remapping
- Add comprehensive documentation: README.md, IMPLEMENTATION_GUIDE.md, SYSTEM_SUMMARY.md
- Update Ghost.Editor.Core.csproj to reference Ghost.Entities assembly
- Support parent-child relationships via Hierarchy component
- Enforce no cross-scene entity references
- Keep runtime minimal: only SceneID, Hierarchy, LocalToWorld components
- All editor metadata (names, UI state) stored in editor-only SceneNode/EntityNode classes

This implements the architecture from SceneGraph Plan.md with clean separation of concerns,
minimal runtime footprint, and AOT compatibility.
2026-01-25 21:42:03 +09:00

106 lines
2.8 KiB
C#

using Ghost.Entities;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
/// <summary>
/// Represents an Entity node in the editor hierarchy.
/// Contains editor-only metadata like name and selection state.
/// References the actual entity data in the ECS world via EntityId.
/// </summary>
public class EntityNode
{
public string Name { get; set; }
public Entity EntityId { get; private set; }
/// <summary>
/// File-local ID within the scene (used for serialization).
/// Only set when loaded from a scene file; may be -1 if not yet assigned.
/// </summary>
public int FileLocalId { get; set; } = -1;
/// <summary>
/// Child entity nodes (parent-child relationships in hierarchy).
/// </summary>
public ObservableCollection<EntityNode> Children { get; }
/// <summary>
/// Reference to parent entity node, if any.
/// </summary>
public EntityNode? ParentNode { get; set; }
/// <summary>
/// Whether this node is expanded in the editor UI.
/// </summary>
public bool IsExpanded { get; set; }
/// <summary>
/// Whether this node is selected in the editor UI.
/// </summary>
public bool IsSelected { get; set; }
public EntityNode(string name, Entity entityId)
{
Name = name;
EntityId = entityId;
Children = new ObservableCollection<EntityNode>();
IsExpanded = false;
IsSelected = false;
}
/// <summary>
/// Finds a child entity node recursively by its global entity ID.
/// </summary>
public EntityNode? FindRecursive(Entity entityId)
{
foreach (var child in Children)
{
if (child.EntityId == entityId)
return child;
var found = child.FindRecursive(entityId);
if (found != null)
return found;
}
return null;
}
/// <summary>
/// Gets the depth of this node in the hierarchy.
/// Root nodes have depth 0.
/// </summary>
public int GetDepth()
{
int depth = 0;
var current = ParentNode;
while (current != null)
{
depth++;
current = current.ParentNode;
}
return depth;
}
/// <summary>
/// Gets all descendant nodes in breadth-first order.
/// </summary>
public IEnumerable<EntityNode> GetAllDescendants()
{
var queue = new Queue<EntityNode>();
queue.Enqueue(this);
while (queue.Count > 0)
{
var node = queue.Dequeue();
foreach (var child in node.Children)
{
yield return child;
queue.Enqueue(child);
}
}
}
public override string ToString() => $"Entity: {Name} (ID: {EntityId})";
}