diff --git a/GlitchyEngine/src/Editor/Editor.bf b/GlitchyEngine/src/Editor/Editor.bf new file mode 100644 index 0000000..02d4d3f --- /dev/null +++ b/GlitchyEngine/src/Editor/Editor.bf @@ -0,0 +1,105 @@ +using GlitchyEngine.World; +using ImGui; +using System; +using System.Collections; +using GlitchyEngine.Collections; + +namespace GlitchyEngine.Editor +{ + class Editor + { + private EcsWorld _world; + + private EntityHierarchyWindow _entityHierarchyWindow = new .(this) ~ delete _; + + private List _selectedEntities = new .() ~ delete _; + + public EcsWorld World => _world; + + public List SelectedEntities => _selectedEntities; + + /// Creates a new editor for the given world + public this(EcsWorld world) + { + _world = world; + } + + public void Update() + { + _entityHierarchyWindow.Show(); + } + + /// Creates a new entity with a transform component. + internal Entity CreateEntityWithTransform() + { + var entity = _world.NewEntity(); + + var transformComponent = ref *_world.AssignComponent(entity); + transformComponent = TransformComponent(); + + var nameComponent = ref *_world.AssignComponent(entity); + nameComponent.SetName("Entity"); + + return entity; + } + + + /// Returns whether or not all selected entities have the same parent. + internal bool AllSelectionsOnSameLevel() + { + Entity? parent = .InvalidEntity; + + for(var selectedEntity in _selectedEntities) + { + var parentComponent = _world.GetComponent(selectedEntity); + + if(parent == .InvalidEntity) + { + parent = parentComponent?.Entity; + } + else if(parentComponent?.Entity != parent) + { + return false; + } + } + + return true; + } + + /// Finds all children of the given entity and stores their IDs in the given list. + internal void FindChildren(Entity entity, List entities) + { + for(var (child, childParent) in _world.Enumerate()) + { + if(childParent.Entity == entity) + { + if(!entities.Contains(child)) + entities.Add(child); + + FindChildren(child, entities); + } + } + } + + /// Deletes all selected entities and their children. + internal void DeleteSelectedEntities() + { + List entities = scope .(); + + for(var entity in _selectedEntities) + { + entities.Add(entity); + + FindChildren(entity, entities); + } + + for(var entity in entities) + { + _world.RemoveEntity(entity); + } + + _selectedEntities.Clear(); + } + + } +} diff --git a/GlitchyEngine/src/Editor/EntityHierarchyWindow.bf b/GlitchyEngine/src/Editor/EntityHierarchyWindow.bf new file mode 100644 index 0000000..a957883 --- /dev/null +++ b/GlitchyEngine/src/Editor/EntityHierarchyWindow.bf @@ -0,0 +1,334 @@ +using GlitchyEngine.Collections; +using GlitchyEngine.World; +using ImGui; +using System; +using System.Collections; + +namespace GlitchyEngine.Editor +{ + using internal GlitchyEngine.Editor; + + /// A window for viewing and editing the scene hierarchy + class EntityHierarchyWindow + { + private Editor _editor; + + /// Buffer for the entity search string. + private char8[] _entitySearchChars = new char8[64] ~ delete _; + + private bool _show = true; + + public Editor Editor => _editor; + + public bool Show + { + get => _show; + set => _show = value; + } + + public this(Editor editor) + { + _editor = editor; + } + + public void Show() + { + if(!ImGui.Begin("Entity Hierarchy", null, .MenuBar)) + { + ImGui.End(); + return; + } + + ShowEntityHierarchyMenuBar(); + + ShowEntityHierarchy(); + + ImGui.End(); + } + + private void ShowEntityHierarchyMenuBar() + { + if(ImGui.BeginMenuBar()) + { + if(ImGui.BeginMenu("Create")) + { + if(ImGui.MenuItem("Empty Entity")) + { + _editor.CreateEntityWithTransform(); + } + + if(ImGui.IsItemHovered()) + ImGui.SetTooltip("Create a new Entity with a transform component."); + + if(ImGui.MenuItem("Empty Child", null, false, !_editor.SelectedEntities.IsEmpty)) + { + var newEntity = _editor.CreateEntityWithTransform(); + var parent = _editor.World.AssignComponent(newEntity); + // Last entity in list is the entity that has been selected last. + parent.Entity = _editor.SelectedEntities.Back; + } + + if(ImGui.IsItemHovered()) + ImGui.SetTooltip("Create a new Entity that is a child of the currently selected entity."); + + if(ImGui.MenuItem("Empty Parent", null, false, !_editor.SelectedEntities.IsEmpty && _editor.AllSelectionsOnSameLevel())) + { + var commonParent = _editor.World.GetComponent(_editor.SelectedEntities.Front); + + var newEntity = _editor.CreateEntityWithTransform(); + + if(commonParent != null) + { + // parent of selected entities is parent of the new entity. + // (which is why this doesn't work if the entities don't have the same parent) + var newEntityParent = _editor.World.AssignComponent(newEntity); + newEntityParent.Entity = commonParent.Entity; + } + + // new entity is parent of all selected entities. + for(var selectedEntity in _editor.SelectedEntities) + { + var selectedEntityParent = _editor.World.AssignComponent(selectedEntity); + selectedEntityParent.Entity = newEntity; + } + } + + if(ImGui.IsItemHovered()) + ImGui.SetTooltip("Create a new Entity that is the parent of the currently selected entities."); + + ImGui.EndMenu(); + } + + if(ImGui.IsItemHovered()) + ImGui.SetTooltip("Create a new Entity."); + + if(ImGui.MenuItem("Delete", null, false, !_editor.SelectedEntities.IsEmpty) || Input.IsKeyPressed(.Delete)) + { + _editor.DeleteSelectedEntities(); + } + + if(ImGui.IsItemHovered()) + ImGui.SetTooltip("Deletes the selected Entities and their children."); + + ImGui.Text("Search:"); + + ImGui.InputText(String.Empty, _entitySearchChars.Ptr, (.)_entitySearchChars.Count); + + ImGui.EndMenuBar(); + } + } + + private void ImGuiPrintEntityTree(TreeNode tree) + { + String name = null; + + var nameComponent = _editor.World.GetComponent(tree.Value); + + if(nameComponent != null) + { + name = nameComponent.DebugName; + } + else + { + name = scope:: $"Entity {((uint64)tree.Value.[Friend]Index)}"; + } + + ImGui.TreeNodeFlags flags = .OpenOnArrow; + + if(tree.Children.Count == 0) + flags |= .Leaf; + + bool inSelectedList = _editor.SelectedEntities.Contains(tree.Value); + + if(inSelectedList) + flags |= .Selected; + + bool isOpen = ImGui.TreeNodeEx(name, flags); + + if(ImGui.BeginDragDropSource()) + { + ImGui.SetDragDropPayload("DND_Entity", &tree.Value, sizeof(Entity)); + + ImGui.Text(name); + + ImGui.EndDragDropSource(); + } + + if(ImGui.BeginDragDropTarget()) + { + ImGui.Payload* payload = &ImGui.AcceptDragDropPayload("DND_Entity"); + + if(payload != null) + { + Log.ClientLogger.AssertDebug(payload.DataSize == sizeof(Entity)); + + Entity movedEntity = *(Entity*)payload.Data; + + bool dropLegal = true; + + Entity walker = tree.Value; + + // make sure the dropped entity is not a parent of the entity we dropped it on. + while(true) + { + var walkerParent = _editor.World.GetComponent(walker); + + if(walkerParent == null) + { + dropLegal = true; + break; + } + else if(walkerParent.Entity == movedEntity) + { + dropLegal = false; + break; + } + + walker = walkerParent.Entity; + } + + if(dropLegal) + { + var movedEntityParent = _editor.World.AssignComponent(movedEntity); + movedEntityParent.Entity = tree.Value; + } + } + + ImGui.EndDragDropTarget(); + } + + bool clicked = ImGui.IsItemClicked(); + + if(isOpen) + { + for(var child in tree.Children) + { + ImGuiPrintEntityTree(child); + } + + ImGui.TreePop(); + } + + if(clicked) + { + if(inSelectedList) + { + _editor.SelectedEntities.Remove(tree.Value); + } + else + { + if(!ImGui.GetIO().KeyCtrl) + { + _editor.SelectedEntities.Clear(); + } + + _editor.SelectedEntities.Add(tree.Value); + } + + inSelectedList = !inSelectedList; + } + } + + private void ShowEntityHierarchy() + { + StringView searchString = StringView(_entitySearchChars.Ptr); + + if(searchString.Length == 0) + { + // Show entity hierarchy as tree + + TreeNode root = scope .(.InvalidEntity); + + TreeNode AddEntity(Entity entity) + { + var parent = _editor.World.GetComponent(entity); + + if(parent == null) + { + return root.AddChild(entity); + } + else + { + var parentNode = root.FindNode(parent.Entity); + + if(parentNode == null) + parentNode = AddEntity(parent.Entity); + + return parentNode.AddChild(entity); + } + } + + for(var entity in _editor.World.Enumerate()) + { + AddEntity(entity); + } + + if(ImGui.TreeNodeEx("Scene", .DefaultOpen)) + { + if(ImGui.BeginDragDropTarget()) + { + ImGui.Payload* payload = &ImGui.AcceptDragDropPayload("DND_Entity"); + + if(payload != null) + { + Log.ClientLogger.AssertDebug(payload.DataSize == sizeof(Entity)); + + Entity movedEntity = *(Entity*)payload.Data; + + _editor.World.RemoveComponent(movedEntity); + + // Also mark transform as dirty + var transformComponent = _editor.World.GetComponent(movedEntity); + transformComponent?.IsDirty = true; + } + + ImGui.EndDragDropTarget(); + } + + for(var child in root.Children) + { + ImGuiPrintEntityTree(child); + } + + ImGui.TreePop(); + } + } + // Otherwise + else + { + // Show search results as flat list + + List searchTokens = new:ScopedAlloc! .(searchString.Split(' ', .RemoveEmptyEntries)); + + worldEnumeration: + for(var entity in _editor.World.Enumerate()) + { + String name = null; + + var nameComponent = _editor.World.GetComponent(entity); + + if(nameComponent != null) + { + name = nameComponent.DebugName; + } + else + { + name = scope:worldEnumeration $"Entity {entity.[Friend]Index}"; + } + + StringView nameView = StringView(name); + + for(var token in searchTokens) + { + if(nameView.IndexOf(token, true) == -1) + { + continue worldEnumeration; + } + } + + ImGuiPrintEntityTree(scope .(entity)); + } + } + } + } +} diff --git a/Sandbox/src/SandboxApp.bf b/Sandbox/src/SandboxApp.bf index 48dbcd1..62270f8 100644 --- a/Sandbox/src/SandboxApp.bf +++ b/Sandbox/src/SandboxApp.bf @@ -11,6 +11,7 @@ using GlitchyEngine.World; using System.Collections; using GlitchyEngine.Content; using GlitchyEngine.Renderer.Animation; +using GlitchyEngine.Editor; namespace Sandbox { @@ -73,6 +74,8 @@ namespace Sandbox EcsWorld _world = new EcsWorld() ~ delete _; + Editor _editor = new Editor(_world) ~ delete _; + // OrthographicCameraController _cameraController ~ delete _; PerspectiveCameraController _cameraController ~ delete _; @@ -541,6 +544,8 @@ namespace Sandbox private bool OnImGuiRender(ImGuiRenderEvent e) { + _editor.Update(); + ImGui.Begin("Animation"); /* if(AnimationPlayer != null)