yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_interaction.cc
Go to the documentation of this file.
1// Related header
3#include "absl/strings/str_format.h"
5
6// C++ standard library headers
7#include <algorithm>
8#include <cmath>
9
10// Third-party library headers
11#include "imgui/imgui.h"
12
13// Project headers
18#include "app/gui/core/icons.h"
21
22namespace yaze::editor {
23
25 const ImGuiIO& io = ImGui::GetIO();
26 const bool hovered = canvas_->IsMouseHovering();
27 const bool mouse_left_down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
28 const bool mouse_left_released =
29 ImGui::IsMouseReleased(ImGuiMouseButton_Left);
30
31 // Keep processing drag/release if an interaction started on the canvas but
32 // the cursor left the bounds before the mouse button was released.
33 const bool has_active_marquee = selection_.IsRectangleSelectionActive();
34 const bool should_process_without_hover =
35 has_active_marquee ||
38 (mouse_left_down || mouse_left_released)) ||
39 mouse_left_released;
40
41 if (!hovered && !should_process_without_hover) {
42 return;
43 }
44
45 // Handle Escape key to cancel any active placement mode
46 if (ImGui::IsKeyPressed(ImGuiKey_Escape) &&
49 return;
50 }
51
52 if (hovered) {
53 if (entity_coordinator_.HandleMouseWheel(io.MouseWheel)) {
54 return;
55 }
57 }
58
59 ImVec2 mouse_pos = io.MousePos;
60 ImVec2 canvas_pos = canvas_->zero_point();
61 ImVec2 canvas_mouse_pos =
62 ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
63
64 // Painting modes are exclusive; don't also select/drag/mutate entities.
65 if (hovered && mouse_left_down) {
66 const auto mode = mode_manager_.GetMode();
68 UpdateCollisionPainting(canvas_mouse_pos);
69 return;
70 }
72 UpdateWaterFillPainting(canvas_mouse_pos);
73 return;
74 }
75 }
76
77 if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
78 HandleLeftClick(canvas_mouse_pos);
79 }
80
81 // Dispatch drag to coordinator (handlers gate internally via drag state).
82 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
83 entity_coordinator_.HandleDrag(canvas_mouse_pos, io.MouseDelta);
84 }
85
86 // Handle mouse release - complete drag operation
87 if (mouse_left_released) {
89 }
90}
91
92void DungeonObjectInteraction::HandleLeftClick(const ImVec2& canvas_mouse_pos) {
93 int canvas_x = static_cast<int>(canvas_mouse_pos.x);
94 int canvas_y = static_cast<int>(canvas_mouse_pos.y);
95
96 // Try to handle click via entity coordinator (handles placement, entity selection, and object selection)
97 if (entity_coordinator_.HandleClick(canvas_x, canvas_y)) {
98 // If an object selection just started, transition to dragging mode if applicable
100 HandleObjectSelectionStart(canvas_mouse_pos);
101 }
102 return;
103 }
104
105 // Not an entity click or placement; handle empty space
106 HandleEmptySpaceClick(canvas_mouse_pos);
107}
108
110 const ImVec2& canvas_mouse_pos) {
111 auto [room_x, room_y] =
112 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
113 static_cast<int>(canvas_mouse_pos.y));
114 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
115 auto& room = (*rooms_)[current_room_id_];
116 auto& state = mode_manager_.GetModeState();
117
118 // Only set for valid interior tiles (0-63)
119 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
120 // Start a paint stroke (single undo snapshot per stroke).
121 if (!state.is_painting) {
122 state.is_painting = true;
123 state.paint_mutation_started = false;
124 state.paint_last_tile_x = room_x;
125 state.paint_last_tile_y = room_y;
126 }
127
128 const int x0 =
129 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
130 const int y0 =
131 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
132
133 bool changed = false;
134 auto ensure_mutation = [&]() {
135 if (!state.paint_mutation_started) {
137 state.paint_mutation_started = true;
138 }
139 };
140
142 x0, y0, room_x, room_y, [&](int lx, int ly) {
144 lx, ly, state.paint_brush_radius,
145 /*min_x=*/0, /*min_y=*/0, /*max_x=*/63, /*max_y=*/63,
146 [&](int bx, int by) {
147 if (room.GetCollisionTile(bx, by) ==
148 state.paint_collision_value) {
149 return;
150 }
151 ensure_mutation();
152 room.SetCollisionTile(bx, by, state.paint_collision_value);
153 changed = true;
154 });
155 });
156
157 if (changed) {
160 }
161
162 state.paint_last_tile_x = room_x;
163 state.paint_last_tile_y = room_y;
164 }
165 }
166}
167
168void DungeonObjectInteraction::UpdateWaterFillPainting(
169 const ImVec2& canvas_mouse_pos) {
170 const ImGuiIO& io = ImGui::GetIO();
171 const bool erase = io.KeyAlt;
172
173 auto [room_x, room_y] =
174 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
175 static_cast<int>(canvas_mouse_pos.y));
176 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
177 auto& room = (*rooms_)[current_room_id_];
178 auto& state = mode_manager_.GetModeState();
179
180 // Only set for valid interior tiles (0-63)
181 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
182 const bool new_val = !erase;
183 // Start a paint stroke (single undo snapshot per stroke).
184 if (!state.is_painting) {
185 state.is_painting = true;
186 state.paint_mutation_started = false;
187 state.paint_last_tile_x = room_x;
188 state.paint_last_tile_y = room_y;
189 }
190
191 const int x0 =
192 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
193 const int y0 =
194 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
195
196 bool changed = false;
197 auto ensure_mutation = [&]() {
198 if (!state.paint_mutation_started) {
199 interaction_context_.NotifyMutation(MutationDomain::kWaterFill);
200 state.paint_mutation_started = true;
201 }
202 };
203
204 paint_util::ForEachPointOnLine(
205 x0, y0, room_x, room_y, [&](int lx, int ly) {
206 paint_util::ForEachPointInSquareBrush(
207 lx, ly, state.paint_brush_radius,
208 /*min_x=*/0, /*min_y=*/0, /*max_x=*/63, /*max_y=*/63,
209 [&](int bx, int by) {
210 if (room.GetWaterFillTile(bx, by) == new_val) {
211 return;
212 }
213 ensure_mutation();
214 room.SetWaterFillTile(bx, by, new_val);
215 changed = true;
216 });
217 });
218
219 if (changed) {
220 interaction_context_.NotifyInvalidateCache(MutationDomain::kWaterFill);
221 }
222
223 state.paint_last_tile_x = room_x;
224 state.paint_last_tile_y = room_y;
225 }
226 }
227}
228
229void DungeonObjectInteraction::HandleObjectSelectionStart(
230 const ImVec2& canvas_mouse_pos) {
231 ClearEntitySelection();
232 if (selection_.HasSelection()) {
233 mode_manager_.SetMode(InteractionMode::DraggingObjects);
234 entity_coordinator_.tile_handler().InitDrag(canvas_mouse_pos);
235 }
236}
237
238void DungeonObjectInteraction::HandleEmptySpaceClick(
239 const ImVec2& canvas_mouse_pos) {
240 const ImGuiIO& io = ImGui::GetIO();
241 const bool additive = io.KeyShift || io.KeyCtrl || io.KeySuper;
242
243 // Tile-object marquee selection is exclusive of door/sprite/item selection.
244 ClearEntitySelection();
245
246 // Clear existing object selection unless modifier held (Shift/Ctrl/Cmd).
247 if (!additive) {
248 selection_.ClearSelection();
249 }
250
251 // Always start a marquee selection drag on empty space; click-release without
252 // dragging behaves like a normal "clear selection" click.
253 entity_coordinator_.tile_handler().BeginMarqueeSelection(canvas_mouse_pos);
254}
255
256void DungeonObjectInteraction::HandleMouseRelease() {
257 {
258 // End paint strokes on mouse release so a new left-drag creates a new undo
259 // snapshot. Keep the paint mode active (tool stays selected).
260 const auto mode = mode_manager_.GetMode();
261 if (mode == InteractionMode::PaintCollision ||
262 mode == InteractionMode::PaintWaterFill) {
263 auto& state = mode_manager_.GetModeState();
264 const bool had_mutation = state.paint_mutation_started;
265 state.is_painting = false;
266 state.paint_mutation_started = false;
267 state.paint_last_tile_x = -1;
268 state.paint_last_tile_y = -1;
269 // Emit a final invalidation after the stroke ends so domain-specific undo
270 // capture can finalize the action once we're no longer "painting".
271 if (had_mutation) {
272 interaction_context_.NotifyInvalidateCache(
273 (mode == InteractionMode::PaintCollision)
274 ? MutationDomain::kCustomCollision
275 : MutationDomain::kWaterFill);
276 }
277 }
278 }
279
280 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
281 mode_manager_.SetMode(InteractionMode::Select);
282 }
283 entity_coordinator_.HandleRelease();
284 // Marquee selection finalization is handled by TileObjectHandler via
285 // CheckForObjectSelection().
286}
287
288void DungeonObjectInteraction::CheckForObjectSelection() {
289 // Draw/update active marquee selection for tile objects (delegated).
290 const ImGuiIO& io = ImGui::GetIO();
291 const ImVec2 canvas_pos = canvas_->zero_point();
292 const ImVec2 mouse_pos =
293 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
294
295 entity_coordinator_.tile_handler().HandleMarqueeSelection(
296 mouse_pos,
297 /*mouse_left_down=*/ImGui::IsMouseDown(ImGuiMouseButton_Left),
298 /*mouse_left_released=*/ImGui::IsMouseReleased(ImGuiMouseButton_Left),
299 /*shift_down=*/io.KeyShift,
300 /*toggle_down=*/io.KeyCtrl || io.KeySuper,
301 /*alt_down=*/io.KeyAlt);
302}
303
304void DungeonObjectInteraction::DrawSelectionHighlights() {
305 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
306 return;
307
308 auto& room = (*rooms_)[current_room_id_];
309 const auto& objects = room.GetTileObjects();
310
311 // Use ObjectSelection's rendering (handles pulsing border, corner handles)
312 selection_.DrawSelectionHighlights(
313 canvas_, objects, [](const zelda3::RoomObject& obj) {
314 auto result = zelda3::DimensionService::Get().GetDimensions(obj);
315 return std::make_tuple(result.offset_x_tiles * 8,
316 result.offset_y_tiles * 8, result.width_pixels(),
317 result.height_pixels());
318 });
319
320 // Enhanced hover tooltip showing object info (always visible on hover)
321 // Skip completely in exclusive entity mode (door/sprite/item selected)
322 if (entity_coordinator_.HasEntitySelection()) {
323 return; // Entity mode active - no object tooltips or hover
324 }
325
326 if (canvas_->IsMouseHovering()) {
327 // Also skip tooltip if cursor is over a door/sprite/item entity (not selected yet)
328 ImGuiIO& io = ImGui::GetIO();
329 ImVec2 canvas_pos = canvas_->zero_point();
330 int cursor_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
331 int cursor_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
332 auto entity_at_cursor =
333 entity_coordinator_.GetEntityAtPosition(cursor_x, cursor_y);
334 if (entity_at_cursor.has_value()) {
335 // Entity has priority - skip object tooltip, DrawHoverHighlight will also skip
336 DrawHoverHighlight(objects);
337 return;
338 }
339
340 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(
341 cursor_x, cursor_y);
342 if (hovered_index.has_value() && *hovered_index < objects.size()) {
343 const auto& object = objects[*hovered_index];
344 std::string object_name = zelda3::GetObjectName(object.id_);
345 int subtype = zelda3::GetObjectSubtype(object.id_);
346 int layer = object.GetLayerValue();
347
348 // Get subtype name
349 const char* subtype_names[] = {"Unknown", "Type 1", "Type 2", "Type 3"};
350 const char* subtype_name =
351 (subtype >= 0 && subtype <= 3) ? subtype_names[subtype] : "Unknown";
352
353 // Build informative tooltip
354 std::string tooltip;
355 tooltip += object_name;
356 tooltip += " (" + std::string(subtype_name) + ")";
357 tooltip += "\n";
358 tooltip += "ID: 0x" + absl::StrFormat("%03X", object.id_);
359 tooltip += " | Layer: " + std::to_string(layer + 1);
360 tooltip += " | Pos: (" + std::to_string(object.x_) + ", " +
361 std::to_string(object.y_) + ")";
362 tooltip += "\nSize: " + std::to_string(object.size_) + " (0x" +
363 absl::StrFormat("%02X", object.size_) + ")";
364
365 if (selection_.IsObjectSelected(*hovered_index)) {
366 tooltip += "\n" ICON_MD_MOUSE " Scroll wheel to resize";
367 tooltip += "\n" ICON_MD_DRAG_INDICATOR " Drag to move";
368 } else {
369 tooltip += "\n" ICON_MD_TOUCH_APP " Click to select";
370 }
371
372 ImGui::SetTooltip("%s", tooltip.c_str());
373 }
374 }
375
376 // Draw hover highlight for non-selected objects
377 DrawHoverHighlight(objects);
378}
379
380void DungeonObjectInteraction::DrawHoverHighlight(
381 const std::vector<zelda3::RoomObject>& objects) {
382 if (!canvas_->IsMouseHovering())
383 return;
384
385 // Skip all object hover in exclusive entity mode (door/sprite/item selected)
386 if (entity_coordinator_.HasEntitySelection())
387 return;
388
389 // Don't show object hover highlight if cursor is over a door/sprite/item entity
390 // Entities take priority over objects for interaction
391 ImGuiIO& io = ImGui::GetIO();
392 ImVec2 canvas_pos = canvas_->zero_point();
393 int cursor_canvas_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
394 int cursor_canvas_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
395 auto entity_at_cursor =
396 entity_coordinator_.GetEntityAtPosition(cursor_canvas_x, cursor_canvas_y);
397 if (entity_at_cursor.has_value()) {
398 return; // Entity has priority - skip object hover highlight
399 }
400
401 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(
402 cursor_canvas_x, cursor_canvas_y);
403 if (!hovered_index.has_value() || *hovered_index >= objects.size()) {
404 return;
405 }
406 const auto& object = objects[*hovered_index];
407
408 // Don't draw hover highlight if object is already selected
409 if (selection_.IsObjectSelected(*hovered_index)) {
410 return;
411 }
412
413 const auto& theme = AgentUI::GetTheme();
414 ImDrawList* draw_list = ImGui::GetWindowDrawList();
415 // canvas_pos already defined above for entity check
416 float scale = canvas_->global_scale();
417
418 // Calculate object position and dimensions
419 auto [sel_x_px, sel_y_px, pixel_width, pixel_height] =
421
422 // Apply scale and canvas offset
423 ImVec2 obj_start(canvas_pos.x + sel_x_px * scale,
424 canvas_pos.y + sel_y_px * scale);
425 ImVec2 obj_end(obj_start.x + pixel_width * scale,
426 obj_start.y + pixel_height * scale);
427
428 // Expand slightly for visibility
429 constexpr float margin = 2.0f;
430 obj_start.x -= margin;
431 obj_start.y -= margin;
432 obj_end.x += margin;
433 obj_end.y += margin;
434
435 // Draw subtle hover highlight with unified theme color
436 ImVec4 hover_fill = theme.selection_hover;
437 hover_fill.w *= 0.5f; // Make it more subtle for hover fill
438
439 ImVec4 hover_border = theme.selection_hover;
440
441 // Draw filled background for better visibility
442 draw_list->AddRectFilled(obj_start, obj_end, ImGui::GetColorU32(hover_fill));
443
444 // Draw dashed-style border (simulated with thinner line)
445 draw_list->AddRect(obj_start, obj_end, ImGui::GetColorU32(hover_border), 0.0f,
446 0, 1.5f);
447}
448
449void DungeonObjectInteraction::PlaceObjectAtPosition(int room_x, int room_y) {
450 entity_coordinator_.tile_handler().PlaceObjectAt(
451 current_room_id_, preview_object_, room_x, room_y);
452
453 if (object_placed_callback_) {
454 object_placed_callback_(preview_object_);
455 }
456
457 interaction_context_.NotifyInvalidateCache(MutationDomain::kTileObjects);
458 CancelPlacement();
459}
460
461std::pair<int, int> DungeonObjectInteraction::RoomToCanvasCoordinates(
462 int room_x, int room_y) const {
463 // Dungeon tiles are 8x8 pixels, convert room coordinates (tiles) to pixels
464 return {room_x * 8, room_y * 8};
465}
466
467std::pair<int, int> DungeonObjectInteraction::CanvasToRoomCoordinates(
468 int canvas_x, int canvas_y) const {
469 // Convert canvas pixels back to room coordinates (tiles)
470 return {canvas_x / 8, canvas_y / 8};
471}
472
473bool DungeonObjectInteraction::IsWithinCanvasBounds(int canvas_x, int canvas_y,
474 int margin) const {
475 auto canvas_size = canvas_->canvas_size();
476 auto global_scale = canvas_->global_scale();
477 int scaled_width = static_cast<int>(canvas_size.x * global_scale);
478 int scaled_height = static_cast<int>(canvas_size.y * global_scale);
479
480 return (canvas_x >= -margin && canvas_y >= -margin &&
481 canvas_x <= scaled_width + margin &&
482 canvas_y <= scaled_height + margin);
483}
484
485void DungeonObjectInteraction::SetCurrentRoom(DungeonRoomStore* rooms,
486 int room_id) {
487 rooms_ = rooms;
488 current_room_id_ = room_id;
489 interaction_context_.rooms = rooms;
490 interaction_context_.current_room_id = room_id;
491 interaction_context_.selection = &selection_;
492 entity_coordinator_.SetContext(&interaction_context_);
493}
494
495void DungeonObjectInteraction::SetPreviewObject(
496 const zelda3::RoomObject& object, bool loaded) {
497 preview_object_ = object;
498
499 if (loaded && object.id_ >= 0) {
500 // Cancel other placement modes (doors/sprites/items) before entering object
501 // placement. We re-enable tile placement below.
502 entity_coordinator_.CancelPlacement();
503
504 // Enter object placement mode
505 mode_manager_.SetMode(InteractionMode::PlaceObject);
506 mode_manager_.GetModeState().preview_object = object;
507
508 // Ensure tile placement mode is active so ghost preview can render and
509 // clicks place the object.
510 auto& tile_handler = entity_coordinator_.tile_handler();
511 tile_handler.SetPreviewObject(preview_object_);
512 if (!tile_handler.IsPlacementActive()) {
513 tile_handler.BeginPlacement();
514 }
515 } else {
516 // Exit placement mode if not loaded
517 if (mode_manager_.GetMode() == InteractionMode::PlaceObject) {
518 CancelPlacement();
519 }
520 }
521}
522
523void DungeonObjectInteraction::ClearSelection() {
524 selection_.ClearSelection();
525 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
526 mode_manager_.SetMode(InteractionMode::Select);
527 }
528}
529
530void DungeonObjectInteraction::HandleDeleteSelected() {
531 auto indices = selection_.GetSelectedIndices();
532 if (!indices.empty()) {
533 entity_coordinator_.tile_handler().DeleteObjects(current_room_id_, indices);
534 selection_.ClearSelection();
535 }
536
537 if (entity_coordinator_.HasEntitySelection()) {
538 entity_coordinator_.DeleteSelectedEntity();
539 }
540}
541
542void DungeonObjectInteraction::HandleDeleteAllObjects() {
543 entity_coordinator_.tile_handler().DeleteAllObjects(current_room_id_);
544 selection_.ClearSelection();
545}
546
547void DungeonObjectInteraction::HandleCopySelected() {
548 entity_coordinator_.tile_handler().CopyObjectsToClipboard(
549 current_room_id_, selection_.GetSelectedIndices());
550}
551
552void DungeonObjectInteraction::HandlePasteObjects() {
553 auto& handler = entity_coordinator_.tile_handler();
554 if (!handler.HasClipboardData())
555 return;
556
557 const ImGuiIO& io = ImGui::GetIO();
558 ImVec2 canvas_mouse_pos = ImVec2(io.MousePos.x - canvas_->zero_point().x,
559 io.MousePos.y - canvas_->zero_point().y);
560 auto [paste_x, paste_y] =
561 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
562 static_cast<int>(canvas_mouse_pos.y));
563
564 auto new_indices =
565 handler.PasteFromClipboardAt(current_room_id_, paste_x, paste_y);
566
567 // Select the newly pasted objects
568 if (!new_indices.empty()) {
569 selection_.ClearSelection();
570 for (size_t idx : new_indices) {
571 selection_.SelectObject(idx, ObjectSelection::SelectionMode::Add);
572 }
573 }
574}
575
576void DungeonObjectInteraction::DrawGhostPreview() {
577 entity_coordinator_.DrawGhostPreviews();
578}
579
580void DungeonObjectInteraction::HandleScrollWheelResize() {
581 const ImGuiIO& io = ImGui::GetIO();
582 entity_coordinator_.HandleMouseWheel(io.MouseWheel);
583}
584
585bool DungeonObjectInteraction::SetObjectId(size_t index, int16_t id) {
586 entity_coordinator_.tile_handler().UpdateObjectsId(current_room_id_, {index},
587 id);
588 return true;
589}
590
591bool DungeonObjectInteraction::SetObjectSize(size_t index, uint8_t size) {
592 entity_coordinator_.tile_handler().UpdateObjectsSize(current_room_id_,
593 {index}, size);
594 return true;
595}
596
597bool DungeonObjectInteraction::SetObjectLayer(
598 size_t index, zelda3::RoomObject::LayerType layer) {
599 entity_coordinator_.tile_handler().UpdateObjectsLayer(
600 current_room_id_, {index}, static_cast<int>(layer));
601 return true;
602}
603
604std::pair<int, int> DungeonObjectInteraction::CalculateObjectBounds(
605 const zelda3::RoomObject& object) {
607}
608
609void DungeonObjectInteraction::SendSelectedToLayer(int target_layer) {
610 entity_coordinator_.tile_handler().UpdateObjectsLayer(
611 current_room_id_, selection_.GetSelectedIndices(), target_layer);
612}
613
614void DungeonObjectInteraction::SendSelectedToFront() {
615 entity_coordinator_.tile_handler().SendToFront(
616 current_room_id_, selection_.GetSelectedIndices());
617}
618
619void DungeonObjectInteraction::SendSelectedToBack() {
620 entity_coordinator_.tile_handler().SendToBack(
621 current_room_id_, selection_.GetSelectedIndices());
622}
623
624void DungeonObjectInteraction::BringSelectedForward() {
625 entity_coordinator_.tile_handler().MoveForward(
626 current_room_id_, selection_.GetSelectedIndices());
627}
628
629void DungeonObjectInteraction::SendSelectedBackward() {
630 entity_coordinator_.tile_handler().MoveBackward(
631 current_room_id_, selection_.GetSelectedIndices());
632}
633
634void DungeonObjectInteraction::HandleLayerKeyboardShortcuts() {
635 // Only process if we have selected objects
636 if (!selection_.HasSelection())
637 return;
638
639 // Only when not typing in a text field
640 if (ImGui::IsAnyItemActive())
641 return;
642
643 // Check for layer assignment shortcuts (1, 2, 3 keys)
644 if (ImGui::IsKeyPressed(ImGuiKey_1)) {
645 SendSelectedToLayer(0); // Layer 1 (BG1)
646 } else if (ImGui::IsKeyPressed(ImGuiKey_2)) {
647 SendSelectedToLayer(1); // Layer 2 (BG2)
648 } else if (ImGui::IsKeyPressed(ImGuiKey_3)) {
649 SendSelectedToLayer(2); // Layer 3 (BG3)
650 }
651
652 // Object ordering shortcuts
653 // Ctrl+Shift+] = Bring to Front, Ctrl+Shift+[ = Send to Back
654 // Ctrl+] = Bring Forward, Ctrl+[ = Send Backward
655 auto& io = ImGui::GetIO();
656 if (io.KeyCtrl && io.KeyShift) {
657 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
658 SendSelectedToFront();
659 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
660 SendSelectedToBack();
661 }
662 } else if (io.KeyCtrl) {
663 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
664 BringSelectedForward();
665 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
666 SendSelectedBackward();
667 }
668 }
669}
670
671// ============================================================================
672// Door Placement Methods
673// ============================================================================
674
675void DungeonObjectInteraction::SetDoorPlacementMode(bool enabled,
676 zelda3::DoorType type) {
677 if (enabled) {
678 mode_manager_.SetMode(InteractionMode::PlaceDoor);
679 entity_coordinator_.door_handler().SetDoorType(type);
680 entity_coordinator_.door_handler().BeginPlacement();
681 } else {
682 entity_coordinator_.door_handler().CancelPlacement();
683 if (mode_manager_.GetMode() == InteractionMode::PlaceDoor)
684 mode_manager_.SetMode(InteractionMode::Select);
685 }
686}
687
688// ============================================================================
689// Sprite Placement Methods
690// ============================================================================
691
692void DungeonObjectInteraction::SetSpritePlacementMode(bool enabled,
693 uint8_t sprite_id) {
694 if (enabled) {
695 mode_manager_.SetMode(InteractionMode::PlaceSprite);
696 entity_coordinator_.sprite_handler().SetSpriteId(sprite_id);
697 entity_coordinator_.sprite_handler().BeginPlacement();
698 } else {
699 entity_coordinator_.sprite_handler().CancelPlacement();
700 if (mode_manager_.GetMode() == InteractionMode::PlaceSprite)
701 mode_manager_.SetMode(InteractionMode::Select);
702 }
703}
704
705// ============================================================================
706// Item Placement Methods
707// ============================================================================
708
709void DungeonObjectInteraction::SetItemPlacementMode(bool enabled,
710 uint8_t item_id) {
711 if (enabled) {
712 mode_manager_.SetMode(InteractionMode::PlaceItem);
713 entity_coordinator_.item_handler().SetItemId(item_id);
714 entity_coordinator_.item_handler().BeginPlacement();
715 } else {
716 entity_coordinator_.item_handler().CancelPlacement();
717 if (mode_manager_.GetMode() == InteractionMode::PlaceItem)
718 mode_manager_.SetMode(InteractionMode::Select);
719 }
720}
721
722// ============================================================================
723// Entity Selection Methods (Doors, Sprites, Items)
724// ============================================================================
725
726void DungeonObjectInteraction::SelectEntity(EntityType type, size_t index) {
727 entity_coordinator_.SelectEntity(type, index);
728}
729
730void DungeonObjectInteraction::ClearEntitySelection() {
731 entity_coordinator_.ClearEntitySelection();
732}
733
734void DungeonObjectInteraction::CancelPlacement() {
735 entity_coordinator_.CancelPlacement();
736 if (mode_manager_.IsPlacementActive()) {
737 mode_manager_.SetMode(InteractionMode::Select);
738 }
739}
740
741void DungeonObjectInteraction::DrawEntitySelectionHighlights() {
742 entity_coordinator_.DrawSelectionHighlights();
743 entity_coordinator_.DrawPostPlacementOverlays();
744}
745
746void DungeonObjectInteraction::DrawDoorSnapIndicators() {
747 // Door snap indicators are now managed by DoorInteractionHandler
748 // through the entity coordinator. No-op here for backward compatibility.
749}
750
751} // namespace yaze::editor
void HandleObjectSelectionStart(const ImVec2 &canvas_mouse_pos)
void HandleLeftClick(const ImVec2 &canvas_mouse_pos)
void UpdateWaterFillPainting(const ImVec2 &canvas_mouse_pos)
std::pair< int, int > CanvasToRoomCoordinates(int canvas_x, int canvas_y) const
void HandleEmptySpaceClick(const ImVec2 &canvas_mouse_pos)
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.
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.
auto zero_point() const
Definition canvas.h:443
bool IsMouseHovering() const
Definition canvas.h:433
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
Definition icons.h:624
#define ICON_MD_TOUCH_APP
Definition icons.h:2000
#define ICON_MD_MOUSE
Definition icons.h:1251
void ForEachPointInSquareBrush(int cx, int cy, int radius, int min_x, int min_y, int max_x, int max_y, Fn &&fn)
Definition paint_util.h:40
void ForEachPointOnLine(int x0, int y0, int x1, int y1, Fn &&fn)
Definition paint_util.h:13
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.
Definition door_types.h:33
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.