3#include "absl/strings/str_format.h"
10#include "imgui/imgui.h"
24 const ImGuiIO& io = ImGui::GetIO();
26 const bool mouse_left_down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
27 const bool mouse_left_released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
32 const bool should_process_without_hover =
36 (mouse_left_down || mouse_left_released)) ||
39 if (!hovered && !should_process_without_hover) {
44 if (ImGui::IsKeyPressed(ImGuiKey_Escape) &&
57 ImVec2 mouse_pos = io.MousePos;
59 ImVec2 canvas_mouse_pos =
60 ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
63 if (hovered && mouse_left_down) {
75 if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
80 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
85 if (mouse_left_released) {
91 int canvas_x =
static_cast<int>(canvas_mouse_pos.x);
92 int canvas_y =
static_cast<int>(canvas_mouse_pos.y);
109 static_cast<int>(canvas_mouse_pos.x),
static_cast<int>(canvas_mouse_pos.y));
115 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
117 if (!state.is_painting) {
119 state.paint_mutation_started =
false;
120 state.paint_last_tile_x = room_x;
121 state.paint_last_tile_y = room_y;
125 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
127 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
129 bool changed =
false;
130 auto ensure_mutation = [&]() {
131 if (!state.paint_mutation_started) {
133 state.paint_mutation_started =
true;
138 [&](
int lx,
int ly) {
140 lx, ly, state.paint_brush_radius,
142 [&](
int bx,
int by) {
143 if (room.GetCollisionTile(bx, by) == state.paint_collision_value) {
147 room.SetCollisionTile(bx, by, state.paint_collision_value);
156 state.paint_last_tile_x = room_x;
157 state.paint_last_tile_y = room_y;
162void DungeonObjectInteraction::UpdateWaterFillPainting(
163 const ImVec2& canvas_mouse_pos) {
164 const ImGuiIO& io = ImGui::GetIO();
165 const bool erase = io.KeyAlt;
167 auto [room_x, room_y] = CanvasToRoomCoordinates(
168 static_cast<int>(canvas_mouse_pos.x),
169 static_cast<int>(canvas_mouse_pos.y));
170 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
171 auto& room = (*rooms_)[current_room_id_];
172 auto& state = mode_manager_.GetModeState();
175 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
176 const bool new_val = !erase;
178 if (!state.is_painting) {
179 state.is_painting =
true;
180 state.paint_mutation_started =
false;
181 state.paint_last_tile_x = room_x;
182 state.paint_last_tile_y = room_y;
186 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
188 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
190 bool changed =
false;
191 auto ensure_mutation = [&]() {
192 if (!state.paint_mutation_started) {
193 interaction_context_.NotifyMutation(MutationDomain::kWaterFill);
194 state.paint_mutation_started =
true;
198 paint_util::ForEachPointOnLine(x0, y0, room_x, room_y,
199 [&](
int lx,
int ly) {
200 paint_util::ForEachPointInSquareBrush(
201 lx, ly, state.paint_brush_radius,
203 [&](
int bx,
int by) {
204 if (room.GetWaterFillTile(bx, by) == new_val) {
208 room.SetWaterFillTile(bx, by, new_val);
214 interaction_context_.NotifyInvalidateCache(MutationDomain::kWaterFill);
217 state.paint_last_tile_x = room_x;
218 state.paint_last_tile_y = room_y;
223void DungeonObjectInteraction::HandleObjectSelectionStart(
const ImVec2& canvas_mouse_pos) {
224 ClearEntitySelection();
225 if (selection_.HasSelection()) {
226 mode_manager_.SetMode(InteractionMode::DraggingObjects);
227 entity_coordinator_.tile_handler().InitDrag(canvas_mouse_pos);
231void DungeonObjectInteraction::HandleEmptySpaceClick(
const ImVec2& canvas_mouse_pos) {
232 const ImGuiIO& io = ImGui::GetIO();
233 const bool additive = io.KeyShift || io.KeyCtrl || io.KeySuper;
236 ClearEntitySelection();
240 selection_.ClearSelection();
245 entity_coordinator_.tile_handler().BeginMarqueeSelection(canvas_mouse_pos);
249void DungeonObjectInteraction::HandleMouseRelease() {
253 const auto mode = mode_manager_.GetMode();
254 if (mode == InteractionMode::PaintCollision ||
255 mode == InteractionMode::PaintWaterFill) {
256 auto& state = mode_manager_.GetModeState();
257 const bool had_mutation = state.paint_mutation_started;
258 state.is_painting =
false;
259 state.paint_mutation_started =
false;
260 state.paint_last_tile_x = -1;
261 state.paint_last_tile_y = -1;
265 interaction_context_.NotifyInvalidateCache(
266 (mode == InteractionMode::PaintCollision)
267 ? MutationDomain::kCustomCollision
268 : MutationDomain::kWaterFill);
273 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
274 mode_manager_.SetMode(InteractionMode::Select);
276 entity_coordinator_.HandleRelease();
281void DungeonObjectInteraction::CheckForObjectSelection() {
283 const ImGuiIO& io = ImGui::GetIO();
284 const ImVec2 canvas_pos = canvas_->zero_point();
285 const ImVec2 mouse_pos =
286 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
288 entity_coordinator_.tile_handler().HandleMarqueeSelection(
290 ImGui::IsMouseDown(ImGuiMouseButton_Left),
291 ImGui::IsMouseReleased(ImGuiMouseButton_Left),
293 io.KeyCtrl || io.KeySuper,
297void DungeonObjectInteraction::DrawSelectionHighlights() {
298 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
301 auto& room = (*rooms_)[current_room_id_];
302 const auto& objects = room.GetTileObjects();
305 selection_.DrawSelectionHighlights(
308 return std::make_tuple(result.offset_x_tiles * 8,
309 result.offset_y_tiles * 8,
310 result.width_pixels(), result.height_pixels());
315 if (entity_coordinator_.HasEntitySelection()) {
319 if (canvas_->IsMouseHovering()) {
321 ImGuiIO& io = ImGui::GetIO();
322 ImVec2 canvas_pos = canvas_->zero_point();
323 int cursor_x =
static_cast<int>(io.MousePos.x - canvas_pos.x);
324 int cursor_y =
static_cast<int>(io.MousePos.y - canvas_pos.y);
325 auto entity_at_cursor = entity_coordinator_.GetEntityAtPosition(cursor_x, cursor_y);
326 if (entity_at_cursor.has_value()) {
328 DrawHoverHighlight(objects);
332 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(cursor_x, cursor_y);
333 if (hovered_index.has_value() && *hovered_index < objects.size()) {
334 const auto&
object = objects[*hovered_index];
337 int layer =
object.GetLayerValue();
340 const char* subtype_names[] = {
"Unknown",
"Type 1",
"Type 2",
"Type 3"};
341 const char* subtype_name =
342 (subtype >= 0 && subtype <= 3) ? subtype_names[subtype] :
"Unknown";
346 tooltip += object_name;
347 tooltip +=
" (" + std::string(subtype_name) +
")";
349 tooltip +=
"ID: 0x" + absl::StrFormat(
"%03X",
object.id_);
350 tooltip +=
" | Layer: " + std::to_string(layer + 1);
351 tooltip +=
" | Pos: (" + std::to_string(
object.x_) +
", " +
352 std::to_string(
object.y_) +
")";
353 tooltip +=
"\nSize: " + std::to_string(
object.size_) +
" (0x" +
354 absl::StrFormat(
"%02X",
object.size_) +
")";
356 if (selection_.IsObjectSelected(*hovered_index)) {
363 ImGui::SetTooltip(
"%s", tooltip.c_str());
368 DrawHoverHighlight(objects);
371void DungeonObjectInteraction::DrawHoverHighlight(
372 const std::vector<zelda3::RoomObject>& objects) {
373 if (!canvas_->IsMouseHovering())
377 if (entity_coordinator_.HasEntitySelection())
382 ImGuiIO& io = ImGui::GetIO();
383 ImVec2 canvas_pos = canvas_->zero_point();
384 int cursor_canvas_x =
static_cast<int>(io.MousePos.x - canvas_pos.x);
385 int cursor_canvas_y =
static_cast<int>(io.MousePos.y - canvas_pos.y);
386 auto entity_at_cursor = entity_coordinator_.GetEntityAtPosition(cursor_canvas_x, cursor_canvas_y);
387 if (entity_at_cursor.has_value()) {
391 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(cursor_canvas_x, cursor_canvas_y);
392 if (!hovered_index.has_value() || *hovered_index >= objects.size()) {
395 const auto&
object = objects[*hovered_index];
398 if (selection_.IsObjectSelected(*hovered_index)) {
402 const auto& theme = AgentUI::GetTheme();
403 ImDrawList* draw_list = ImGui::GetWindowDrawList();
405 float scale = canvas_->global_scale();
408 auto [sel_x_px, sel_y_px, pixel_width, pixel_height] =
412 ImVec2 obj_start(canvas_pos.x + sel_x_px * scale,
413 canvas_pos.y + sel_y_px * scale);
414 ImVec2 obj_end(obj_start.x + pixel_width * scale,
415 obj_start.y + pixel_height * scale);
418 constexpr float margin = 2.0f;
419 obj_start.x -= margin;
420 obj_start.y -= margin;
425 ImVec4 hover_fill = theme.selection_hover;
426 hover_fill.w *= 0.5f;
428 ImVec4 hover_border = theme.selection_hover;
431 draw_list->AddRectFilled(obj_start, obj_end, ImGui::GetColorU32(hover_fill));
434 draw_list->AddRect(obj_start, obj_end, ImGui::GetColorU32(hover_border), 0.0f,
438void DungeonObjectInteraction::PlaceObjectAtPosition(
int room_x,
int room_y) {
439 entity_coordinator_.tile_handler().PlaceObjectAt(current_room_id_, preview_object_, room_x, room_y);
441 if (object_placed_callback_) {
442 object_placed_callback_(preview_object_);
445 interaction_context_.NotifyInvalidateCache(MutationDomain::kTileObjects);
449std::pair<int, int> DungeonObjectInteraction::RoomToCanvasCoordinates(
450 int room_x,
int room_y)
const {
452 return {room_x * 8, room_y * 8};
455std::pair<int, int> DungeonObjectInteraction::CanvasToRoomCoordinates(
456 int canvas_x,
int canvas_y)
const {
458 return {canvas_x / 8, canvas_y / 8};
461bool DungeonObjectInteraction::IsWithinCanvasBounds(
int canvas_x,
int canvas_y,
463 auto canvas_size = canvas_->canvas_size();
464 auto global_scale = canvas_->global_scale();
465 int scaled_width =
static_cast<int>(canvas_size.x * global_scale);
466 int scaled_height =
static_cast<int>(canvas_size.y * global_scale);
468 return (canvas_x >= -margin && canvas_y >= -margin &&
469 canvas_x <= scaled_width + margin &&
470 canvas_y <= scaled_height + margin);
473void DungeonObjectInteraction::SetCurrentRoom(
474 std::array<zelda3::Room, dungeon_coords::kRoomCount>* rooms,
int room_id) {
476 current_room_id_ = room_id;
477 interaction_context_.rooms = rooms;
478 interaction_context_.current_room_id = room_id;
479 interaction_context_.selection = &selection_;
480 entity_coordinator_.SetContext(&interaction_context_);
483void DungeonObjectInteraction::SetPreviewObject(
485 preview_object_ = object;
487 if (loaded &&
object.id_ >= 0) {
490 entity_coordinator_.CancelPlacement();
493 mode_manager_.SetMode(InteractionMode::PlaceObject);
494 mode_manager_.GetModeState().preview_object = object;
498 auto& tile_handler = entity_coordinator_.tile_handler();
499 tile_handler.SetPreviewObject(preview_object_);
500 if (!tile_handler.IsPlacementActive()) {
501 tile_handler.BeginPlacement();
505 if (mode_manager_.GetMode() == InteractionMode::PlaceObject) {
513void DungeonObjectInteraction::ClearSelection() {
514 selection_.ClearSelection();
515 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
516 mode_manager_.SetMode(InteractionMode::Select);
521void DungeonObjectInteraction::HandleDeleteSelected() {
522 auto indices = selection_.GetSelectedIndices();
523 if (!indices.empty()) {
524 entity_coordinator_.tile_handler().DeleteObjects(current_room_id_, indices);
525 selection_.ClearSelection();
528 if (entity_coordinator_.HasEntitySelection()) {
529 entity_coordinator_.DeleteSelectedEntity();
533void DungeonObjectInteraction::HandleDeleteAllObjects() {
534 entity_coordinator_.tile_handler().DeleteAllObjects(current_room_id_);
535 selection_.ClearSelection();
538void DungeonObjectInteraction::HandleCopySelected() {
539 entity_coordinator_.tile_handler().CopyObjectsToClipboard(
540 current_room_id_, selection_.GetSelectedIndices());
543void DungeonObjectInteraction::HandlePasteObjects() {
544 auto& handler = entity_coordinator_.tile_handler();
545 if (!handler.HasClipboardData())
return;
547 const ImGuiIO& io = ImGui::GetIO();
548 ImVec2 canvas_mouse_pos = ImVec2(io.MousePos.x - canvas_->zero_point().x,
549 io.MousePos.y - canvas_->zero_point().y);
550 auto [paste_x, paste_y] = CanvasToRoomCoordinates(
static_cast<int>(canvas_mouse_pos.x),
551 static_cast<int>(canvas_mouse_pos.y));
553 auto new_indices = handler.PasteFromClipboardAt(current_room_id_, paste_x, paste_y);
556 if (!new_indices.empty()) {
557 selection_.ClearSelection();
558 for (
size_t idx : new_indices) {
559 selection_.SelectObject(idx, ObjectSelection::SelectionMode::Add);
564void DungeonObjectInteraction::DrawGhostPreview() {
565 entity_coordinator_.DrawGhostPreviews();
568void DungeonObjectInteraction::HandleScrollWheelResize() {
569 const ImGuiIO& io = ImGui::GetIO();
570 entity_coordinator_.HandleMouseWheel(io.MouseWheel);
573bool DungeonObjectInteraction::SetObjectId(
size_t index, int16_t
id) {
574 entity_coordinator_.tile_handler().UpdateObjectsId(current_room_id_, {index}, id);
578bool DungeonObjectInteraction::SetObjectSize(
size_t index, uint8_t size) {
579 entity_coordinator_.tile_handler().UpdateObjectsSize(current_room_id_, {index}, size);
584 entity_coordinator_.tile_handler().UpdateObjectsLayer(current_room_id_, {index},
static_cast<int>(layer));
588std::pair<int, int> DungeonObjectInteraction::CalculateObjectBounds(
594void DungeonObjectInteraction::SendSelectedToLayer(
int target_layer) {
595 entity_coordinator_.tile_handler().UpdateObjectsLayer(
596 current_room_id_, selection_.GetSelectedIndices(), target_layer);
599void DungeonObjectInteraction::SendSelectedToFront() {
600 entity_coordinator_.tile_handler().SendToFront(current_room_id_, selection_.GetSelectedIndices());
603void DungeonObjectInteraction::SendSelectedToBack() {
604 entity_coordinator_.tile_handler().SendToBack(current_room_id_, selection_.GetSelectedIndices());
607void DungeonObjectInteraction::BringSelectedForward() {
608 entity_coordinator_.tile_handler().MoveForward(current_room_id_, selection_.GetSelectedIndices());
611void DungeonObjectInteraction::SendSelectedBackward() {
612 entity_coordinator_.tile_handler().MoveBackward(current_room_id_, selection_.GetSelectedIndices());
615void DungeonObjectInteraction::HandleLayerKeyboardShortcuts() {
617 if (!selection_.HasSelection())
621 if (ImGui::IsAnyItemActive())
625 if (ImGui::IsKeyPressed(ImGuiKey_1)) {
626 SendSelectedToLayer(0);
627 }
else if (ImGui::IsKeyPressed(ImGuiKey_2)) {
628 SendSelectedToLayer(1);
629 }
else if (ImGui::IsKeyPressed(ImGuiKey_3)) {
630 SendSelectedToLayer(2);
636 auto& io = ImGui::GetIO();
637 if (io.KeyCtrl && io.KeyShift) {
638 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
639 SendSelectedToFront();
640 }
else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
641 SendSelectedToBack();
643 }
else if (io.KeyCtrl) {
644 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
645 BringSelectedForward();
646 }
else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
647 SendSelectedBackward();
658 mode_manager_.SetMode(InteractionMode::PlaceDoor);
659 entity_coordinator_.door_handler().SetDoorType(type);
660 entity_coordinator_.door_handler().BeginPlacement();
662 entity_coordinator_.door_handler().CancelPlacement();
663 if (mode_manager_.GetMode() == InteractionMode::PlaceDoor) mode_manager_.SetMode(InteractionMode::Select);
673void DungeonObjectInteraction::SetSpritePlacementMode(
bool enabled, uint8_t sprite_id) {
675 mode_manager_.SetMode(InteractionMode::PlaceSprite);
676 entity_coordinator_.sprite_handler().SetSpriteId(sprite_id);
677 entity_coordinator_.sprite_handler().BeginPlacement();
679 entity_coordinator_.sprite_handler().CancelPlacement();
680 if (mode_manager_.GetMode() == InteractionMode::PlaceSprite) mode_manager_.SetMode(InteractionMode::Select);
690void DungeonObjectInteraction::SetItemPlacementMode(
bool enabled, uint8_t item_id) {
692 mode_manager_.SetMode(InteractionMode::PlaceItem);
693 entity_coordinator_.item_handler().SetItemId(item_id);
694 entity_coordinator_.item_handler().BeginPlacement();
696 entity_coordinator_.item_handler().CancelPlacement();
697 if (mode_manager_.GetMode() == InteractionMode::PlaceItem) mode_manager_.SetMode(InteractionMode::Select);
707void DungeonObjectInteraction::SelectEntity(
EntityType type,
size_t index) {
708 entity_coordinator_.SelectEntity(type, index);
711void DungeonObjectInteraction::ClearEntitySelection() {
712 entity_coordinator_.ClearEntitySelection();
715void DungeonObjectInteraction::CancelPlacement() {
716 entity_coordinator_.CancelPlacement();
717 if (mode_manager_.IsPlacementActive()) {
718 mode_manager_.SetMode(InteractionMode::Select);
722void DungeonObjectInteraction::DrawEntitySelectionHighlights() {
723 entity_coordinator_.DrawSelectionHighlights();
724 entity_coordinator_.DrawPostPlacementOverlays();
727void DungeonObjectInteraction::DrawDoorSnapIndicators() {
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)
std::array< zelda3::Room, dungeon_coords::kRoomCount > * rooms_
bool HasEntitySelection() const
std::pair< int, int > CanvasToRoomCoordinates(int canvas_x, int canvas_y) const
void HandleEmptySpaceClick(const ImVec2 &canvas_mouse_pos)
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
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)
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.