diff --git a/DECISIONS.md b/DECISIONS.md index c150e22..4f74182 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -845,7 +845,7 @@ If undo/redo is bolted into each feature separately, the project will accumulate ### Decision -Implement scale before rotate. +Implement scale before rotate, defer snapping to the grid until we have a better understanding of tools, undo/redo, and inspector behaviour. ### Why diff --git a/NOTES.md b/NOTES.md index b886e56..f15b8e5 100644 --- a/NOTES.md +++ b/NOTES.md @@ -130,11 +130,11 @@ Scale fits the current object model because Prune already has width/height and r Initial scope: -- Single selected authored object -- Corner or edge handle -- Minimum size clamp -- Inspector updates immediately -- Undo/redo command recorded when the drag completes +- [x] Single selected authored object +- [x] Corner or edge handle +- [x] Minimum size clamp +- [x] Inspector updates immediately +- [x] Undo/redo command recorded when the drag completes ### 7. Basic audio and event hooks @@ -148,22 +148,43 @@ Preferred direction: Initial scope: -- One or two hard-coded sound resources -- Fire event -- Hit/destroy event -- Global enable/disable toggle +- [ ] One or two hard-coded sound resources +- [ ] Fire event +- [ ] Hit/destroy event +- [ ] Global enable/disable toggle -## Later +## Medium term, in no particular order +- Z-Index ordering - Rotate tool -- Grouping -- Copy/paste -- Prefabs/templates +- Delete action and command +- Background image support +- Simplified asset management +- Copy command +- Text rendering +- Paste command +- Behaviour toggles for authored objects +- Polished sample scenes - Asset browser -- Animation/facing support +- Animated sprites/facing support - Scene file versioning +- Grid snapping support on all tools and UI option, not in settings +- Runtime reset and reload, not just pause/resume +- Object locking and protection +- More robust collision shapes and collision options + +## Later term, in no particular order + +- Scene layering, rendering and collision etc. +- Grouping +- Input mapping and rebinding +- Scene settings panel for background, music, and other scene-wide options +- Game UI panels +- Pathfinding support - Full audio mixer -- Many more tools +- Prefabs/templates +- Native playable export +- WebAssembly playable export - Card scene - Puzzle scene -- More robust collision shapes and collision options +- Many more scene tools diff --git a/README.md b/README.md index 78a1994..962cf84 100644 --- a/README.md +++ b/README.md @@ -194,8 +194,8 @@ Current priorities: - [x] Command history panel - [x] Multi-select - [x] Multi-select move/delete -- [ ] Tool mode state -- [ ] Scale tool +- [x] Tool mode state +- [x] Scale tool - [ ] Basic audio hooks My development plan is tracked in [NOTES.md](NOTES.md), check the file for more details on what each of these points mean as well as what is included - this is what I will be working on in the next development phase. @@ -204,18 +204,18 @@ My development plan is tracked in [NOTES.md](NOTES.md), check the file for more I will consider Prune ready for users when the below is ready, this is the minimum I think users expect from a live editor/runtime prototype: -- Open app. -- Pick a scene type. -- Select object in viewport. -- Move with handle. -- Resize with scale tool. -- Duplicate it. -- Delete it. -- Undo/redo all of that. -- Save. -- Load. -- Behaviour still works. -- Runtime objects do not get accidentally edited or saved. +- [x] Open app. +- [ ] Pick a scene type. (Deferred for now, initial scene type is hardcoded to Platformer, others in new dropdown.) +- [x] Select object in viewport. +- [x] Move with handle. +- [x] Resize with scale tool. +- [ ] Duplicate it. +- [ ] Delete it. +- [x] Undo/redo all of that. +- [x] Save. +- [x] Load. +- [x] Behaviour still works. +- [x] Runtime objects do not get accidentally edited or saved. ## Documentation stance diff --git a/src/prune/editor/editor_tool.hpp b/src/prune/editor/editor_tool.hpp index 6832d11..95d8e75 100644 --- a/src/prune/editor/editor_tool.hpp +++ b/src/prune/editor/editor_tool.hpp @@ -4,7 +4,8 @@ namespace prune { enum class EditorTool { Select = 0, - Move + Move, + Scale }; [[nodiscard]] inline constexpr const char* editor_tool_label(EditorTool tool) noexcept @@ -14,6 +15,8 @@ namespace prune { return "Select"; case EditorTool::Move: return "Move"; + case EditorTool::Scale: + return "Scale"; } return "Unknown"; @@ -24,4 +27,9 @@ namespace prune { return tool == EditorTool::Move; } + [[nodiscard]] inline constexpr bool editor_tool_allows_scale(EditorTool tool) noexcept + { + return tool == EditorTool::Scale; + } + } diff --git a/src/prune/editor/tools/transform_gizmo.hpp b/src/prune/editor/tools/transform_gizmo.hpp index fd26220..19804a0 100644 --- a/src/prune/editor/tools/transform_gizmo.hpp +++ b/src/prune/editor/tools/transform_gizmo.hpp @@ -1,11 +1,37 @@ #pragma once +#include + #include namespace prune::editor::tools::transform_gizmo { constexpr int k_outline_padding = 2; constexpr int k_move_handle_size = 12; + constexpr int k_scale_handle_size = 8; + + enum class ScaleHandle { + None = 0, + TopLeft, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left + }; + + inline constexpr std::array k_scale_handles{ + ScaleHandle::TopLeft, + ScaleHandle::Top, + ScaleHandle::TopRight, + ScaleHandle::Right, + ScaleHandle::BottomRight, + ScaleHandle::Bottom, + ScaleHandle::BottomLeft, + ScaleHandle::Left + }; [[nodiscard]] inline SDL_Rect selected_outline_rect(const SDL_Rect& object_rect) noexcept { @@ -27,6 +53,38 @@ namespace prune::editor::tools::transform_gizmo { }; } + [[nodiscard]] inline SDL_Rect scale_handle_rect(const SDL_Rect& selected_outline, ScaleHandle handle) noexcept + { + const int half_size = k_scale_handle_size / 2; + const int center_x = selected_outline.x + (selected_outline.w / 2); + const int center_y = selected_outline.y + (selected_outline.h / 2); + const int right = selected_outline.x + selected_outline.w; + const int bottom = selected_outline.y + selected_outline.h; + + switch (handle) { + case ScaleHandle::TopLeft: + return SDL_Rect{ selected_outline.x - half_size, selected_outline.y - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::Top: + return SDL_Rect{ center_x - half_size, selected_outline.y - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::TopRight: + return SDL_Rect{ right - half_size, selected_outline.y - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::Right: + return SDL_Rect{ right - half_size, center_y - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::BottomRight: + return SDL_Rect{ right - half_size, bottom - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::Bottom: + return SDL_Rect{ center_x - half_size, bottom - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::BottomLeft: + return SDL_Rect{ selected_outline.x - half_size, bottom - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::Left: + return SDL_Rect{ selected_outline.x - half_size, center_y - half_size, k_scale_handle_size, k_scale_handle_size }; + case ScaleHandle::None: + break; + } + + return SDL_Rect{}; + } + [[nodiscard]] inline bool contains_point(const SDL_Rect& rect, int x, int y) noexcept { return x >= rect.x && @@ -35,4 +93,43 @@ namespace prune::editor::tools::transform_gizmo { y < rect.y + rect.h; } + [[nodiscard]] inline ScaleHandle scale_handle_at_point(const SDL_Rect& selected_outline, int x, int y) noexcept + { + for (const ScaleHandle handle : k_scale_handles) { + if (contains_point(scale_handle_rect(selected_outline, handle), x, y)) { + return handle; + } + } + + return ScaleHandle::None; + } + + [[nodiscard]] inline bool scale_handle_resizes_left(ScaleHandle handle) noexcept + { + return handle == ScaleHandle::TopLeft || + handle == ScaleHandle::BottomLeft || + handle == ScaleHandle::Left; + } + + [[nodiscard]] inline bool scale_handle_resizes_right(ScaleHandle handle) noexcept + { + return handle == ScaleHandle::TopRight || + handle == ScaleHandle::BottomRight || + handle == ScaleHandle::Right; + } + + [[nodiscard]] inline bool scale_handle_resizes_top(ScaleHandle handle) noexcept + { + return handle == ScaleHandle::TopLeft || + handle == ScaleHandle::TopRight || + handle == ScaleHandle::Top; + } + + [[nodiscard]] inline bool scale_handle_resizes_bottom(ScaleHandle handle) noexcept + { + return handle == ScaleHandle::BottomLeft || + handle == ScaleHandle::BottomRight || + handle == ScaleHandle::Bottom; + } + } diff --git a/src/prune/scene/scene.hpp b/src/prune/scene/scene.hpp index c9c088b..05b0266 100644 --- a/src/prune/scene/scene.hpp +++ b/src/prune/scene/scene.hpp @@ -91,6 +91,11 @@ namespace prune { object.editor.movable; } + [[nodiscard]] virtual bool object_is_scalable(const GameObject& object) const + { + return object_is_selectable(object) && object_is_editable(object); + } + [[nodiscard]] virtual ObjectConcept object_concept_for(const GameObject& object) const { return object_concepts::fallback_for(object); diff --git a/src/prune/scene/scene_interaction.cpp b/src/prune/scene/scene_interaction.cpp index 09a005e..1fdd48d 100644 --- a/src/prune/scene/scene_interaction.cpp +++ b/src/prune/scene/scene_interaction.cpp @@ -1,7 +1,3 @@ -#include "prune/scene/scene_interaction.hpp" - -#include "prune/scene/scene.hpp" -#include "prune/editor/tools/transform_gizmo.hpp" #include #include @@ -10,6 +6,11 @@ #include +#include "prune/editor/tools/transform_gizmo.hpp" +#include "prune/scene/scene.hpp" +#include "prune/scene/scene_interaction.hpp" + + namespace prune { void SceneInteraction::update( @@ -113,46 +114,74 @@ namespace prune { { if (state.drag_state.active) { if (!input.is_mouse_button_down(SDL_BUTTON_LEFT) || !state.viewport.has_area()) { - std::vector before_objects; - std::vector after_objects; - - before_objects.reserve(state.drag_state.object_starts.size()); - after_objects.reserve(state.drag_state.object_starts.size()); - - for (const DragObjectStart& start : state.drag_state.object_starts) { - GameObject* object = state.objects.get_by_id(start.object_id); - if (!object) { - continue; + switch (state.drag_state.mode) { + case DragMode::MoveObjects: + { + std::vector before_objects; + std::vector after_objects; + + before_objects.reserve(state.drag_state.object_starts.size()); + after_objects.reserve(state.drag_state.object_starts.size()); + + for (const DragObjectStart& start : state.drag_state.object_starts) { + GameObject* object = state.objects.get_by_id(start.object_id); + if (!object) { + continue; + } + + const GameObject& before = start.object; + if (before.transform.x == object->transform.x && before.transform.y == object->transform.y) { + continue; + } + + before_objects.push_back(before); + after_objects.push_back(*object); + } + + if (before_objects.size() == 1) { + scene.record_editor_command(make_object_command( + EditorCommandType::MoveObject, + editor_command_type_label(EditorCommandType::MoveObject), + before_objects.front(), + after_objects.front(), + "Mouse drag" + )); + } + else if (!before_objects.empty()) { + scene.record_editor_command(make_multi_object_command( + EditorCommandType::MoveObjects, + editor_command_type_label(EditorCommandType::MoveObjects), + before_objects, + after_objects, + std::to_string(before_objects.size()) + " objects, mouse drag" + )); + } } - - GameObject before = *object; - before.transform = start.transform; - - if (before.transform.x == object->transform.x && before.transform.y == object->transform.y) { - continue; + break; + + case DragMode::ScaleObject: + if (GameObject* object = state.objects.get_by_id(state.drag_state.object_id)) { + const GameObject& before = state.drag_state.object_start; + const bool changed = + before.transform.x != object->transform.x || + before.transform.y != object->transform.y || + before.size.width != object->size.width || + before.size.height != object->size.height; + + if (changed) { + scene.record_editor_command(make_object_command( + EditorCommandType::ChangeObjectSize, + "Scale object", + before, + *object, + "Mouse drag" + )); + } } + break; - before_objects.push_back(before); - after_objects.push_back(*object); - } - - if (before_objects.size() == 1) { - scene.record_editor_command(make_object_command( - EditorCommandType::MoveObject, - editor_command_type_label(EditorCommandType::MoveObject), - before_objects.front(), - after_objects.front(), - "Mouse drag" - )); - } - else if (!before_objects.empty()) { - scene.record_editor_command(make_multi_object_command( - EditorCommandType::MoveObjects, - editor_command_type_label(EditorCommandType::MoveObjects), - before_objects, - after_objects, - std::to_string(before_objects.size()) + " objects, mouse drag" - )); + case DragMode::None: + break; } state.drag_state = {}; @@ -163,18 +192,38 @@ namespace prune { const float delta_x = mouse_world.x - state.drag_state.mouse_start_world.x; const float delta_y = mouse_world.y - state.drag_state.mouse_start_world.y; - for (const DragObjectStart& start : state.drag_state.object_starts) { - GameObject* object = state.objects.get_by_id(start.object_id); - if (!object || !scene.object_is_movable(*object)) { - continue; - } + switch (state.drag_state.mode) { + case DragMode::MoveObjects: + for (const DragObjectStart& start : state.drag_state.object_starts) { + GameObject* object = state.objects.get_by_id(start.object_id); + if (!object || !scene.object_is_movable(*object)) { + continue; + } - object->transform.x = start.transform.x + delta_x; - object->transform.y = start.transform.y + delta_y; + object->transform.x = start.object.transform.x + delta_x; + object->transform.y = start.object.transform.y + delta_y; - if (grid_options.snap_to_grid) { - snap_object_to_grid(grid_options, *object); + if (grid_options.snap_to_grid) { + snap_object_to_grid(grid_options, *object); + } } + break; + + case DragMode::ScaleObject: + if (GameObject* object = state.objects.get_by_id(state.drag_state.object_id); + object && scene.object_is_scalable(*object)) { + apply_scale_drag( + *object, + state.drag_state.object_start, + state.drag_state.scale_handle, + delta_x, + delta_y + ); + } + break; + + case DragMode::None: + break; } return; @@ -192,6 +241,23 @@ namespace prune { return; } + const auto scale_handle = scale_handle_from_scale_tool_at_screen(scene, state, camera, input.mouse_x(), input.mouse_y()); + if (scale_handle != editor::tools::transform_gizmo::ScaleHandle::None) { + GameObject* selected = state.objects.selected_object(); + if (!selected || !scene.object_is_scalable(*selected)) { + return; + } + + state.drag_state = {}; + state.drag_state.active = true; + state.drag_state.mode = DragMode::ScaleObject; + state.drag_state.object_id = selected->identity.id; + state.drag_state.object_start = *selected; + state.drag_state.mouse_start_world = camera.screen_to_world(state.viewport, input.mouse_x(), input.mouse_y()); + state.drag_state.scale_handle = scale_handle; + return; + } + const std::vector movable_ids = movable_objects_from_drag_start_at_screen(scene, state, camera, input.mouse_x(), input.mouse_y()); if (movable_ids.empty()) { return; @@ -199,6 +265,7 @@ namespace prune { state.drag_state = {}; state.drag_state.active = true; + state.drag_state.mode = DragMode::MoveObjects; state.drag_state.object_id = movable_ids.front(); state.drag_state.mouse_start_world = camera.screen_to_world(state.viewport, input.mouse_x(), input.mouse_y()); state.drag_state.object_starts.reserve(movable_ids.size()); @@ -209,11 +276,11 @@ namespace prune { continue; } - state.drag_state.object_starts.push_back(DragObjectStart{ id, object->transform }); + state.drag_state.object_starts.push_back(DragObjectStart{ id, *object }); } if (const GameObject* object = state.objects.get_by_id(state.drag_state.object_id)) { - state.drag_state.object_start = object->transform; + state.drag_state.object_start = *object; } } @@ -432,6 +499,33 @@ namespace prune { return nullptr; } + editor::tools::transform_gizmo::ScaleHandle SceneInteraction::scale_handle_from_scale_tool_at_screen(Scene& scene, SceneState& state, const SceneCamera& camera, int screen_x, int screen_y) noexcept + { + if (!editor_tool_allows_scale(state.editor_tool)) { + return editor::tools::transform_gizmo::ScaleHandle::None; + } + + if (!state.scene_options.highlight_selected || state.objects.selected_count() != 1) { + return editor::tools::transform_gizmo::ScaleHandle::None; + } + + GameObject* selected = state.objects.selected_object(); + if (!selected || !scene.object_is_scalable(*selected)) { + return editor::tools::transform_gizmo::ScaleHandle::None; + } + + const SDL_Rect object_rect = camera.world_to_screen_rect(*selected); + if (!rect_visible(state.viewport, object_rect)) { + return editor::tools::transform_gizmo::ScaleHandle::None; + } + + const int local_mouse_x = screen_x - state.viewport.screen_x; + const int local_mouse_y = screen_y - state.viewport.screen_y; + + const SDL_Rect selected_outline = editor::tools::transform_gizmo::selected_outline_rect(object_rect); + return editor::tools::transform_gizmo::scale_handle_at_point(selected_outline, local_mouse_x, local_mouse_y); + } + std::vector SceneInteraction::movable_objects_from_drag_start_at_screen(Scene& scene, SceneState& state, const SceneCamera& camera, int screen_x, int screen_y) @@ -443,6 +537,10 @@ namespace prune { } } + if (editor_tool_allows_scale(state.editor_tool)) { + return {}; + } + return movable_objects_from_handle_at_screen(scene, state, camera, screen_x, screen_y); } @@ -602,4 +700,64 @@ namespace prune { object.transform.y = snap_value_to_grid(grid_options, object.transform.y); } + void SceneInteraction::apply_scale_drag( + GameObject& object, + const GameObject& start, + editor::tools::transform_gizmo::ScaleHandle handle, + float delta_x, + float delta_y + ) noexcept + { + namespace gizmo = editor::tools::transform_gizmo; + + const float start_left = start.transform.x; + const float start_top = start.transform.y; + const float start_right = start_left + static_cast(start.size.width); + const float start_bottom = start_top + static_cast(start.size.height); + + float left = start_left; + float top = start_top; + float right = start_right; + float bottom = start_bottom; + + if (gizmo::scale_handle_resizes_left(handle)) { + left += delta_x; + } + + if (gizmo::scale_handle_resizes_right(handle)) { + right += delta_x; + } + + if (gizmo::scale_handle_resizes_top(handle)) { + top += delta_y; + } + + if (gizmo::scale_handle_resizes_bottom(handle)) { + bottom += delta_y; + } + + const int width = std::clamp( + static_cast(std::round(right - left)), + k_min_object_size, + k_max_object_size + ); + + const int height = std::clamp( + static_cast(std::round(bottom - top)), + k_min_object_size, + k_max_object_size + ); + + object.size.width = width; + object.size.height = height; + + object.transform.x = gizmo::scale_handle_resizes_left(handle) + ? start_right - static_cast(width) + : start_left; + + object.transform.y = gizmo::scale_handle_resizes_top(handle) + ? start_bottom - static_cast(height) + : start_top; + } + } \ No newline at end of file diff --git a/src/prune/scene/scene_interaction.hpp b/src/prune/scene/scene_interaction.hpp index bbdebdc..3df929e 100644 --- a/src/prune/scene/scene_interaction.hpp +++ b/src/prune/scene/scene_interaction.hpp @@ -1,10 +1,12 @@ #pragma once +#include + +#include "prune/editor/tools/transform_gizmo.hpp" #include "prune/core/input.hpp" #include "prune/scene/scene_camera.hpp" #include "prune/scene/scene_state.hpp" -#include namespace prune { @@ -32,6 +34,7 @@ namespace prune { void handle_delete_duplicate_shortcuts(Scene& scene, SceneState& state, const GridOptions& grid_options, const Input& input); [[nodiscard]] GameObject* pick_object_at_screen(Scene& scene, SceneState& state, const SceneCamera& camera, int screen_x, int screen_y) noexcept; + [[nodiscard]] editor::tools::transform_gizmo::ScaleHandle scale_handle_from_scale_tool_at_screen(Scene& scene, SceneState& state, const SceneCamera& camera, int screen_x, int screen_y) noexcept; [[nodiscard]] std::vector movable_objects_from_drag_start_at_screen(Scene& scene, SceneState& state, const SceneCamera& camera, int screen_x, int screen_y); [[nodiscard]] std::vector movable_objects_from_move_tool_at_screen(Scene& scene, SceneState& state, const SceneCamera& camera, int screen_x, int screen_y); [[nodiscard]] std::vector movable_objects_from_handle_at_screen(Scene& scene, SceneState& state, const SceneCamera& camera, int screen_x, int screen_y); @@ -48,6 +51,14 @@ namespace prune { GameObject& object ) noexcept; + static void apply_scale_drag( + GameObject& object, + const GameObject& start, + editor::tools::transform_gizmo::ScaleHandle handle, + float delta_x, + float delta_y + ) noexcept; + bool m_viewport_pan_active = false; Camera m_viewport_pan_start{}; }; diff --git a/src/prune/scene/scene_renderer.cpp b/src/prune/scene/scene_renderer.cpp index d05d273..ae2f585 100644 --- a/src/prune/scene/scene_renderer.cpp +++ b/src/prune/scene/scene_renderer.cpp @@ -247,11 +247,31 @@ namespace prune { } } - void SceneRenderer::draw_selected_gizmo(SDL_Renderer* renderer, const SDL_Rect& selected_outline, bool movable) const + void SceneRenderer::draw_selected_gizmo( + SDL_Renderer* renderer, + const SDL_Rect& selected_outline, + EditorTool current_tool, + bool movable, + bool scalable + ) const { SDL_SetRenderDrawColor(renderer, 174, 99, 242, 190); SDL_RenderDrawRect(renderer, &selected_outline); + if (current_tool == EditorTool::Scale) { + if (!scalable) { + return; + } + + SDL_SetRenderDrawColor(renderer, 236, 205, 255, 255); + for (const editor::tools::transform_gizmo::ScaleHandle handle : editor::tools::transform_gizmo::k_scale_handles) { + const SDL_Rect scale_handle = editor::tools::transform_gizmo::scale_handle_rect(selected_outline, handle); + SDL_RenderFillRect(renderer, &scale_handle); + } + + return; + } + if (!movable) { return; } @@ -283,7 +303,13 @@ namespace prune { const SDL_Rect selected_outline = editor::tools::transform_gizmo::selected_outline_rect(object_rect); const bool show_object_handle = state.objects.selected_count() == 1; - draw_selected_gizmo(renderer, selected_outline, show_object_handle && scene.object_is_movable(object)); + draw_selected_gizmo( + renderer, + selected_outline, + state.editor_tool, + show_object_handle && scene.object_is_movable(object), + show_object_handle && scene.object_is_scalable(object) + ); } @@ -303,6 +329,10 @@ namespace prune { SDL_SetRenderDrawColor(renderer, 120, 190, 255, 230); SDL_RenderDrawRect(renderer, &outline); + if (state.editor_tool == EditorTool::Scale) { + return; + } + if (!multi_selection_is_movable(scene, state)) { return; } diff --git a/src/prune/scene/scene_renderer.hpp b/src/prune/scene/scene_renderer.hpp index f4e8d0c..f6c6426 100644 --- a/src/prune/scene/scene_renderer.hpp +++ b/src/prune/scene/scene_renderer.hpp @@ -34,7 +34,7 @@ namespace prune { void draw_sprite_object(SDL_Renderer* renderer, const SceneState& state, const SceneCamera& camera, const GameObject& object); void draw_sprite_fallback(SDL_Renderer* renderer, const SDL_Rect& rect) const; void draw_debug_overlays(SDL_Renderer* renderer, const SceneState& state, const SceneCamera& camera, const GameObject& object) const; - void draw_selected_gizmo(SDL_Renderer* renderer, const SDL_Rect& selected_outline, bool movable) const; + void draw_selected_gizmo(SDL_Renderer* renderer, const SDL_Rect& selected_outline, EditorTool current_tool, bool movable, bool scalable) const; void draw_selected_outline(SDL_Renderer* renderer, const Scene& scene, const SceneState& state, const SceneCamera& camera, const GameObject& object) const; void draw_multi_selection_bounds(SDL_Renderer* renderer, const Scene& scene, const SceneState& state, const SceneCamera& camera) const; [[nodiscard]] bool selected_screen_bounds(const SceneState& state, const SceneCamera& camera, SDL_Rect& bounds) const; diff --git a/src/prune/scene/scene_state.hpp b/src/prune/scene/scene_state.hpp index f71afb2..05c7e32 100644 --- a/src/prune/scene/scene_state.hpp +++ b/src/prune/scene/scene_state.hpp @@ -2,6 +2,7 @@ #include "prune/core/defaults.hpp" #include "prune/editor/editor_command.hpp" +#include "prune/editor/tools/transform_gizmo.hpp" #include "prune/editor/editor_tool.hpp" #include "prune/scene/game_object.hpp" #include "prune/scene/game_object_manager.hpp" @@ -55,17 +56,26 @@ namespace prune { int max_nudge_step = 64; }; + enum class DragMode { + None = 0, + MoveObjects, + ScaleObject + }; + struct DragObjectStart { GameObjectId object_id = k_invalid_game_object_id; - Transform transform{}; + GameObject object{}; }; struct DragState { bool active = false; + DragMode mode = DragMode::None; GameObjectId object_id = k_invalid_game_object_id; - Transform object_start{}; + GameObject object_start{}; std::vector object_starts; Transform mouse_start_world{}; + editor::tools::transform_gizmo::ScaleHandle scale_handle = + editor::tools::transform_gizmo::ScaleHandle::None; }; struct SceneState { diff --git a/src/prune/tooling/inspector.cpp b/src/prune/tooling/inspector.cpp index 2b65672..13ddacf 100644 --- a/src/prune/tooling/inspector.cpp +++ b/src/prune/tooling/inspector.cpp @@ -197,6 +197,7 @@ namespace prune { const bool can_edit = scene.object_is_editable(*selected); const bool can_move = scene.object_is_movable(*selected); + const bool can_scale = scene.object_is_scalable(*selected); if (!can_edit) { ImGui::TextWrapped("This object is protected by the scene and cannot be edited from the generic inspector."); @@ -243,7 +244,7 @@ namespace prune { if (tooling::imgui::layout::collapsing_header("Size")) { if (tooling::imgui::property_table::begin("##size")) { - ImGui::BeginDisabled(!can_edit); + ImGui::BeginDisabled(!can_scale); tooling::editor::tracked_property_table::slider_int( m_object_edit_tracker, diff --git a/src/prune/tooling/ui.cpp b/src/prune/tooling/ui.cpp index 8a27780..4e5df60 100644 --- a/src/prune/tooling/ui.cpp +++ b/src/prune/tooling/ui.cpp @@ -45,7 +45,7 @@ namespace prune { const std::span actions = scene.scene_creation_actions(); const float editor_tools_width = - (k_tool_button_min_width * 2.0f) + style.ItemSpacing.x; + (k_tool_button_min_width * 3.0f) + (style.ItemSpacing.x * 2.0f); ToolPaletteLayout layout{}; float content_width = editor_tools_width; @@ -167,6 +167,8 @@ namespace prune { 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_scene_creation_actions) { ImGui::Separator();