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"
22
23namespace yaze::editor {
24
25namespace {
26
27constexpr int kRoomPixelMax = 511;
28
29uint16_t EncodePotItemPosition(int pixel_x, int pixel_y) {
30 const int clamped_x = std::clamp(pixel_x, 0, kRoomPixelMax);
31 const int clamped_y = std::clamp(pixel_y, 0, kRoomPixelMax);
32 const int encoded_x = std::clamp(clamped_x / 4, 0, 255);
33 const int encoded_y = std::clamp(clamped_y / 16, 0, 255);
34 return static_cast<uint16_t>((encoded_y << 8) | encoded_x);
35}
36
37} // namespace
38
40 const ImGuiIO& io = ImGui::GetIO();
41 const bool hovered = canvas_->IsMouseHovering();
42 const bool mouse_left_down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
43 const bool mouse_left_released =
44 ImGui::IsMouseReleased(ImGuiMouseButton_Left);
45
46 // Keep processing drag/release if an interaction started on the canvas but
47 // the cursor left the bounds before the mouse button was released.
48 const bool has_active_marquee = selection_.IsRectangleSelectionActive();
49 const bool should_process_without_hover =
50 has_active_marquee ||
53 (mouse_left_down || mouse_left_released)) ||
54 mouse_left_released;
55
56 if (!hovered && !should_process_without_hover) {
57 return;
58 }
59
60 const ImVec2 canvas_mouse_pos =
62 const int canvas_mouse_x = static_cast<int>(std::floor(canvas_mouse_pos.x));
63 const int canvas_mouse_y = static_cast<int>(std::floor(canvas_mouse_pos.y));
64 const bool pointer_within_room =
65 dungeon_coords::IsWithinBounds(canvas_mouse_x, canvas_mouse_y);
66
67 // Handle Escape key to cancel any active placement mode
68 if (ImGui::IsKeyPressed(ImGuiKey_Escape) &&
71 return;
72 }
73
74 if (hovered) {
75 if (pointer_within_room &&
77 return;
78 }
80 if (HandleKeyboardNudge()) {
81 return;
82 }
83 }
84
85 // Painting modes are exclusive; don't also select/drag/mutate entities.
86 if (hovered && mouse_left_down) {
87 const auto mode = mode_manager_.GetMode();
89 if (pointer_within_room) {
90 UpdateCollisionPainting(canvas_mouse_pos);
91 }
92 return;
93 }
95 if (pointer_within_room) {
96 UpdateWaterFillPainting(canvas_mouse_pos);
97 }
98 return;
99 }
100 }
101
102 if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
103 HandleLeftClick(canvas_mouse_pos);
104 }
105
106 // Dispatch drag to coordinator (handlers gate internally via drag state).
107 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
108 entity_coordinator_.HandleDrag(canvas_mouse_pos, io.MouseDelta);
109 }
110
111 // Handle mouse release - complete drag operation
112 if (mouse_left_released) {
114 }
115}
116
117void DungeonObjectInteraction::HandleLeftClick(const ImVec2& canvas_mouse_pos) {
118 int canvas_x = static_cast<int>(std::floor(canvas_mouse_pos.x));
119 int canvas_y = static_cast<int>(std::floor(canvas_mouse_pos.y));
120
121 // Try to handle click via entity coordinator (handles placement, entity selection, and object selection)
122 if (entity_coordinator_.HandleClick(canvas_x, canvas_y)) {
123 // If a selected room element was clicked, prime drag state. Plain clicks on
124 // selected mixed members preserve the whole selection; movement begins only
125 // if the mouse actually drags.
128 HandleObjectSelectionStart(canvas_mouse_pos);
129 }
130 return;
131 }
132
133 // The canvas viewport can contain blank space after panning. Overlay clicks
134 // are dispatched above, but blank space outside the translated room must not
135 // clear selection or start a marquee.
136 if (!dungeon_coords::IsWithinBounds(canvas_x, canvas_y)) {
137 return;
138 }
139
140 // Not an entity click or placement; handle empty space
141 HandleEmptySpaceClick(canvas_mouse_pos);
142}
143
145 const ImVec2& canvas_mouse_pos) {
146 auto [room_x, room_y] =
147 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
148 static_cast<int>(canvas_mouse_pos.y));
149 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
150 auto& room = (*rooms_)[current_room_id_];
151 auto& state = mode_manager_.GetModeState();
152
153 // Only set for valid interior tiles (0-63)
154 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
155 // Start a paint stroke (single undo snapshot per stroke).
156 if (!state.is_painting) {
157 state.is_painting = true;
158 state.paint_mutation_started = false;
159 state.paint_last_tile_x = room_x;
160 state.paint_last_tile_y = room_y;
161 }
162
163 const int x0 =
164 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
165 const int y0 =
166 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
167
168 bool changed = false;
169 auto ensure_mutation = [&]() {
170 if (!state.paint_mutation_started) {
172 state.paint_mutation_started = true;
173 }
174 };
175
177 x0, y0, room_x, room_y, [&](int lx, int ly) {
179 lx, ly, state.paint_brush_radius,
180 /*min_x=*/0, /*min_y=*/0, /*max_x=*/63, /*max_y=*/63,
181 [&](int bx, int by) {
182 if (room.GetCollisionTile(bx, by) ==
183 state.paint_collision_value) {
184 return;
185 }
186 ensure_mutation();
187 room.SetCollisionTile(bx, by, state.paint_collision_value);
188 changed = true;
189 });
190 });
191
192 if (changed) {
195 }
196
197 state.paint_last_tile_x = room_x;
198 state.paint_last_tile_y = room_y;
199 }
200 }
201}
202
203void DungeonObjectInteraction::UpdateWaterFillPainting(
204 const ImVec2& canvas_mouse_pos) {
205 const ImGuiIO& io = ImGui::GetIO();
206 const bool erase = io.KeyAlt;
207
208 auto [room_x, room_y] =
209 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
210 static_cast<int>(canvas_mouse_pos.y));
211 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
212 auto& room = (*rooms_)[current_room_id_];
213 auto& state = mode_manager_.GetModeState();
214
215 // Only set for valid interior tiles (0-63)
216 if (room_x >= 0 && room_x < 64 && room_y >= 0 && room_y < 64) {
217 const bool new_val = !erase;
218 // Start a paint stroke (single undo snapshot per stroke).
219 if (!state.is_painting) {
220 state.is_painting = true;
221 state.paint_mutation_started = false;
222 state.paint_last_tile_x = room_x;
223 state.paint_last_tile_y = room_y;
224 }
225
226 const int x0 =
227 (state.paint_last_tile_x >= 0) ? state.paint_last_tile_x : room_x;
228 const int y0 =
229 (state.paint_last_tile_y >= 0) ? state.paint_last_tile_y : room_y;
230
231 bool changed = false;
232 auto ensure_mutation = [&]() {
233 if (!state.paint_mutation_started) {
234 interaction_context_.NotifyMutation(MutationDomain::kWaterFill);
235 state.paint_mutation_started = true;
236 }
237 };
238
239 paint_util::ForEachPointOnLine(
240 x0, y0, room_x, room_y, [&](int lx, int ly) {
241 paint_util::ForEachPointInSquareBrush(
242 lx, ly, state.paint_brush_radius,
243 /*min_x=*/0, /*min_y=*/0, /*max_x=*/63, /*max_y=*/63,
244 [&](int bx, int by) {
245 if (room.GetWaterFillTile(bx, by) == new_val) {
246 return;
247 }
248 ensure_mutation();
249 room.SetWaterFillTile(bx, by, new_val);
250 changed = true;
251 });
252 });
253
254 if (changed) {
255 interaction_context_.NotifyInvalidateCache(MutationDomain::kWaterFill);
256 }
257
258 state.paint_last_tile_x = room_x;
259 state.paint_last_tile_y = room_y;
260 }
261 }
262}
263
264void DungeonObjectInteraction::HandleObjectSelectionStart(
265 const ImVec2& canvas_mouse_pos) {
266 const bool has_object_selection = selection_.HasSelection();
267 const bool has_entity_selection = HasEntitySelection();
268 if (!has_object_selection && !has_entity_selection) {
269 return;
270 }
271
272 mode_manager_.SetMode(InteractionMode::DraggingObjects);
273 if (has_object_selection) {
274 entity_coordinator_.tile_handler().InitDrag(canvas_mouse_pos);
275 }
276 if (has_entity_selection) {
277 entity_coordinator_.BeginSelectionDrag(canvas_mouse_pos);
278 }
279}
280
281void DungeonObjectInteraction::HandleEmptySpaceClick(
282 const ImVec2& canvas_mouse_pos) {
283 const ImGuiIO& io = ImGui::GetIO();
284 const bool additive = io.KeyShift || io.KeyCtrl || io.KeySuper;
285 const bool had_selection =
286 selection_.HasSelection() || entity_coordinator_.HasEntitySelection();
287
288 // ZScream treats an empty click against an existing selection as a clear
289 // action. Rectangle selection starts from a clean canvas gesture.
290 if (!additive) {
291 ClearEntitySelection();
292 selection_.ClearSelection();
293 }
294
295 if (!had_selection) {
296 entity_coordinator_.tile_handler().BeginMarqueeSelection(canvas_mouse_pos);
297 }
298}
299
300void DungeonObjectInteraction::HandleMouseRelease() {
301 {
302 // End paint strokes on mouse release so a new left-drag creates a new undo
303 // snapshot. Keep the paint mode active (tool stays selected).
304 const auto mode = mode_manager_.GetMode();
305 if (mode == InteractionMode::PaintCollision ||
306 mode == InteractionMode::PaintWaterFill) {
307 auto& state = mode_manager_.GetModeState();
308 const bool had_mutation = state.paint_mutation_started;
309 state.is_painting = false;
310 state.paint_mutation_started = false;
311 state.paint_last_tile_x = -1;
312 state.paint_last_tile_y = -1;
313 // Emit a final invalidation after the stroke ends so domain-specific undo
314 // capture can finalize the action once we're no longer "painting".
315 if (had_mutation) {
316 interaction_context_.NotifyInvalidateCache(
317 (mode == InteractionMode::PaintCollision)
318 ? MutationDomain::kCustomCollision
319 : MutationDomain::kWaterFill);
320 }
321 }
322 }
323
324 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
325 mode_manager_.SetMode(InteractionMode::Select);
326 }
327 entity_coordinator_.HandleRelease();
328 // Marquee selection finalization is handled by TileObjectHandler via
329 // CheckForObjectSelection().
330}
331
332bool DungeonObjectInteraction::HandleKeyboardNudge() {
333 if (!rooms_ || current_room_id_ < 0 ||
334 current_room_id_ >= static_cast<int>(rooms_->size())) {
335 return false;
336 }
337
338 const ImGuiIO& io = ImGui::GetIO();
339 if (ImGui::IsAnyItemActive() || io.KeyCtrl || io.KeySuper || io.KeyAlt ||
340 ImGui::IsMouseDown(ImGuiMouseButton_Left) ||
341 entity_coordinator_.IsPlacementActive()) {
342 return false;
343 }
344
345 const auto mode = mode_manager_.GetMode();
346 if (mode == InteractionMode::DraggingObjects ||
347 mode == InteractionMode::PaintCollision ||
348 mode == InteractionMode::PaintWaterFill) {
349 return false;
350 }
351
352 int delta_x = 0;
353 int delta_y = 0;
354 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true)) {
355 delta_x = -1;
356 } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow, true)) {
357 delta_x = 1;
358 }
359 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, true)) {
360 delta_y = -1;
361 } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)) {
362 delta_y = 1;
363 }
364
365 if (delta_x == 0 && delta_y == 0) {
366 return false;
367 }
368
369 return NudgeSelected(delta_x, delta_y);
370}
371
372bool DungeonObjectInteraction::NudgeSelected(int delta_x, int delta_y) {
373 if (!rooms_ || current_room_id_ < 0 ||
374 current_room_id_ >= static_cast<int>(rooms_->size())) {
375 return false;
376 }
377
378 bool handled = false;
379 if (selection_.HasSelection()) {
380 entity_coordinator_.tile_handler().MoveObjects(
381 current_room_id_, selection_.GetSelectedIndices(), delta_x, delta_y);
382 handled = true;
383 }
384
385 if (entity_coordinator_.HasEntitySelection()) {
386 handled = entity_coordinator_.NudgeSelected(delta_x, delta_y) || handled;
387 }
388
389 return handled;
390}
391
392void DungeonObjectInteraction::CheckForObjectSelection() {
393 // Draw/update active marquee selection for tile objects (delegated).
394 const ImGuiIO& io = ImGui::GetIO();
395 const ImVec2 mouse_pos = GetCanvasTransform().ScreenToRoomPixels(io.MousePos);
396 const bool mouse_left_released =
397 ImGui::IsMouseReleased(ImGuiMouseButton_Left);
398
399 if (mouse_left_released && selection_.IsRectangleSelectionActive()) {
400 selection_.UpdateRectangleSelection(static_cast<int>(mouse_pos.x),
401 static_cast<int>(mouse_pos.y));
402 constexpr int kMinRectPixels = 6;
403 if (!io.KeyAlt && selection_.IsRectangleLargeEnough(kMinRectPixels)) {
404 entity_coordinator_.SelectEntitiesInRect(
405 selection_.GetRectangleSelectionBounds(),
406 /*additive=*/false,
407 /*toggle=*/false);
408 }
409 }
410
411 entity_coordinator_.tile_handler().HandleMarqueeSelection(
412 mouse_pos,
413 /*mouse_left_down=*/ImGui::IsMouseDown(ImGuiMouseButton_Left),
414 /*mouse_left_released=*/mouse_left_released,
415 /*shift_down=*/io.KeyShift,
416 /*toggle_down=*/io.KeyCtrl || io.KeySuper,
417 /*alt_down=*/io.KeyAlt);
418}
419
420void DungeonObjectInteraction::DrawSelectionHighlights() {
421 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
422 return;
423
424 auto& room = (*rooms_)[current_room_id_];
425 const auto& objects = room.GetTileObjects();
426
427 // Use ObjectSelection's rendering (handles pulsing border, corner handles)
428 selection_.DrawSelectionHighlights(
429 canvas_, objects, [](const zelda3::RoomObject& obj) {
430 auto result = zelda3::DimensionService::Get().GetDimensions(obj);
431 return std::make_tuple(result.offset_x_tiles * 8,
432 result.offset_y_tiles * 8, result.width_pixels(),
433 result.height_pixels());
434 });
435
436 // Enhanced hover tooltip showing object info (always visible on hover)
437 // Skip completely in exclusive entity mode (door/sprite/item selected)
438 if (entity_coordinator_.HasEntitySelection()) {
439 return; // Entity mode active - no object tooltips or hover
440 }
441
442 if (canvas_->IsMouseHovering()) {
443 // Also skip tooltip if cursor is over a door/sprite/item entity (not selected yet)
444 ImGuiIO& io = ImGui::GetIO();
445 const auto [cursor_x, cursor_y] =
446 GetCanvasTransform().ScreenToRoomPixelCoordinates(io.MousePos);
447 auto entity_at_cursor =
448 entity_coordinator_.GetEntityAtPosition(cursor_x, cursor_y);
449 if (entity_at_cursor.has_value()) {
450 // Entity has priority - skip object tooltip, DrawHoverHighlight will also skip
451 DrawHoverHighlight(objects);
452 return;
453 }
454
455 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(
456 cursor_x, cursor_y);
457 if (hovered_index.has_value() && *hovered_index < objects.size()) {
458 const auto& object = objects[*hovered_index];
459 std::string object_name = zelda3::GetObjectName(object.id_);
460 int subtype = zelda3::GetObjectSubtype(object.id_);
461 const int layer = object.GetLayerValue();
462
463 // Get subtype name
464 const char* subtype_names[] = {"Unknown", "Type 1", "Type 2", "Type 3"};
465 const char* subtype_name =
466 (subtype >= 0 && subtype <= 3) ? subtype_names[subtype] : "Unknown";
467
468 // Build informative tooltip
469 std::string tooltip;
470 tooltip += object_name;
471 tooltip += " (" + std::string(subtype_name) + ")";
472 tooltip += "\n";
473 tooltip += "ID: 0x" + absl::StrFormat("%03X", object.id_);
474 if (zelda3::UsesRoomObjectStream(object)) {
475 static constexpr const char* kStreamNames[] = {"Primary", "BG2 overlay",
476 "BG1 overlay"};
477 const char* stream_name =
478 (layer >= 0 && layer < 3) ? kStreamNames[layer] : "Unknown";
479 tooltip += " | Object stream: " + std::string(stream_name);
480 } else {
481 const char* layer_name =
482 layer == 0 ? "Upper layer (BG1)" : "Lower layer (BG2)";
483 tooltip += " | Special layer: " + std::string(layer_name);
484 }
485 tooltip += " | Pos: (" + std::to_string(object.x_) + ", " +
486 std::to_string(object.y_) + ")";
487 tooltip += "\nSize: " + std::to_string(object.size_) + " (0x" +
488 absl::StrFormat("%02X", object.size_) + ")";
489
490 if (selection_.IsObjectSelected(*hovered_index)) {
491 tooltip += "\n" ICON_MD_MOUSE " Scroll wheel to resize";
492 tooltip += "\n" ICON_MD_DRAG_INDICATOR " Drag to move";
493 } else {
494 tooltip += "\n" ICON_MD_TOUCH_APP " Click to select";
495 }
496
497 ImGui::SetTooltip("%s", tooltip.c_str());
498 }
499 }
500
501 // Draw hover highlight for non-selected objects
502 DrawHoverHighlight(objects);
503}
504
505void DungeonObjectInteraction::DrawHoverHighlight(
506 const std::vector<zelda3::RoomObject>& objects) {
507 if (!canvas_->IsMouseHovering())
508 return;
509
510 // Skip all object hover in exclusive entity mode (door/sprite/item selected)
511 if (entity_coordinator_.HasEntitySelection())
512 return;
513
514 // Don't show object hover highlight if cursor is over a door/sprite/item entity
515 // Entities take priority over objects for interaction
516 ImGuiIO& io = ImGui::GetIO();
517 const DungeonCanvasTransform transform = GetCanvasTransform();
518 const auto [cursor_canvas_x, cursor_canvas_y] =
519 transform.ScreenToRoomPixelCoordinates(io.MousePos);
520 auto entity_at_cursor =
521 entity_coordinator_.GetEntityAtPosition(cursor_canvas_x, cursor_canvas_y);
522 if (entity_at_cursor.has_value()) {
523 return; // Entity has priority - skip object hover highlight
524 }
525
526 auto hovered_index = entity_coordinator_.tile_handler().GetEntityAtPosition(
527 cursor_canvas_x, cursor_canvas_y);
528 if (!hovered_index.has_value() || *hovered_index >= objects.size()) {
529 return;
530 }
531 const auto& object = objects[*hovered_index];
532
533 // Don't draw hover highlight if object is already selected
534 if (selection_.IsObjectSelected(*hovered_index)) {
535 return;
536 }
537
538 const auto& theme = AgentUI::GetTheme();
539 ImDrawList* draw_list = ImGui::GetWindowDrawList();
540 // Calculate object position and dimensions
541 auto [sel_x_px, sel_y_px, pixel_width, pixel_height] =
543
544 ImVec2 obj_start = transform.RoomPixelsToScreen(
545 ImVec2(static_cast<float>(sel_x_px), static_cast<float>(sel_y_px)));
546 const ImVec2 obj_size = transform.RoomSizeToScreen(ImVec2(
547 static_cast<float>(pixel_width), static_cast<float>(pixel_height)));
548 ImVec2 obj_end(obj_start.x + obj_size.x, obj_start.y + obj_size.y);
549
550 // Expand slightly for visibility
551 constexpr float margin = 2.0f;
552 obj_start.x -= margin;
553 obj_start.y -= margin;
554 obj_end.x += margin;
555 obj_end.y += margin;
556
557 // Draw subtle hover highlight with unified theme color
558 ImVec4 hover_fill = theme.selection_hover;
559 hover_fill.w *= 0.5f; // Make it more subtle for hover fill
560
561 ImVec4 hover_border = theme.selection_hover;
562
563 // Draw filled background for better visibility
564 draw_list->AddRectFilled(obj_start, obj_end, ImGui::GetColorU32(hover_fill));
565
566 // Draw dashed-style border (simulated with thinner line)
567 draw_list->AddRect(obj_start, obj_end, ImGui::GetColorU32(hover_border), 0.0f,
568 0, 1.5f);
569}
570
571void DungeonObjectInteraction::PlaceObjectAtPosition(int room_x, int room_y) {
572 entity_coordinator_.tile_handler().PlaceObjectAt(
573 current_room_id_, preview_object_, room_x, room_y);
574
575 if (object_placed_callback_) {
576 object_placed_callback_(preview_object_);
577 }
578
579 interaction_context_.NotifyInvalidateCache(MutationDomain::kTileObjects);
580 CancelPlacement();
581}
582
583std::pair<int, int> DungeonObjectInteraction::RoomToCanvasCoordinates(
584 int room_x, int room_y) const {
585 // Dungeon tiles are 8x8 pixels, convert room coordinates (tiles) to pixels
586 return {room_x * 8, room_y * 8};
587}
588
589std::pair<int, int> DungeonObjectInteraction::CanvasToRoomCoordinates(
590 int canvas_x, int canvas_y) const {
591 // Convert canvas pixels back to room coordinates (tiles)
592 return {canvas_x / 8, canvas_y / 8};
593}
594
595bool DungeonObjectInteraction::IsWithinCanvasBounds(int canvas_x, int canvas_y,
596 int margin) const {
597 return dungeon_coords::IsWithinBounds(canvas_x, canvas_y, margin);
598}
599
600void DungeonObjectInteraction::SetCurrentRoom(DungeonRoomStore* rooms,
601 int room_id) {
602 rooms_ = rooms;
603 current_room_id_ = room_id;
604 interaction_context_.rooms = rooms;
605 interaction_context_.current_room_id = room_id;
606 interaction_context_.selection = &selection_;
607 entity_coordinator_.SetContext(&interaction_context_);
608}
609
610void DungeonObjectInteraction::SetPreviewObject(
611 const zelda3::RoomObject& object, bool loaded) {
612 preview_object_ = object;
613
614 if (loaded && object.id_ >= 0) {
615 // Cancel other placement modes (doors/sprites/items) before entering object
616 // placement. We re-enable tile placement below.
617 entity_coordinator_.CancelPlacement();
618
619 // Enter object placement mode
620 mode_manager_.SetMode(InteractionMode::PlaceObject);
621 mode_manager_.GetModeState().preview_object = object;
622
623 // Ensure tile placement mode is active so ghost preview can render and
624 // clicks place the object.
625 auto& tile_handler = entity_coordinator_.tile_handler();
626 tile_handler.SetPreviewObject(preview_object_);
627 if (!tile_handler.IsPlacementActive()) {
628 tile_handler.BeginPlacement();
629 }
630 } else {
631 // Exit placement mode if not loaded
632 if (mode_manager_.GetMode() == InteractionMode::PlaceObject) {
633 CancelPlacement();
634 }
635 }
636}
637
638void DungeonObjectInteraction::ClearSelection() {
639 selection_.ClearSelection();
640 if (mode_manager_.GetMode() == InteractionMode::DraggingObjects) {
641 mode_manager_.SetMode(InteractionMode::Select);
642 }
643}
644
645void DungeonObjectInteraction::HandleDeleteSelected() {
646 auto indices = selection_.GetSelectedIndices();
647 if (!indices.empty()) {
648 entity_coordinator_.tile_handler().DeleteObjects(current_room_id_, indices);
649 selection_.ClearSelection();
650 }
651
652 if (entity_coordinator_.HasEntitySelection()) {
653 entity_coordinator_.DeleteSelectedEntity();
654 }
655}
656
657void DungeonObjectInteraction::HandleDeleteAllObjects() {
658 entity_coordinator_.tile_handler().DeleteAllObjects(current_room_id_);
659 selection_.ClearSelection();
660}
661
662void DungeonObjectInteraction::HandleCopySelected() {
663 if (!rooms_ || current_room_id_ < 0 ||
664 current_room_id_ >= static_cast<int>(rooms_->size())) {
665 return;
666 }
667
668 const auto selected_objects = selection_.GetSelectedIndices();
669 const bool has_object_selection = !selected_objects.empty();
670 const bool has_entity_selection = entity_coordinator_.HasEntitySelection();
671 if (!has_object_selection && !has_entity_selection) {
672 return;
673 }
674
675 entity_clipboard_.Clear();
676 bool clipboard_origin_set = false;
677
678 if (has_object_selection) {
679 entity_coordinator_.tile_handler().CopyObjectsToClipboard(current_room_id_,
680 selected_objects);
681 const auto& objects = (*rooms_)[current_room_id_].GetTileObjects();
682 for (size_t index : selected_objects) {
683 if (index >= objects.size()) {
684 continue;
685 }
686 entity_clipboard_.origin_tile_x = objects[index].x_;
687 entity_clipboard_.origin_tile_y = objects[index].y_;
688 entity_clipboard_.origin_pixel_x =
689 entity_clipboard_.origin_tile_x * dungeon_coords::kTileSize;
690 entity_clipboard_.origin_pixel_y =
691 entity_clipboard_.origin_tile_y * dungeon_coords::kTileSize;
692 clipboard_origin_set = true;
693 break;
694 }
695 } else {
696 entity_coordinator_.tile_handler().ClearClipboard();
697 }
698
699 CopySelectedEntitiesToClipboard(clipboard_origin_set);
700}
701
702void DungeonObjectInteraction::CopySelectedEntitiesToClipboard(
703 bool clipboard_origin_set) {
704 if (!rooms_ || current_room_id_ < 0 ||
705 current_room_id_ >= static_cast<int>(rooms_->size())) {
706 return;
707 }
708
709 const auto& room = (*rooms_)[current_room_id_];
710 auto selected_entities = entity_coordinator_.GetSelectedEntities();
711 if (selected_entities.empty() && entity_coordinator_.HasEntitySelection()) {
712 const SelectedEntity selected = entity_coordinator_.GetSelectedEntity();
713 if (selected.type != EntityType::None) {
714 selected_entities.push_back(selected);
715 }
716 }
717
718 for (const auto entity : selected_entities) {
719 switch (entity.type) {
720 case EntityType::Sprite: {
721 const auto& sprites = room.GetSprites();
722 if (entity.index >= sprites.size()) {
723 break;
724 }
725 const auto& sprite = sprites[entity.index];
726 if (!clipboard_origin_set && !entity_clipboard_.HasData()) {
727 entity_clipboard_.origin_pixel_x =
728 sprite.x() * dungeon_coords::kSpriteTileSize;
729 entity_clipboard_.origin_pixel_y =
730 sprite.y() * dungeon_coords::kSpriteTileSize;
731 entity_clipboard_.origin_tile_x =
732 entity_clipboard_.origin_pixel_x / dungeon_coords::kTileSize;
733 entity_clipboard_.origin_tile_y =
734 entity_clipboard_.origin_pixel_y / dungeon_coords::kTileSize;
735 }
736 entity_clipboard_.sprites.push_back(sprite);
737 break;
738 }
739 case EntityType::Item: {
740 const auto& items = room.GetPotItems();
741 if (entity.index >= items.size()) {
742 break;
743 }
744 const auto& item = items[entity.index];
745 if (!clipboard_origin_set && !entity_clipboard_.HasData()) {
746 entity_clipboard_.origin_pixel_x = item.GetPixelX();
747 entity_clipboard_.origin_pixel_y = item.GetPixelY();
748 entity_clipboard_.origin_tile_x =
749 entity_clipboard_.origin_pixel_x / dungeon_coords::kTileSize;
750 entity_clipboard_.origin_tile_y =
751 entity_clipboard_.origin_pixel_y / dungeon_coords::kTileSize;
752 }
753 entity_clipboard_.items.push_back(item);
754 break;
755 }
756 case EntityType::Door:
757 case EntityType::Object:
758 case EntityType::None:
759 default:
760 break;
761 }
762 }
763}
764
765std::vector<SelectedEntity> DungeonObjectInteraction::PasteEntityClipboardAt(
766 int target_pixel_x, int target_pixel_y) {
767 std::vector<SelectedEntity> pasted_entities;
768 if (!entity_clipboard_.HasData() || !rooms_ || current_room_id_ < 0 ||
769 current_room_id_ >= static_cast<int>(rooms_->size())) {
770 return pasted_entities;
771 }
772
773 auto& room = (*rooms_)[current_room_id_];
774 const int delta_pixel_x = target_pixel_x - entity_clipboard_.origin_pixel_x;
775 const int delta_pixel_y = target_pixel_y - entity_clipboard_.origin_pixel_y;
776
777 if (!entity_clipboard_.sprites.empty()) {
778 interaction_context_.NotifyMutation(MutationDomain::kSprites);
779 auto& sprites = room.GetSprites();
780 for (auto sprite : entity_clipboard_.sprites) {
781 const int next_x = std::clamp(
782 (sprite.x() * dungeon_coords::kSpriteTileSize + delta_pixel_x) /
783 dungeon_coords::kSpriteTileSize,
784 0, dungeon_coords::kSpriteGridMax);
785 const int next_y = std::clamp(
786 (sprite.y() * dungeon_coords::kSpriteTileSize + delta_pixel_y) /
787 dungeon_coords::kSpriteTileSize,
788 0, dungeon_coords::kSpriteGridMax);
789 sprite.set_x(next_x);
790 sprite.set_y(next_y);
791 sprites.push_back(sprite);
792 pasted_entities.push_back(
793 SelectedEntity{EntityType::Sprite, sprites.size() - 1});
794 }
795 room.MarkSpritesDirty();
796 interaction_context_.NotifyInvalidateCache(MutationDomain::kSprites);
797 }
798
799 if (!entity_clipboard_.items.empty()) {
800 interaction_context_.NotifyMutation(MutationDomain::kItems);
801 auto& items = room.GetPotItems();
802 for (auto item : entity_clipboard_.items) {
803 item.position = EncodePotItemPosition(item.GetPixelX() + delta_pixel_x,
804 item.GetPixelY() + delta_pixel_y);
805 items.push_back(item);
806 pasted_entities.push_back(
807 SelectedEntity{EntityType::Item, items.size() - 1});
808 }
809 room.MarkPotItemsDirty();
810 interaction_context_.NotifyInvalidateCache(MutationDomain::kItems);
811 }
812
813 if (!pasted_entities.empty()) {
814 interaction_context_.NotifyEntityChanged();
815 }
816 return pasted_entities;
817}
818
819void DungeonObjectInteraction::HandlePasteObjects() {
820 if (!HasClipboardData()) {
821 return;
822 }
823
824 if (!rooms_ || current_room_id_ < 0 ||
825 current_room_id_ >= static_cast<int>(rooms_->size())) {
826 return;
827 }
828
829 auto& handler = entity_coordinator_.tile_handler();
830 const ImGuiIO& io = ImGui::GetIO();
831 const auto [canvas_mouse_x, canvas_mouse_y] =
832 GetCanvasTransform().ScreenToRoomPixelCoordinates(io.MousePos);
833 auto [paste_x, paste_y] =
834 CanvasToRoomCoordinates(canvas_mouse_x, canvas_mouse_y);
835 int paste_pixel_x = paste_x * dungeon_coords::kTileSize;
836 int paste_pixel_y = paste_y * dungeon_coords::kTileSize;
837
838 if (!IsWithinCanvasBounds(canvas_mouse_x, canvas_mouse_y, 0)) {
839 const int fallback_delta = entity_clipboard_.sprites.empty()
840 ? dungeon_coords::kTileSize
841 : dungeon_coords::kSpriteTileSize;
842 paste_pixel_x =
843 std::clamp(entity_clipboard_.origin_pixel_x + fallback_delta, 0,
844 dungeon_coords::kRoomPixelWidth - dungeon_coords::kTileSize);
845 paste_pixel_y = std::clamp(
846 entity_clipboard_.origin_pixel_y + fallback_delta, 0,
847 dungeon_coords::kRoomPixelHeight - dungeon_coords::kTileSize);
848 paste_x = paste_pixel_x / dungeon_coords::kTileSize;
849 paste_y = paste_pixel_y / dungeon_coords::kTileSize;
850 }
851
852 std::vector<size_t> new_indices;
853 if (handler.HasClipboardData()) {
854 new_indices = handler.PasteFromClipboard(
855 current_room_id_, paste_x - entity_clipboard_.origin_tile_x,
856 paste_y - entity_clipboard_.origin_tile_y);
857 }
858 auto new_entities = PasteEntityClipboardAt(paste_pixel_x, paste_pixel_y);
859
860 if (!new_indices.empty() || !new_entities.empty()) {
861 selection_.ClearSelection();
862 for (size_t idx : new_indices) {
863 selection_.SelectObject(idx, ObjectSelection::SelectionMode::Add);
864 }
865 entity_coordinator_.SetSelectedEntities(std::move(new_entities));
866 }
867}
868
869void DungeonObjectInteraction::DrawGhostPreview() {
870 entity_coordinator_.DrawGhostPreviews();
871}
872
873void DungeonObjectInteraction::HandleScrollWheelResize() {
874 const ImGuiIO& io = ImGui::GetIO();
875 entity_coordinator_.HandleMouseWheel(io.MouseWheel);
876}
877
878bool DungeonObjectInteraction::SetObjectId(size_t index, int16_t id) {
879 entity_coordinator_.tile_handler().UpdateObjectsId(current_room_id_, {index},
880 id);
881 return true;
882}
883
884bool DungeonObjectInteraction::SetObjectSize(size_t index, uint8_t size) {
885 entity_coordinator_.tile_handler().UpdateObjectsSize(current_room_id_,
886 {index}, size);
887 return true;
888}
889
890bool DungeonObjectInteraction::SetObjectLayer(
891 size_t index, zelda3::RoomObject::LayerType layer) {
892 return entity_coordinator_.tile_handler().UpdateObjectsLayer(
893 current_room_id_, {index}, static_cast<int>(layer));
894}
895
896std::pair<int, int> DungeonObjectInteraction::CalculateObjectBounds(
897 const zelda3::RoomObject& object) {
899}
900
901bool DungeonObjectInteraction::CanAssignSelectedObjectsToLayer(
902 int target_layer) const {
903 if (!rooms_ || current_room_id_ < 0 ||
904 current_room_id_ >= static_cast<int>(rooms_->size()) ||
905 target_layer < 0 || target_layer > 2) {
906 return false;
907 }
908
909 const auto selected = selection_.GetSelectedIndices();
910 if (selected.empty()) {
911 return false;
912 }
913
914 const auto& objects = (*rooms_)[current_room_id_].GetTileObjects();
915 for (const size_t index : selected) {
916 if (index >= objects.size() ||
917 (target_layer == 2 && !zelda3::UsesRoomObjectStream(objects[index]))) {
918 return false;
919 }
920 }
921 return true;
922}
923
924bool DungeonObjectInteraction::SendSelectedToLayer(int target_layer) {
925 if (!CanAssignSelectedObjectsToLayer(target_layer)) {
926 return false;
927 }
928 return entity_coordinator_.tile_handler().UpdateObjectsLayer(
929 current_room_id_, selection_.GetSelectedIndices(), target_layer);
930}
931
932void DungeonObjectInteraction::SendSelectedToFront() {
933 entity_coordinator_.tile_handler().SendToFront(
934 current_room_id_, selection_.GetSelectedIndices());
935}
936
937void DungeonObjectInteraction::SendSelectedToBack() {
938 entity_coordinator_.tile_handler().SendToBack(
939 current_room_id_, selection_.GetSelectedIndices());
940}
941
942void DungeonObjectInteraction::BringSelectedForward() {
943 entity_coordinator_.tile_handler().MoveForward(
944 current_room_id_, selection_.GetSelectedIndices());
945}
946
947void DungeonObjectInteraction::SendSelectedBackward() {
948 entity_coordinator_.tile_handler().MoveBackward(
949 current_room_id_, selection_.GetSelectedIndices());
950}
951
952void DungeonObjectInteraction::HandleLayerKeyboardShortcuts() {
953 // Only process if we have selected objects
954 if (!selection_.HasSelection())
955 return;
956
957 // Only when not typing in a text field
958 if (ImGui::IsAnyItemActive())
959 return;
960
961 // Check for stored placement shortcuts (1, 2, 3 keys). The third room
962 // object stream is intentionally unavailable to torches/pushable blocks.
963 if (ImGui::IsKeyPressed(ImGuiKey_1)) {
964 SendSelectedToLayer(0); // Primary stream / upper layer (BG1)
965 } else if (ImGui::IsKeyPressed(ImGuiKey_2)) {
966 SendSelectedToLayer(1); // BG2 overlay / lower layer (BG2)
967 } else if (ImGui::IsKeyPressed(ImGuiKey_3)) {
968 SendSelectedToLayer(2); // BG1 overlay stream
969 }
970
971 // Object ordering shortcuts
972 // Ctrl+Shift+] = Bring to Front, Ctrl+Shift+[ = Send to Back
973 // Ctrl+] = Bring Forward, Ctrl+[ = Send Backward
974 auto& io = ImGui::GetIO();
975 if (io.KeyCtrl && io.KeyShift) {
976 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
977 SendSelectedToFront();
978 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
979 SendSelectedToBack();
980 }
981 } else if (io.KeyCtrl) {
982 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
983 BringSelectedForward();
984 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
985 SendSelectedBackward();
986 }
987 }
988}
989
990// ============================================================================
991// Door Placement Methods
992// ============================================================================
993
994void DungeonObjectInteraction::SetDoorPlacementMode(bool enabled,
995 zelda3::DoorType type) {
996 if (enabled) {
997 mode_manager_.SetMode(InteractionMode::PlaceDoor);
998 entity_coordinator_.door_handler().SetDoorType(type);
999 entity_coordinator_.door_handler().BeginPlacement();
1000 } else {
1001 entity_coordinator_.door_handler().CancelPlacement();
1002 if (mode_manager_.GetMode() == InteractionMode::PlaceDoor)
1003 mode_manager_.SetMode(InteractionMode::Select);
1004 }
1005}
1006
1007// ============================================================================
1008// Sprite Placement Methods
1009// ============================================================================
1010
1011void DungeonObjectInteraction::SetSpritePlacementMode(bool enabled,
1012 uint8_t sprite_id) {
1013 if (enabled) {
1014 mode_manager_.SetMode(InteractionMode::PlaceSprite);
1015 entity_coordinator_.sprite_handler().SetSpriteId(sprite_id);
1016 entity_coordinator_.sprite_handler().BeginPlacement();
1017 } else {
1018 entity_coordinator_.sprite_handler().CancelPlacement();
1019 if (mode_manager_.GetMode() == InteractionMode::PlaceSprite)
1020 mode_manager_.SetMode(InteractionMode::Select);
1021 }
1022}
1023
1024// ============================================================================
1025// Item Placement Methods
1026// ============================================================================
1027
1028void DungeonObjectInteraction::SetItemPlacementMode(bool enabled,
1029 uint8_t item_id) {
1030 if (enabled) {
1031 mode_manager_.SetMode(InteractionMode::PlaceItem);
1032 entity_coordinator_.item_handler().SetItemId(item_id);
1033 entity_coordinator_.item_handler().BeginPlacement();
1034 } else {
1035 entity_coordinator_.item_handler().CancelPlacement();
1036 if (mode_manager_.GetMode() == InteractionMode::PlaceItem)
1037 mode_manager_.SetMode(InteractionMode::Select);
1038 }
1039}
1040
1041// ============================================================================
1042// Entity Selection Methods (Doors, Sprites, Items)
1043// ============================================================================
1044
1045void DungeonObjectInteraction::SelectEntity(EntityType type, size_t index) {
1046 selection_.ClearSelection();
1047 entity_coordinator_.SelectEntity(type, index);
1048}
1049
1050void DungeonObjectInteraction::ClearEntitySelection() {
1051 entity_coordinator_.ClearEntitySelection();
1052}
1053
1054void DungeonObjectInteraction::CancelPlacement() {
1055 entity_coordinator_.CancelPlacement();
1056 if (mode_manager_.IsPlacementActive()) {
1057 mode_manager_.SetMode(InteractionMode::Select);
1058 }
1059}
1060
1061void DungeonObjectInteraction::DrawEntitySelectionHighlights() {
1062 entity_coordinator_.DrawSelectionHighlights();
1063 entity_coordinator_.DrawPostPlacementOverlays();
1064}
1065
1066void DungeonObjectInteraction::DrawDoorSnapIndicators() {
1067 // Door snap indicators are now managed by DoorInteractionHandler
1068 // through the entity coordinator. No-op here for backward compatibility.
1069}
1070
1071} // namespace yaze::editor
std::pair< int, int > ScreenToRoomPixelCoordinates(ImVec2 screen) const
ImVec2 ScreenToRoomPixels(ImVec2 screen) const
ImVec2 RoomSizeToScreen(ImVec2 room_size) const
DungeonCanvasTransform GetCanvasTransform() const
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.
bool IsMouseHovering() const
Definition canvas.h:338
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
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)
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)
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.