3#include "absl/strings/str_format.h"
11#include "imgui/imgui.h"
33 const int encoded_x = std::clamp(clamped_x / 4, 0, 255);
34 const int encoded_y = std::clamp(clamped_y / 16, 0, 255);
35 return static_cast<uint16_t
>((encoded_y << 8) | encoded_x);
41 const ImGuiIO& io = ImGui::GetIO();
43 const bool mouse_left_down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
44 const bool mouse_left_released =
45 ImGui::IsMouseReleased(ImGuiMouseButton_Left);
50 const bool should_process_without_hover =
54 (mouse_left_down || mouse_left_released)) ||
57 if (!hovered && !should_process_without_hover) {
61 const ImVec2 canvas_mouse_pos =
63 const int canvas_mouse_x =
static_cast<int>(std::floor(canvas_mouse_pos.x));
64 const int canvas_mouse_y =
static_cast<int>(std::floor(canvas_mouse_pos.y));
65 const bool pointer_within_room =
69 if (ImGui::IsKeyPressed(ImGuiKey_Escape) &&
76 if (pointer_within_room &&
87 if (hovered && mouse_left_down) {
90 if (pointer_within_room) {
96 if (pointer_within_room) {
103 if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
108 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
113 if (mouse_left_released) {
119 int canvas_x =
static_cast<int>(std::floor(canvas_mouse_pos.x));
120 int canvas_y =
static_cast<int>(std::floor(canvas_mouse_pos.y));
146 const ImVec2& canvas_mouse_pos) {
147 auto [room_x, room_y] =
149 static_cast<int>(canvas_mouse_pos.y));
155 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
157 if (!state.is_painting) {
159 state.paint_mutation_started =
false;
160 state.paint_last_tile_x = room_x;
161 state.paint_last_tile_y = room_y;
165 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
167 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
169 bool changed =
false;
170 auto ensure_mutation = [&]() {
171 if (!state.paint_mutation_started) {
173 state.paint_mutation_started =
true;
178 x0, y0, room_x, room_y, [&](
int lx,
int ly) {
180 lx, ly, state.paint_brush_radius,
182 [&](
int bx,
int by) {
183 if (room.GetCollisionTile(bx, by) ==
184 state.paint_collision_value) {
188 room.SetCollisionTile(bx, by, state.paint_collision_value);
198 state.paint_last_tile_x = room_x;
199 state.paint_last_tile_y = room_y;
204void DungeonObjectInteraction::UpdateWaterFillPainting(
205 const ImVec2& canvas_mouse_pos) {
207 mode_manager_.SetMode(InteractionMode::Select);
211 const ImGuiIO& io = ImGui::GetIO();
212 const bool erase = io.KeyAlt;
214 auto [room_x, room_y] =
215 CanvasToRoomCoordinates(
static_cast<int>(canvas_mouse_pos.x),
216 static_cast<int>(canvas_mouse_pos.y));
217 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
218 auto& room = (*rooms_)[current_room_id_];
219 auto& state = mode_manager_.GetModeState();
222 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
223 const bool new_val = !erase;
225 if (!state.is_painting) {
226 state.is_painting =
true;
227 state.paint_mutation_started =
false;
228 state.paint_last_tile_x = room_x;
229 state.paint_last_tile_y = room_y;
233 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
235 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
237 bool changed =
false;
238 auto ensure_mutation = [&]() {
239 if (!state.paint_mutation_started) {
240 interaction_context_.NotifyMutation(MutationDomain::kWaterFill);
241 state.paint_mutation_started =
true;
245 paint_util::ForEachPointOnLine(
246 x0, y0, room_x, room_y, [&](
int lx,
int ly) {
247 paint_util::ForEachPointInSquareBrush(
248 lx, ly, state.paint_brush_radius,
250 [&](
int bx,
int by) {
251 if (room.GetWaterFillTile(bx, by) == new_val) {
255 room.SetWaterFillTile(bx, by, new_val);
261 interaction_context_.NotifyInvalidateCache(MutationDomain::kWaterFill);
264 state.paint_last_tile_x = room_x;
265 state.paint_last_tile_y = room_y;
270void DungeonObjectInteraction::HandleObjectSelectionStart(
271 const ImVec2& canvas_mouse_pos) {
272 const bool has_object_selection = selection_.HasSelection();
273 const bool has_entity_selection = HasEntitySelection();
274 if (!has_object_selection && !has_entity_selection) {
278 mode_manager_.SetMode(InteractionMode::DraggingObjects);
279 if (has_object_selection) {
280 entity_coordinator_.tile_handler().InitDrag(canvas_mouse_pos);
282 if (has_entity_selection) {
283 entity_coordinator_.BeginSelectionDrag(canvas_mouse_pos);
287void DungeonObjectInteraction::HandleEmptySpaceClick(
288 const ImVec2& canvas_mouse_pos) {
289 const ImGuiIO& io = ImGui::GetIO();
290 const bool additive = io.KeyShift || io.KeyCtrl || io.KeySuper;
291 const bool had_selection =
292 selection_.HasSelection() || entity_coordinator_.HasEntitySelection();
297 ClearEntitySelection();
298 selection_.ClearSelection();
301 if (!had_selection) {
302 entity_coordinator_.tile_handler().BeginMarqueeSelection(canvas_mouse_pos);
306void DungeonObjectInteraction::HandleMouseRelease() {
310 const auto mode = mode_manager_.GetMode();
311 if (mode == InteractionMode::PaintCollision ||
312 mode == InteractionMode::PaintWaterFill) {
313 auto& state = mode_manager_.GetModeState();
314 const bool had_mutation = state.paint_mutation_started;
315 state.is_painting =
false;
316 state.paint_mutation_started =
false;
317 state.paint_last_tile_x = -1;
318 state.paint_last_tile_y = -1;
322 interaction_context_.NotifyInvalidateCache(
323 (mode == InteractionMode::PaintCollision)
324 ? MutationDomain::kCustomCollision
325 : MutationDomain::kWaterFill);
330 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
331 mode_manager_.SetMode(InteractionMode::Select);
333 entity_coordinator_.HandleRelease();
338bool DungeonObjectInteraction::HandleKeyboardNudge() {
339 if (!rooms_ || current_room_id_ < 0 ||
340 current_room_id_ >=
static_cast<int>(rooms_->size())) {
344 const ImGuiIO& io = ImGui::GetIO();
345 if (ImGui::IsAnyItemActive() || io.KeyCtrl || io.KeySuper || io.KeyAlt ||
346 ImGui::IsMouseDown(ImGuiMouseButton_Left) ||
347 entity_coordinator_.IsPlacementActive()) {
351 const auto mode = mode_manager_.GetMode();
352 if (mode == InteractionMode::DraggingObjects ||
353 mode == InteractionMode::PaintCollision ||
354 mode == InteractionMode::PaintWaterFill) {
360 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow,
true)) {
362 }
else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow,
true)) {
365 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow,
true)) {
367 }
else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow,
true)) {
371 if (delta_x == 0 && delta_y == 0) {
375 return NudgeSelected(delta_x, delta_y);
378bool DungeonObjectInteraction::NudgeSelected(
int delta_x,
int delta_y) {
379 if (!rooms_ || current_room_id_ < 0 ||
380 current_room_id_ >=
static_cast<int>(rooms_->size())) {
384 bool handled =
false;
385 if (selection_.HasSelection()) {
386 entity_coordinator_.tile_handler().MoveObjects(
387 current_room_id_, selection_.GetSelectedIndices(), delta_x, delta_y);
391 if (entity_coordinator_.HasEntitySelection()) {
392 handled = entity_coordinator_.NudgeSelected(delta_x, delta_y) || handled;
398void DungeonObjectInteraction::CheckForObjectSelection() {
400 const ImGuiIO& io = ImGui::GetIO();
401 const ImVec2 mouse_pos = GetCanvasTransform().ScreenToRoomPixels(io.MousePos);
402 const bool mouse_left_released =
403 ImGui::IsMouseReleased(ImGuiMouseButton_Left);
405 if (mouse_left_released && selection_.IsRectangleSelectionActive()) {
406 selection_.UpdateRectangleSelection(
static_cast<int>(mouse_pos.x),
407 static_cast<int>(mouse_pos.y));
408 constexpr int kMinRectPixels = 6;
409 if (!io.KeyAlt && selection_.IsRectangleLargeEnough(kMinRectPixels)) {
410 entity_coordinator_.SelectEntitiesInRect(
411 selection_.GetRectangleSelectionBounds(),
417 entity_coordinator_.tile_handler().HandleMarqueeSelection(
419 ImGui::IsMouseDown(ImGuiMouseButton_Left),
422 io.KeyCtrl || io.KeySuper,
426void DungeonObjectInteraction::DrawSelectionHighlights() {
427 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
430 auto& room = (*rooms_)[current_room_id_];
431 const auto& objects = room.GetTileObjects();
434 selection_.DrawSelectionHighlights(
437 return std::make_tuple(result.offset_x_tiles * 8,
438 result.offset_y_tiles * 8, result.width_pixels(),
439 result.height_pixels());
444 if (entity_coordinator_.HasEntitySelection()) {
448 if (canvas_->IsMouseHovering()) {
450 ImGuiIO& io = ImGui::GetIO();
451 const auto [cursor_x, cursor_y] =
452 GetCanvasTransform().ScreenToRoomPixelCoordinates(io.MousePos);
453 auto entity_at_cursor =
454 entity_coordinator_.GetEntityAtPosition(cursor_x, cursor_y);
455 if (entity_at_cursor.has_value()) {
457 DrawHoverHighlight(objects);
461 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(
463 if (hovered_index.has_value() && *hovered_index < objects.size()) {
464 const auto&
object = objects[*hovered_index];
467 const int layer =
object.GetLayerValue();
470 const char* subtype_names[] = {
"Unknown",
"Type 1",
"Type 2",
"Type 3"};
471 const char* subtype_name =
472 (subtype >= 0 && subtype <= 3) ? subtype_names[subtype] :
"Unknown";
476 tooltip += object_name;
477 tooltip +=
" (" + std::string(subtype_name) +
")";
479 tooltip +=
"ID: 0x" + absl::StrFormat(
"%03X",
object.id_);
481 static constexpr const char* kStreamNames[] = {
"Primary",
"BG2 overlay",
483 const char* stream_name =
484 (layer >= 0 && layer < 3) ? kStreamNames[layer] :
"Unknown";
485 tooltip +=
" | Object stream: " + std::string(stream_name);
487 const char* layer_name =
488 layer == 0 ?
"Upper layer (BG1)" :
"Lower layer (BG2)";
489 tooltip +=
" | Special layer: " + std::string(layer_name);
491 tooltip +=
" | Pos: (" + std::to_string(
object.x_) +
", " +
492 std::to_string(
object.y_) +
")";
493 tooltip +=
"\nSize: " + std::to_string(
object.size_) +
" (0x" +
494 absl::StrFormat(
"%02X",
object.size_) +
")";
496 if (selection_.IsObjectSelected(*hovered_index)) {
503 ImGui::SetTooltip(
"%s", tooltip.c_str());
508 DrawHoverHighlight(objects);
511void DungeonObjectInteraction::DrawHoverHighlight(
512 const std::vector<zelda3::RoomObject>& objects) {
513 if (!canvas_->IsMouseHovering())
517 if (entity_coordinator_.HasEntitySelection())
522 ImGuiIO& io = ImGui::GetIO();
524 const auto [cursor_canvas_x, cursor_canvas_y] =
526 auto entity_at_cursor =
527 entity_coordinator_.GetEntityAtPosition(cursor_canvas_x, cursor_canvas_y);
528 if (entity_at_cursor.has_value()) {
532 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(
533 cursor_canvas_x, cursor_canvas_y);
534 if (!hovered_index.has_value() || *hovered_index >= objects.size()) {
537 const auto&
object = objects[*hovered_index];
540 if (selection_.IsObjectSelected(*hovered_index)) {
544 const auto& theme = AgentUI::GetTheme();
545 ImDrawList* draw_list = ImGui::GetWindowDrawList();
547 auto [sel_x_px, sel_y_px, pixel_width, pixel_height] =
551 ImVec2(
static_cast<float>(sel_x_px),
static_cast<float>(sel_y_px)));
553 static_cast<float>(pixel_width),
static_cast<float>(pixel_height)));
554 ImVec2 obj_end(obj_start.x + obj_size.x, obj_start.y + obj_size.y);
557 constexpr float margin = 2.0f;
558 obj_start.x -= margin;
559 obj_start.y -= margin;
564 ImVec4 hover_fill = theme.selection_hover;
565 hover_fill.w *= 0.5f;
567 ImVec4 hover_border = theme.selection_hover;
570 draw_list->AddRectFilled(obj_start, obj_end, ImGui::GetColorU32(hover_fill));
573 draw_list->AddRect(obj_start, obj_end, ImGui::GetColorU32(hover_border), 0.0f,
577void DungeonObjectInteraction::PlaceObjectAtPosition(
int room_x,
int room_y) {
578 entity_coordinator_.tile_handler().PlaceObjectAt(
579 current_room_id_, preview_object_, room_x, room_y);
581 if (object_placed_callback_) {
582 object_placed_callback_(preview_object_);
585 interaction_context_.NotifyInvalidateCache(MutationDomain::kTileObjects);
589std::pair<int, int> DungeonObjectInteraction::RoomToCanvasCoordinates(
590 int room_x,
int room_y)
const {
592 return {room_x * 8, room_y * 8};
595std::pair<int, int> DungeonObjectInteraction::CanvasToRoomCoordinates(
596 int canvas_x,
int canvas_y)
const {
598 return {canvas_x / 8, canvas_y / 8};
601bool DungeonObjectInteraction::IsWithinCanvasBounds(
int canvas_x,
int canvas_y,
603 return dungeon_coords::IsWithinBounds(canvas_x, canvas_y, margin);
609 current_room_id_ = room_id;
610 interaction_context_.rooms = rooms;
611 interaction_context_.current_room_id = room_id;
612 interaction_context_.selection = &selection_;
613 entity_coordinator_.SetContext(&interaction_context_);
616void DungeonObjectInteraction::SetPreviewObject(
618 preview_object_ = object;
620 if (loaded &&
object.id_ >= 0) {
623 entity_coordinator_.CancelPlacement();
626 mode_manager_.SetMode(InteractionMode::PlaceObject);
627 mode_manager_.GetModeState().preview_object = object;
631 auto& tile_handler = entity_coordinator_.tile_handler();
632 tile_handler.SetPreviewObject(preview_object_);
633 if (!tile_handler.IsPlacementActive()) {
634 tile_handler.BeginPlacement();
638 if (mode_manager_.GetMode() == InteractionMode::PlaceObject) {
644void DungeonObjectInteraction::ClearSelection() {
645 selection_.ClearSelection();
646 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
647 mode_manager_.SetMode(InteractionMode::Select);
651void DungeonObjectInteraction::HandleDeleteSelected() {
652 auto indices = selection_.GetSelectedIndices();
653 if (!indices.empty()) {
654 entity_coordinator_.tile_handler().DeleteObjects(current_room_id_, indices);
655 selection_.ClearSelection();
658 if (entity_coordinator_.HasEntitySelection()) {
659 entity_coordinator_.DeleteSelectedEntity();
663void DungeonObjectInteraction::HandleDeleteAllObjects() {
664 entity_coordinator_.tile_handler().DeleteAllObjects(current_room_id_);
665 selection_.ClearSelection();
668void DungeonObjectInteraction::HandleCopySelected() {
669 if (!rooms_ || current_room_id_ < 0 ||
670 current_room_id_ >=
static_cast<int>(rooms_->size())) {
674 const auto selected_objects = selection_.GetSelectedIndices();
675 const bool has_object_selection = !selected_objects.empty();
676 const bool has_entity_selection = entity_coordinator_.HasEntitySelection();
677 if (!has_object_selection && !has_entity_selection) {
681 entity_clipboard_.Clear();
682 bool clipboard_origin_set =
false;
684 if (has_object_selection) {
685 entity_coordinator_.tile_handler().CopyObjectsToClipboard(current_room_id_,
687 const auto& objects = (*rooms_)[current_room_id_].GetTileObjects();
688 for (
size_t index : selected_objects) {
689 if (index >= objects.size()) {
692 entity_clipboard_.origin_tile_x = objects[index].x_;
693 entity_clipboard_.origin_tile_y = objects[index].y_;
694 entity_clipboard_.origin_pixel_x =
695 entity_clipboard_.origin_tile_x * dungeon_coords::kTileSize;
696 entity_clipboard_.origin_pixel_y =
697 entity_clipboard_.origin_tile_y * dungeon_coords::kTileSize;
698 clipboard_origin_set =
true;
702 entity_coordinator_.tile_handler().ClearClipboard();
705 CopySelectedEntitiesToClipboard(clipboard_origin_set);
708void DungeonObjectInteraction::CopySelectedEntitiesToClipboard(
709 bool clipboard_origin_set) {
710 if (!rooms_ || current_room_id_ < 0 ||
711 current_room_id_ >=
static_cast<int>(rooms_->size())) {
715 const auto& room = (*rooms_)[current_room_id_];
716 auto selected_entities = entity_coordinator_.GetSelectedEntities();
717 if (selected_entities.empty() && entity_coordinator_.HasEntitySelection()) {
718 const SelectedEntity selected = entity_coordinator_.GetSelectedEntity();
719 if (selected.
type != EntityType::None) {
720 selected_entities.push_back(selected);
724 for (
const auto entity : selected_entities) {
725 switch (entity.type) {
726 case EntityType::Sprite: {
727 const auto& sprites = room.GetSprites();
728 if (entity.index >= sprites.size()) {
731 const auto& sprite = sprites[entity.index];
732 if (!clipboard_origin_set && !entity_clipboard_.HasData()) {
733 entity_clipboard_.origin_pixel_x =
734 sprite.x() * dungeon_coords::kSpriteTileSize;
735 entity_clipboard_.origin_pixel_y =
736 sprite.y() * dungeon_coords::kSpriteTileSize;
737 entity_clipboard_.origin_tile_x =
738 entity_clipboard_.origin_pixel_x / dungeon_coords::kTileSize;
739 entity_clipboard_.origin_tile_y =
740 entity_clipboard_.origin_pixel_y / dungeon_coords::kTileSize;
742 entity_clipboard_.sprites.push_back(sprite);
745 case EntityType::Item: {
746 const auto& items = room.GetPotItems();
747 if (entity.index >= items.size()) {
750 const auto& item = items[entity.index];
751 if (!clipboard_origin_set && !entity_clipboard_.HasData()) {
752 entity_clipboard_.origin_pixel_x = item.GetPixelX();
753 entity_clipboard_.origin_pixel_y = item.GetPixelY();
754 entity_clipboard_.origin_tile_x =
755 entity_clipboard_.origin_pixel_x / dungeon_coords::kTileSize;
756 entity_clipboard_.origin_tile_y =
757 entity_clipboard_.origin_pixel_y / dungeon_coords::kTileSize;
759 entity_clipboard_.items.push_back(item);
762 case EntityType::Door:
763 case EntityType::Object:
764 case EntityType::None:
771std::vector<SelectedEntity> DungeonObjectInteraction::PasteEntityClipboardAt(
772 int target_pixel_x,
int target_pixel_y) {
773 std::vector<SelectedEntity> pasted_entities;
774 if (!entity_clipboard_.HasData() || !rooms_ || current_room_id_ < 0 ||
775 current_room_id_ >=
static_cast<int>(rooms_->size())) {
776 return pasted_entities;
779 auto& room = (*rooms_)[current_room_id_];
780 const int delta_pixel_x = target_pixel_x - entity_clipboard_.origin_pixel_x;
781 const int delta_pixel_y = target_pixel_y - entity_clipboard_.origin_pixel_y;
783 if (!entity_clipboard_.sprites.empty()) {
784 interaction_context_.NotifyMutation(MutationDomain::kSprites);
785 auto& sprites = room.GetSprites();
786 for (
auto sprite : entity_clipboard_.sprites) {
787 const int next_x = std::clamp(
788 (sprite.x() * dungeon_coords::kSpriteTileSize + delta_pixel_x) /
789 dungeon_coords::kSpriteTileSize,
790 0, dungeon_coords::kSpriteGridMax);
791 const int next_y = std::clamp(
792 (sprite.y() * dungeon_coords::kSpriteTileSize + delta_pixel_y) /
793 dungeon_coords::kSpriteTileSize,
794 0, dungeon_coords::kSpriteGridMax);
795 sprite.set_x(next_x);
796 sprite.set_y(next_y);
797 sprites.push_back(sprite);
798 pasted_entities.push_back(
801 room.MarkSpritesDirty();
802 interaction_context_.NotifyInvalidateCache(MutationDomain::kSprites);
805 if (!entity_clipboard_.items.empty()) {
806 interaction_context_.NotifyMutation(MutationDomain::kItems);
807 auto& items = room.GetPotItems();
808 for (
auto item : entity_clipboard_.items) {
809 item.position = EncodePotItemPosition(item.GetPixelX() + delta_pixel_x,
810 item.GetPixelY() + delta_pixel_y);
811 items.push_back(item);
812 pasted_entities.push_back(
815 room.MarkPotItemsDirty();
816 interaction_context_.NotifyInvalidateCache(MutationDomain::kItems);
819 if (!pasted_entities.empty()) {
820 interaction_context_.NotifyEntityChanged();
822 return pasted_entities;
825void DungeonObjectInteraction::HandlePasteObjects() {
826 if (!HasClipboardData()) {
830 if (!rooms_ || current_room_id_ < 0 ||
831 current_room_id_ >=
static_cast<int>(rooms_->size())) {
835 auto& handler = entity_coordinator_.tile_handler();
836 const ImGuiIO& io = ImGui::GetIO();
837 const auto [canvas_mouse_x, canvas_mouse_y] =
838 GetCanvasTransform().ScreenToRoomPixelCoordinates(io.MousePos);
839 auto [paste_x, paste_y] =
840 CanvasToRoomCoordinates(canvas_mouse_x, canvas_mouse_y);
841 int paste_pixel_x = paste_x * dungeon_coords::kTileSize;
842 int paste_pixel_y = paste_y * dungeon_coords::kTileSize;
844 if (!IsWithinCanvasBounds(canvas_mouse_x, canvas_mouse_y, 0)) {
845 const int fallback_delta = entity_clipboard_.sprites.empty()
846 ? dungeon_coords::kTileSize
847 : dungeon_coords::kSpriteTileSize;
849 std::clamp(entity_clipboard_.origin_pixel_x + fallback_delta, 0,
850 dungeon_coords::kRoomPixelWidth - dungeon_coords::kTileSize);
851 paste_pixel_y = std::clamp(
852 entity_clipboard_.origin_pixel_y + fallback_delta, 0,
853 dungeon_coords::kRoomPixelHeight - dungeon_coords::kTileSize);
854 paste_x = paste_pixel_x / dungeon_coords::kTileSize;
855 paste_y = paste_pixel_y / dungeon_coords::kTileSize;
858 std::vector<size_t> new_indices;
859 if (handler.HasClipboardData()) {
860 new_indices = handler.PasteFromClipboard(
861 current_room_id_, paste_x - entity_clipboard_.origin_tile_x,
862 paste_y - entity_clipboard_.origin_tile_y);
864 auto new_entities = PasteEntityClipboardAt(paste_pixel_x, paste_pixel_y);
866 if (!new_indices.empty() || !new_entities.empty()) {
867 selection_.ClearSelection();
868 for (
size_t idx : new_indices) {
869 selection_.SelectObject(idx, ObjectSelection::SelectionMode::Add);
871 entity_coordinator_.SetSelectedEntities(std::move(new_entities));
875void DungeonObjectInteraction::DrawGhostPreview() {
876 entity_coordinator_.DrawGhostPreviews();
879void DungeonObjectInteraction::HandleScrollWheelResize() {
880 const ImGuiIO& io = ImGui::GetIO();
881 entity_coordinator_.HandleMouseWheel(io.MouseWheel);
884bool DungeonObjectInteraction::SetObjectId(
size_t index, int16_t
id) {
885 entity_coordinator_.tile_handler().UpdateObjectsId(current_room_id_, {index},
890bool DungeonObjectInteraction::SetObjectSize(
size_t index, uint8_t size) {
891 entity_coordinator_.tile_handler().UpdateObjectsSize(current_room_id_,
896bool DungeonObjectInteraction::SetObjectLayer(
898 return entity_coordinator_.tile_handler().UpdateObjectsLayer(
899 current_room_id_, {index},
static_cast<int>(layer));
902std::pair<int, int> DungeonObjectInteraction::CalculateObjectBounds(
907bool DungeonObjectInteraction::CanAssignSelectedObjectsToLayer(
908 int target_layer)
const {
909 if (!rooms_ || current_room_id_ < 0 ||
910 current_room_id_ >=
static_cast<int>(rooms_->size()) ||
911 target_layer < 0 || target_layer > 2) {
915 const auto selected = selection_.GetSelectedIndices();
916 if (selected.empty()) {
920 const auto& objects = (*rooms_)[current_room_id_].GetTileObjects();
921 for (
const size_t index : selected) {
922 if (index >= objects.size() ||
930bool DungeonObjectInteraction::SendSelectedToLayer(
int target_layer) {
931 if (!CanAssignSelectedObjectsToLayer(target_layer)) {
934 return entity_coordinator_.tile_handler().UpdateObjectsLayer(
935 current_room_id_, selection_.GetSelectedIndices(), target_layer);
938void DungeonObjectInteraction::SendSelectedToFront() {
939 entity_coordinator_.tile_handler().SendToFront(
940 current_room_id_, selection_.GetSelectedIndices());
943void DungeonObjectInteraction::SendSelectedToBack() {
944 entity_coordinator_.tile_handler().SendToBack(
945 current_room_id_, selection_.GetSelectedIndices());
948void DungeonObjectInteraction::BringSelectedForward() {
949 entity_coordinator_.tile_handler().MoveForward(
950 current_room_id_, selection_.GetSelectedIndices());
953void DungeonObjectInteraction::SendSelectedBackward() {
954 entity_coordinator_.tile_handler().MoveBackward(
955 current_room_id_, selection_.GetSelectedIndices());
958void DungeonObjectInteraction::HandleLayerKeyboardShortcuts() {
960 if (!selection_.HasSelection())
964 if (ImGui::IsAnyItemActive())
969 if (ImGui::IsKeyPressed(ImGuiKey_1)) {
970 SendSelectedToLayer(0);
971 }
else if (ImGui::IsKeyPressed(ImGuiKey_2)) {
972 SendSelectedToLayer(1);
973 }
else if (ImGui::IsKeyPressed(ImGuiKey_3)) {
974 SendSelectedToLayer(2);
980 auto& io = ImGui::GetIO();
981 if (io.KeyCtrl && io.KeyShift) {
982 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
983 SendSelectedToFront();
984 }
else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
985 SendSelectedToBack();
987 }
else if (io.KeyCtrl) {
988 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
989 BringSelectedForward();
990 }
else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
991 SendSelectedBackward();
1000void DungeonObjectInteraction::SetDoorPlacementMode(
bool enabled,
1003 mode_manager_.SetMode(InteractionMode::PlaceDoor);
1004 entity_coordinator_.door_handler().SetDoorType(type);
1005 entity_coordinator_.door_handler().BeginPlacement();
1007 entity_coordinator_.door_handler().CancelPlacement();
1008 if (mode_manager_.GetMode() == InteractionMode::PlaceDoor)
1009 mode_manager_.SetMode(InteractionMode::Select);
1017void DungeonObjectInteraction::SetSpritePlacementMode(
bool enabled,
1018 uint8_t sprite_id) {
1020 mode_manager_.SetMode(InteractionMode::PlaceSprite);
1021 entity_coordinator_.sprite_handler().SetSpriteId(sprite_id);
1022 entity_coordinator_.sprite_handler().BeginPlacement();
1024 entity_coordinator_.sprite_handler().CancelPlacement();
1025 if (mode_manager_.GetMode() == InteractionMode::PlaceSprite)
1026 mode_manager_.SetMode(InteractionMode::Select);
1034void DungeonObjectInteraction::SetItemPlacementMode(
bool enabled,
1037 mode_manager_.SetMode(InteractionMode::PlaceItem);
1038 entity_coordinator_.item_handler().SetItemId(item_id);
1039 entity_coordinator_.item_handler().BeginPlacement();
1041 entity_coordinator_.item_handler().CancelPlacement();
1042 if (mode_manager_.GetMode() == InteractionMode::PlaceItem)
1043 mode_manager_.SetMode(InteractionMode::Select);
1051void DungeonObjectInteraction::SelectEntity(
EntityType type,
size_t index) {
1052 selection_.ClearSelection();
1053 entity_coordinator_.SelectEntity(type, index);
1056void DungeonObjectInteraction::ClearEntitySelection() {
1057 entity_coordinator_.ClearEntitySelection();
1060void DungeonObjectInteraction::CancelPlacement() {
1061 entity_coordinator_.CancelPlacement();
1062 if (mode_manager_.IsPlacementActive()) {
1063 mode_manager_.SetMode(InteractionMode::Select);
1067void DungeonObjectInteraction::DrawEntitySelectionHighlights() {
1068 entity_coordinator_.DrawSelectionHighlights();
1069 entity_coordinator_.DrawPostPlacementOverlays();
1072void DungeonObjectInteraction::DrawDoorSnapIndicators() {
DungeonCanvasTransform GetCanvasTransform() const
InteractionContext interaction_context_
void HandleCanvasMouseInput()
void HandleObjectSelectionStart(const ImVec2 &canvas_mouse_pos)
InteractionCoordinator entity_coordinator_
ObjectSelection selection_
void HandleMouseRelease()
void HandleLayerKeyboardShortcuts()
void HandleLeftClick(const ImVec2 &canvas_mouse_pos)
void UpdateWaterFillPainting(const ImVec2 &canvas_mouse_pos)
bool HasEntitySelection() const
std::pair< int, int > CanvasToRoomCoordinates(int canvas_x, int canvas_y) const
void HandleEmptySpaceClick(const ImVec2 &canvas_mouse_pos)
DungeonRoomStore * rooms_
bool HandleKeyboardNudge()
InteractionModeManager mode_manager_
void UpdateCollisionPainting(const ImVec2 &canvas_mouse_pos)
bool IsPlacementActive() const
Check if any placement mode is active.
bool HandleClick(int canvas_x, int canvas_y)
Handle click at canvas position.
bool HandleMouseWheel(float delta)
bool HasEntitySelection() const
void HandleDrag(ImVec2 current_pos, ImVec2 delta)
Handle drag operation.
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
bool IsRectangleSelectionActive() const
Check if a rectangle selection is in progress.
bool HasSelection() const
Check if any objects are selected.
bool IsMouseHovering() const
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
DimensionResult GetDimensions(const RoomObject &obj) const
std::pair< int, int > GetPixelDimensions(const RoomObject &obj) const
#define ICON_MD_DRAG_INDICATOR
#define ICON_MD_TOUCH_APP
constexpr int kRoomPixelMax
uint16_t EncodePotItemPosition(int pixel_x, int pixel_y)
bool IsWithinBounds(int canvas_x, int canvas_y, int margin=0)
Check if coordinates are within room bounds.
void ForEachPointInSquareBrush(int cx, int cy, int radius, int min_x, int min_y, int max_x, int max_y, Fn &&fn)
void ForEachPointOnLine(int x0, int y0, int x1, int y1, Fn &&fn)
Editors are the view controllers for the application.
EntityType
Type of entity that can be selected in the dungeon editor.
DoorType
Door types from ALTTP.
int GetObjectSubtype(int object_id)
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
void NotifyInvalidateCache(MutationDomain domain=MutationDomain::kUnknown) const
Notify that cache invalidation is needed.
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.
Represents a selected entity in the dungeon editor.