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