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