diff --git a/CMakeLists.txt b/CMakeLists.txt index 67c6389..41eb905 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(Prune src/prune/core/input.cpp src/prune/editor/editor_command.cpp + src/prune/editor/editor_actions.cpp src/prune/scene/game_object.cpp src/prune/scene/game_object_manager.cpp diff --git a/NOTES.md b/NOTES.md index 6b3c494..f37d94e 100644 --- a/NOTES.md +++ b/NOTES.md @@ -163,10 +163,8 @@ Initial scope: ## Medium term, in no particular order - Rotate tool -- Delete action and command - Background image support - Simplified asset management -- Copy / Paste command - Text rendering - Behaviour toggles for authored objects - Polished sample scenes diff --git a/src/prune/editor/editor_actions.cpp b/src/prune/editor/editor_actions.cpp new file mode 100644 index 0000000..374f250 --- /dev/null +++ b/src/prune/editor/editor_actions.cpp @@ -0,0 +1,115 @@ +#include "prune/editor/editor_actions.hpp" + +#include +#include +#include + +#include "prune/editor/editor_command.hpp" + +namespace prune { + + namespace { + + [[nodiscard]] float clone_offset(const GridOptions& grid_options) noexcept + { + return static_cast( + grid_options.snap_to_grid + ? std::max(1, grid_options.grid_size) + : std::max(1, grid_options.nudge_step) + ); + } + + } + + SelectedObjectActionAvailability selected_object_action_availability(Scene& scene) + { + GameObjectManager& objects = scene.get_object_manager(); + + SelectedObjectActionAvailability availability{}; + if (GameObject* active = objects.selected_object()) { + availability.can_clone_active = + scene.object_is_editable(*active) && + active->editor.cloneable; + } + + for (const GameObjectId id : objects.selected_ids()) { + const GameObject* object = objects.get_by_id(id); + if (!object || !scene.object_is_editable(*object) || !object->editor.deletable) { + continue; + } + + ++availability.deletable_count; + } + + availability.can_delete_selection = availability.deletable_count > 0; + return availability; + } + + bool clone_active_selected_object(Scene& scene, const GridOptions& grid_options) + { + GameObjectManager& objects = scene.get_object_manager(); + const GameObject* selected = objects.selected_object(); + + if (!selected || !scene.object_is_editable(*selected) || !selected->editor.cloneable) { + return false; + } + + GameObject clone = *selected; + clone.identity.name = objects.make_unique_name(clone.identity.name + " Copy", k_invalid_game_object_id); + + const float offset = clone_offset(grid_options); + clone.transform.x += offset; + clone.transform.y += offset; + + const GameObjectId clone_id = objects.create_object(clone); + GameObject* created = objects.get_by_id(clone_id); + if (!created) { + return false; + } + + objects.select(clone_id); + scene.record_editor_command(make_create_object_command(*created, created->identity.name)); + return true; + } + + bool delete_selected_objects(Scene& scene) + { + GameObjectManager& objects = scene.get_object_manager(); + std::vector deleted_objects; + deleted_objects.reserve(objects.selected_count()); + + for (const GameObjectId id : objects.selected_ids()) { + const GameObject* object = objects.get_by_id(id); + if (!object || !scene.object_is_editable(*object) || !object->editor.deletable) { + continue; + } + + deleted_objects.push_back(*object); + } + + if (deleted_objects.empty()) { + return false; + } + + for (const GameObject& object : deleted_objects) { + objects.remove_object(object.identity.id); + } + + if (deleted_objects.size() > 1) { + objects.clear_selection(); + } + + if (deleted_objects.size() == 1) { + scene.record_editor_command(make_delete_object_command(deleted_objects.front(), deleted_objects.front().identity.name)); + } + else { + scene.record_editor_command(make_multi_delete_object_command( + deleted_objects, + std::to_string(deleted_objects.size()) + " objects" + )); + } + + return true; + } + +} diff --git a/src/prune/editor/editor_actions.hpp b/src/prune/editor/editor_actions.hpp new file mode 100644 index 0000000..3d0758a --- /dev/null +++ b/src/prune/editor/editor_actions.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "prune/scene/scene.hpp" + +namespace prune { + + struct SelectedObjectActionAvailability { + bool can_clone_active = false; + bool can_delete_selection = false; + std::size_t deletable_count = 0; + }; + + [[nodiscard]] SelectedObjectActionAvailability selected_object_action_availability(Scene& scene); + bool clone_active_selected_object(Scene& scene, const GridOptions& grid_options); + bool delete_selected_objects(Scene& scene); + +} diff --git a/src/prune/scene/scene_interaction.cpp b/src/prune/scene/scene_interaction.cpp index 1fdd48d..33fe698 100644 --- a/src/prune/scene/scene_interaction.cpp +++ b/src/prune/scene/scene_interaction.cpp @@ -6,6 +6,7 @@ #include +#include "prune/editor/editor_actions.hpp" #include "prune/editor/tools/transform_gizmo.hpp" #include "prune/scene/scene.hpp" #include "prune/scene/scene_interaction.hpp" @@ -407,67 +408,12 @@ namespace prune { input.is_key_down(SDL_SCANCODE_RCTRL); if (input.was_key_pressed(SDL_SCANCODE_DELETE) || input.was_key_pressed(SDL_SCANCODE_BACKSPACE)) { - std::vector deleted_objects; - deleted_objects.reserve(state.objects.selected_count()); - - for (const GameObjectId id : state.objects.selected_ids()) { - const GameObject* object = state.objects.get_by_id(id); - if (!object || !scene.object_is_editable(*object) || !object->editor.deletable) { - continue; - } - - deleted_objects.push_back(*object); - } - - if (deleted_objects.empty()) { - return; - } - - for (const GameObject& object : deleted_objects) { - state.objects.remove_object(object.identity.id); - } - - if (deleted_objects.size() > 1) { - state.objects.clear_selection(); - } - - if (deleted_objects.size() == 1) { - scene.record_editor_command(make_delete_object_command(deleted_objects.front(), deleted_objects.front().identity.name)); - } - else { - scene.record_editor_command(make_multi_delete_object_command( - deleted_objects, - std::to_string(deleted_objects.size()) + " objects" - )); - } + delete_selected_objects(scene); return; } - GameObject* selected = state.objects.selected_object(); - if (!selected || !scene.object_is_editable(*selected)) { - return; - } - - if (!selected->editor.cloneable || !ctrl_down || !input.was_key_pressed(SDL_SCANCODE_D)) { - return; - } - - GameObject duplicate = *selected; - duplicate.identity.name = state.objects.make_unique_name(duplicate.identity.name + " Copy", k_invalid_game_object_id); - - const float offset = static_cast( - grid_options.snap_to_grid - ? std::max(1, grid_options.grid_size) - : std::max(1, grid_options.nudge_step) - ); - - duplicate.transform.x += offset; - duplicate.transform.y += offset; - - const GameObjectId duplicate_id = state.objects.create_object(duplicate); - if (GameObject* created = state.objects.get_by_id(duplicate_id)) { - state.objects.select(duplicate_id); - scene.record_editor_command(make_create_object_command(*created, "Duplicate object")); + if (ctrl_down && input.was_key_pressed(SDL_SCANCODE_D)) { + clone_active_selected_object(scene, grid_options); } } diff --git a/src/prune/tooling/controls.cpp b/src/prune/tooling/controls.cpp index d33d2c1..20ba423 100644 --- a/src/prune/tooling/controls.cpp +++ b/src/prune/tooling/controls.cpp @@ -22,15 +22,23 @@ namespace prune { tooling::imgui::layout::spacing(2); + tooling::imgui::layout::text_wrapped("Artillery"); + tooling::imgui::layout::text_wrapped("A/D keys control the angle left and right"); + tooling::imgui::layout::text_wrapped("W/S keys control the power up and down"); + tooling::imgui::layout::text_wrapped("Space fires a projectile"); + + tooling::imgui::layout::spacing(2); + tooling::imgui::layout::text_wrapped("Objects"); tooling::imgui::layout::text_wrapped("CTRL+Arrow keys move the selected object or selected objects"); tooling::imgui::layout::text_wrapped("Hold Shift for larger movements"); + tooling::imgui::layout::text_wrapped("The Selected tool palette actions clone or delete the selected editable object"); tooling::imgui::layout::text_wrapped("Delete or Backspace deletes selected deletable objects"); - tooling::imgui::layout::text_wrapped("CTRL+D duplicates the active selected object"); + tooling::imgui::layout::text_wrapped("CTRL+D clones the active selected object"); tooling::imgui::layout::text_wrapped("Left click selects one object"); tooling::imgui::layout::text_wrapped("Shift+Left click toggles objects in the selection set"); tooling::imgui::layout::text_wrapped("Empty viewport click clears the selection set"); - tooling::imgui::layout::text_wrapped("Use the Select/Move tool buttons in the top right of the viewport to change tool state"); + tooling::imgui::layout::text_wrapped("Use the Select/Move/Scale tool buttons in the top right of the viewport to change tool mode"); tooling::imgui::layout::text_wrapped("Select tool: left click selects, Shift+Left click toggles selection, and selection handles move selected objects"); tooling::imgui::layout::text_wrapped("Move tool: left drag on an object handle to move the object"); diff --git a/src/prune/tooling/inspector.cpp b/src/prune/tooling/inspector.cpp index 13ddacf..8e41f84 100644 --- a/src/prune/tooling/inspector.cpp +++ b/src/prune/tooling/inspector.cpp @@ -1,4 +1,3 @@ -#include #include #include "imgui.h" @@ -11,22 +10,19 @@ namespace prune { void Inspector::draw( Scene& scene, - GridOptions& grid_options, const Camera& camera ) { GameObjectManager& objects = scene.get_object_manager(); - draw_selected(scene, grid_options); + draw_selected(scene); draw_scene_meaning(scene); draw_properties(scene, objects); draw_computed(objects, camera); draw_flags(scene, objects); } - void Inspector::draw_selected( - Scene& scene, - GridOptions& grid_options - ) { + void Inspector::draw_selected(Scene& scene) + { GameObjectManager& objects = scene.get_object_manager(); GameObject* selected = objects.selected_object(); @@ -36,8 +32,6 @@ namespace prune { const bool can_edit = scene.object_is_editable(*selected); const bool can_rename = selected->editor.renameable && can_edit; - const bool can_delete = selected->editor.deletable && can_edit; - const bool can_clone = selected->editor.cloneable && can_edit; if (tooling::imgui::layout::collapsing_header("Selected")) { if (tooling::imgui::property_table::begin("Selected")) { @@ -86,53 +80,6 @@ namespace prune { } } - if (can_delete || can_clone) { - - tooling::imgui::layout::separator(); - - tooling::imgui::property_table::begin_row("Actions"); - - if (can_delete && tooling::imgui::property_table::button_raw("Delete")) { - const GameObject deleted = *selected; - const GameObjectId id_to_remove = selected->identity.id; - if (objects.remove_object(id_to_remove)) { - scene.record_editor_command(make_delete_object_command(deleted, deleted.identity.name)); - } - - tooling::imgui::property_table::end(); - return; - } - - if (can_delete && can_clone) { - ImGui::SameLine(); - } - - if (can_clone && tooling::imgui::property_table::button_raw("Clone")) { - const std::string source_name = selected->identity.name; - - GameObject clone = *selected; - - const float step = grid_options.snap_to_grid - ? static_cast(std::max(1, grid_options.grid_size)) - : static_cast(k_default_object_size); - - clone.transform.x += step; - clone.transform.y += step; - - const GameObjectId clone_id = objects.create_object(clone); - - if (GameObject* created = objects.get_by_id(clone_id)) { - created->identity.name = objects.make_unique_name(source_name, clone_id); - scene.record_editor_command(make_create_object_command(*created, created->identity.name)); - } - - objects.select(clone_id); - - tooling::imgui::property_table::end(); - return; - } - } - tooling::imgui::property_table::end(); } } diff --git a/src/prune/tooling/inspector.hpp b/src/prune/tooling/inspector.hpp index f09b9ad..023605e 100644 --- a/src/prune/tooling/inspector.hpp +++ b/src/prune/tooling/inspector.hpp @@ -15,12 +15,11 @@ namespace prune { public: void draw( Scene& scene, - GridOptions& grid_options, const Camera& camera ); private: - void draw_selected(Scene& scene, GridOptions& grid_options); + void draw_selected(Scene& scene); void draw_scene_meaning(Scene& scene); void draw_properties(Scene& scene, GameObjectManager& objects); void draw_computed(GameObjectManager& objects, const Camera& camera); diff --git a/src/prune/tooling/ui.cpp b/src/prune/tooling/ui.cpp index 4e5df60..bfaa6c0 100644 --- a/src/prune/tooling/ui.cpp +++ b/src/prune/tooling/ui.cpp @@ -7,6 +7,7 @@ #include "imgui.h" +#include "prune/editor/editor_actions.hpp" #include "prune/editor/editor_tool.hpp" #include "prune/scene/scene.hpp" #include "prune/tooling/editor_layout.hpp" @@ -23,6 +24,11 @@ namespace prune { struct ToolPaletteLayout { ImVec2 size{}; + float selection_action_button_width = 0.0f; + std::size_t selection_action_columns = 0; + bool has_selection_actions = false; + bool show_clone_action = false; + bool show_delete_action = false; float scene_creation_button_width = 0.0f; std::size_t scene_creation_action_columns = 0; bool has_scene_creation_actions = false; @@ -39,7 +45,7 @@ namespace prune { ); } - [[nodiscard]] ToolPaletteLayout editor_tool_palette_layout(const Scene& scene) + [[nodiscard]] ToolPaletteLayout editor_tool_palette_layout(Scene& scene, const GridOptions* grid_options) { const ImGuiStyle& style = ImGui::GetStyle(); const std::span actions = scene.scene_creation_actions(); @@ -54,6 +60,32 @@ namespace prune { style.ItemSpacing.y + ImGui::GetFrameHeight(); + const SelectedObjectActionAvailability selection_actions = selected_object_action_availability(scene); + layout.show_clone_action = selection_actions.can_clone_active && grid_options != nullptr; + layout.show_delete_action = selection_actions.can_delete_selection; + layout.has_selection_actions = layout.show_clone_action || layout.show_delete_action; + + if (layout.has_selection_actions) { + layout.selection_action_columns = + (layout.show_clone_action && layout.show_delete_action) ? std::size_t{ 2 } : std::size_t{ 1 }; + layout.selection_action_button_width = std::max( + tool_button_width("Clone"), + tool_button_width("Delete") + ); + + const float selection_actions_width = + (layout.selection_action_button_width * static_cast(layout.selection_action_columns)) + + (style.ItemSpacing.x * static_cast(layout.selection_action_columns - 1)); + + content_width = std::max(content_width, selection_actions_width); + content_height += + (style.ItemSpacing.y * 2.0f) + + 1.0f + + ImGui::GetTextLineHeight() + + style.ItemSpacing.y + + ImGui::GetFrameHeight(); + } + if (!actions.empty()) { layout.has_scene_creation_actions = true; layout.scene_creation_action_columns = std::min( @@ -137,6 +169,7 @@ namespace prune { void draw_editor_tool_palette( Scene& scene, const ToolPaletteLayout& layout, + const GridOptions* grid_options, const ImVec2& palette_min, const ImVec2& palette_max ) @@ -163,16 +196,40 @@ namespace prune { )); ImGui::BeginGroup(); - ImGui::TextUnformatted("Tools"); + ImGui::TextUnformatted("Mode"); draw_editor_tool_button(scene, EditorTool::Select); ImGui::SameLine(); draw_editor_tool_button(scene, EditorTool::Move); ImGui::SameLine(); draw_editor_tool_button(scene, EditorTool::Scale); + if (layout.has_selection_actions) { + ImGui::Separator(); + ImGui::TextUnformatted("Selected"); + + bool rendered_button = false; + + if (layout.show_clone_action && grid_options != nullptr) { + if (ImGui::Button("Clone", ImVec2(layout.selection_action_button_width, 0.0f))) { + clone_active_selected_object(scene, *grid_options); + } + rendered_button = true; + } + + if (layout.show_delete_action) { + if (rendered_button) { + ImGui::SameLine(); + } + + if (ImGui::Button("Delete", ImVec2(layout.selection_action_button_width, 0.0f))) { + delete_selected_objects(scene); + } + } + } + if (layout.has_scene_creation_actions) { ImGui::Separator(); - ImGui::TextUnformatted("Scene Creation"); + ImGui::TextUnformatted("Create"); const std::span actions = scene.scene_creation_actions(); for (std::size_t index = 0; index < actions.size(); ++index) { @@ -325,7 +382,6 @@ namespace prune { if (context.available()) { m_inspector.draw( scene, - *context.grid_options, context.camera->active() ); } @@ -468,7 +524,9 @@ namespace prune { viewport_pos.x + viewport_size.x, viewport_pos.y + viewport_size.y }; - const ToolPaletteLayout tool_palette_layout = editor_tool_palette_layout(scene); + const WorldSceneContext context = scene.world_scene_context(); + const GridOptions* grid_options = context.available() ? context.grid_options : nullptr; + const ToolPaletteLayout tool_palette_layout = editor_tool_palette_layout(scene, grid_options); const ImVec2 palette_min{ viewport_max.x - k_tool_palette_margin - tool_palette_layout.size.x, viewport_pos.y + k_tool_palette_margin @@ -503,7 +561,7 @@ namespace prune { scene.set_viewport(viewport); scene.draw_viewport_overlays(); - draw_editor_tool_palette(scene, tool_palette_layout, palette_min, palette_max); + draw_editor_tool_palette(scene, tool_palette_layout, grid_options, palette_min, palette_max); } else { ImGui::Dummy(viewport_size);