yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
interaction_coordinator.cc
Go to the documentation of this file.
4#include "util/i18n/tr.h"
5
6#include <algorithm>
7#include <cmath>
8#include <cstdlib>
9#include <functional>
10#include <optional>
11#include <tuple>
12
13// Third-party library headers
14#include "absl/strings/str_format.h"
15#include "imgui/imgui.h"
16
24
25namespace yaze::editor {
26
27namespace {
28
29constexpr double kCycleHudHoldSeconds = 1.25;
30constexpr size_t kCycleHudMaxLabelChars = 54;
31
32bool Intersects(int ax, int ay, int aw, int ah, int bx, int by, int bw,
33 int bh) {
34 return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
35}
36
37std::optional<std::tuple<int, int, int, int>> GetEntityBounds(
38 const zelda3::Room& room, SelectedEntity entity) {
39 switch (entity.type) {
40 case EntityType::Door: {
41 const auto& doors = room.GetDoors();
42 if (entity.index >= doors.size()) {
43 return std::nullopt;
44 }
45 return doors[entity.index].GetEditorBounds();
46 }
47 case EntityType::Sprite: {
48 const auto& sprites = room.GetSprites();
49 if (entity.index >= sprites.size()) {
50 return std::nullopt;
51 }
52 const auto& sprite = sprites[entity.index];
53 constexpr int kSize = dungeon_coords::kSpriteTileSize;
54 return std::make_tuple(sprite.x() * kSize, sprite.y() * kSize, kSize,
55 kSize);
56 }
57 case EntityType::Item: {
58 const auto& items = room.GetPotItems();
59 if (entity.index >= items.size()) {
60 return std::nullopt;
61 }
62 const auto& item = items[entity.index];
63 return std::make_tuple(item.GetPixelX(), item.GetPixelY(), 16, 16);
64 }
65 case EntityType::Object:
66 case EntityType::None:
67 default:
68 return std::nullopt;
69 }
70}
71
72ImVec4 EntitySelectionColor(const AgentUITheme& theme, EntityType type) {
73 switch (type) {
74 case EntityType::Door:
75 return theme.status_warning;
76 case EntityType::Sprite:
77 return theme.status_success;
78 case EntityType::Item:
79 return theme.dungeon_selection_primary;
80 case EntityType::Object:
81 case EntityType::None:
82 default:
83 return theme.accent_color;
84 }
85}
86
87bool IsCycleModifierHeld(const ImGuiIO& io) {
88 return io.KeyAlt && (io.KeyCtrl || io.KeySuper);
89}
90
91std::string TruncateCycleHudLabel(std::string label) {
92 if (label.size() <= kCycleHudMaxLabelChars) {
93 return label;
94 }
95 label.resize(kCycleHudMaxLabelChars - 3);
96 label += "...";
97 return label;
98}
99
100} // namespace
101
109
111 // Cancel current mode first
113
114 current_mode_ = mode;
115
116 // Activate the new mode
117 switch (mode) {
118 case Mode::PlaceDoor:
120 break;
123 break;
124 case Mode::PlaceItem:
126 break;
127 case Mode::Select:
128 // Nothing to activate
129 break;
130 }
131}
132
142
148
149bool InteractionCoordinator::HandleClick(int canvas_x, int canvas_y) {
150 // Check placement modes first
152 return door_handler_.HandleClick(canvas_x, canvas_y);
153 }
155 return sprite_handler_.HandleClick(canvas_x, canvas_y);
156 }
158 return item_handler_.HandleClick(canvas_x, canvas_y);
159 }
161 return tile_handler_.HandleClick(canvas_x, canvas_y);
162 }
163
164 if (door_handler_.HandleOverlayClick(canvas_x, canvas_y)) {
165 return true;
166 }
167
168 if (!dungeon_coords::IsWithinBounds(canvas_x, canvas_y)) {
169 return false;
170 }
171
172 // In select mode, only handle the click if the cursor is over an entity or object.
173 const auto hits = GetEntitiesAtPosition(canvas_x, canvas_y);
174 if (hits.empty()) {
175 return false;
176 }
177
178 const ImGuiIO& io = ImGui::GetIO();
179 const bool cycle_modifier = IsCycleModifierHeld(io);
180 if (io.KeyAlt && !cycle_modifier) {
181 const bool had_entity_selection = HasEntitySelection();
182 const bool had_object_selection =
185 if (ctx_ && ctx_->selection) {
187 }
188 cycle_last_hits_.clear();
191 if ((had_entity_selection || had_object_selection) && ctx_) {
193 }
194 return true;
195 }
196 if (cycle_modifier) {
197 if (!SameCycleTarget(canvas_x, canvas_y, hits)) {
199 }
200 const size_t selected_index = cycle_next_index_ % hits.size();
201 const SelectedEntity selected = hits[selected_index];
202 cycle_next_index_ = (cycle_next_index_ + 1) % hits.size();
203 cycle_last_x_ = canvas_x;
204 cycle_last_y_ = canvas_y;
205 cycle_active_index_ = selected_index;
206 cycle_hud_screen_pos_ = io.MousePos;
207 cycle_hud_start_time_ = ImGui::GetCurrentContext() ? ImGui::GetTime() : 0.0;
208 cycle_last_hits_ = hits;
209 return ApplySelection(selected);
210 }
211
212 const auto entity = hits.front();
213 const bool additive = io.KeyShift || io.KeyCtrl || io.KeySuper;
214 const bool hit_already_selected = IsSelectionHitSelected(entity);
215
216 // Cross-selection rules:
217 // 1. Plain clicks select one stack participant and clear the other family.
218 // 2. Shift/Ctrl/Cmd allow mixed object/entity selection.
219 // 3. Alt-click clears, matching ZScream muscle memory. Ctrl/Cmd+Alt keeps
220 // Yaze's overlap cycle affordance available without stealing Alt.
221
222 if (entity.type == EntityType::Object) {
223 const bool has_multi_object_selection =
225 if (!additive && hit_already_selected &&
226 (has_multi_object_selection || HasGroupDragSelection())) {
227 return true;
228 }
229 if (!additive) {
231 if (ctx_ && ctx_->selection) {
233 }
234 }
235 return tile_handler_.HandleClick(canvas_x, canvas_y);
236 }
237
238 const bool toggle = io.KeyCtrl || io.KeySuper;
239 if (additive) {
240 return UpdateEntitySelection(entity, io.KeyShift, toggle);
241 }
242
243 if (hit_already_selected && HasGroupDragSelection()) {
244 return true;
245 }
246
247 // Plain entity clicks preserve the existing handler drag affordance.
248 selected_entities_.clear();
249 selected_entities_.push_back(entity);
253 if (ctx_ && ctx_->selection) {
255 }
256 switch (entity.type) {
257 case EntityType::Door:
258 return door_handler_.HandleClick(canvas_x, canvas_y);
259 case EntityType::Sprite:
260 return sprite_handler_.HandleClick(canvas_x, canvas_y);
261 case EntityType::Item:
262 return item_handler_.HandleClick(canvas_x, canvas_y);
263 default:
264 selected_entities_.clear();
265 return false;
266 }
267}
268
270 if (type == EntityType::Object || type == EntityType::None) {
272 if (ctx_ && ctx_->selection) {
274 }
275 return;
276 }
277
279
280 selected_entities_.push_back(SelectedEntity{type, index});
281
282 switch (type) {
283 case EntityType::Door:
285 break;
286 case EntityType::Sprite:
288 break;
289 case EntityType::Item:
291 break;
292 case EntityType::Object:
293 case EntityType::None:
294 default:
295 break;
296 }
297}
298
300 std::vector<SelectedEntity> entities) {
304 selected_entities_.clear();
305
306 for (const auto entity : entities) {
307 if (entity.type != EntityType::Door && entity.type != EntityType::Sprite &&
308 entity.type != EntityType::Item) {
309 continue;
310 }
311 if (std::find(selected_entities_.begin(), selected_entities_.end(),
312 entity) == selected_entities_.end()) {
313 selected_entities_.push_back(entity);
314 }
315 }
316
317 if (selected_entities_.size() == 1) {
318 const SelectedEntity selected = selected_entities_.front();
319 switch (selected.type) {
320 case EntityType::Door:
322 break;
323 case EntityType::Sprite:
325 break;
326 case EntityType::Item:
328 break;
329 case EntityType::Object:
330 case EntityType::None:
331 default:
332 break;
333 }
334 return;
335 }
336
337 if (ctx_) {
339 }
340}
341
354
356 const bool had_selection = HasEntitySelection();
358 if (had_selection && ctx_) {
360 }
361}
362
369
371 int canvas_x, int canvas_y) const {
372 const auto hits = GetEntitiesAtPosition(canvas_x, canvas_y);
373 if (hits.empty()) {
374 return std::nullopt;
375 }
376 return hits.front();
377}
378
380 int canvas_x, int canvas_y) const {
381 std::vector<SelectedEntity> hits;
382 if (!dungeon_coords::IsWithinBounds(canvas_x, canvas_y)) {
383 return hits;
384 }
385 if (auto door = door_handler_.GetEntityAtPosition(canvas_x, canvas_y)) {
386 hits.push_back(SelectedEntity{EntityType::Door, *door});
387 }
388 if (auto sprite = sprite_handler_.GetEntityAtPosition(canvas_x, canvas_y)) {
389 hits.push_back(SelectedEntity{EntityType::Sprite, *sprite});
390 }
391 if (auto item = item_handler_.GetEntityAtPosition(canvas_x, canvas_y)) {
392 hits.push_back(SelectedEntity{EntityType::Item, *item});
393 }
394 if (auto object = tile_handler_.GetEntityAtPosition(canvas_x, canvas_y)) {
395 hits.push_back(SelectedEntity{EntityType::Object, *object});
396 }
397 return hits;
398}
399
401 if (!selected_entities_.empty()) {
402 return selected_entities_.front();
403 }
404 if (auto idx = door_handler_.GetSelectedIndex()) {
405 return SelectedEntity{EntityType::Door, *idx};
406 }
407 if (auto idx = sprite_handler_.GetSelectedIndex()) {
408 return SelectedEntity{EntityType::Sprite, *idx};
409 }
410 if (auto idx = item_handler_.GetSelectedIndex()) {
411 return SelectedEntity{EntityType::Item, *idx};
412 }
413 return SelectedEntity{EntityType::None, 0};
414}
415
416void InteractionCoordinator::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
417 // Forward drag to handlers that have active selections
419 HandleEntityGroupDrag(current_pos);
420 return;
421 } else if (door_handler_.HasSelection()) {
422 door_handler_.HandleDrag(current_pos, delta);
423 }
425 sprite_handler_.HandleDrag(current_pos, delta);
426 }
428 item_handler_.HandleDrag(current_pos, delta);
429 }
430
431 // Tile objects (managed by ObjectSelection)
434 tile_handler_.HandleDrag(current_pos, delta);
435 }
436}
437
445
451
453 const bool was_active = entity_group_drag_active_;
454 // Clear first: finalization can restore selection or flush other gestures.
456 if (was_active && ctx_ && ctx_->on_selection_edit_finished) {
458 }
459}
460
464
480
482 if (selected_entities_.size() > 1) {
484 } else {
485 // Preserve the richer single-selection overlays (door pair badge, drag
486 // preview) for the common one-entity inspector workflow.
490 }
491
492 // Draw snap indicators for door placement
495 }
497}
498
500 // Render placement success toasts for all handlers unconditionally so they
501 // remain visible even after the user exits placement mode immediately.
506}
507
509 int canvas_y) {
510 // Clear all selections first
512
513 // Try to select in priority order: doors, sprites, items
514 // (matches original DungeonObjectInteraction behavior)
515 if (door_handler_.HandleClick(canvas_x, canvas_y)) {
516 if (auto index = door_handler_.GetSelectedIndex()) {
517 selected_entities_ = {SelectedEntity{EntityType::Door, *index}};
518 }
519 return true;
520 }
521 if (sprite_handler_.HandleClick(canvas_x, canvas_y)) {
522 if (auto index = sprite_handler_.GetSelectedIndex()) {
523 selected_entities_ = {SelectedEntity{EntityType::Sprite, *index}};
524 }
525 return true;
526 }
527 if (item_handler_.HandleClick(canvas_x, canvas_y)) {
528 if (auto index = item_handler_.GetSelectedIndex()) {
529 selected_entities_ = {SelectedEntity{EntityType::Item, *index}};
530 }
531 return true;
532 }
533
534 return false;
535}
536
541
543 const {
544 if (!selected_entities_.empty()) {
545 return selected_entities_;
546 }
547 const auto selected = GetSelectedEntity();
548 return selected.type == EntityType::None
549 ? std::vector<SelectedEntity>{}
550 : std::vector<SelectedEntity>{selected};
551}
552
554 const auto entities = SelectedEntitiesForEdit();
555 return std::any_of(entities.begin(), entities.end(),
556 [](const auto entity) {
557 return entity.type == EntityType::Sprite;
558 })
561}
562
564 absl::Status status) {
565 const bool new_error = !status.ok() && status != selection_edit_status_;
566 selection_edit_status_ = std::move(status);
567 if (new_error && ctx_ && ctx_->on_selection_edit_error) {
569 }
571}
572
574 const DungeonSelectionEditRequest& request, bool continuous) {
575 auto* room = ctx_ ? ctx_->GetCurrentRoom() : nullptr;
576 if (!room) {
577 return ReportSelectionEditStatus(absl::FailedPreconditionError(
578 "No room is available for selection editing"));
579 }
580 auto planned = PlanDungeonSelectionEdit(*room, request);
581 if (!planned.ok()) {
582 return ReportSelectionEditStatus(planned.status());
583 }
584 if (!planned->changed()) {
585 return ReportSelectionEditStatus(absl::OkStatus());
586 }
587 if (ctx_->on_selection_edit) {
588 const auto status = ctx_->on_selection_edit(*planned, continuous);
589 if (!status.ok()) {
590 return ReportSelectionEditStatus(status);
591 }
592 } else {
593 // Standalone handlers retain legacy notifications; the editor callback
594 // stages global constraints and contributes one shared undo action.
595 if (!continuous)
597 for (const auto [mask, domain] :
602 if (planned->domains & mask)
603 ctx_->NotifyMutation(domain);
604 }
605 ApplyDungeonSelectionEditState(*room, planned->after, planned->domains);
606 }
607 // A drag keeps its indices and active state. Re-selecting handlers during
608 // each increment would end their drag and could recursively finalize undo.
609 if (!continuous) {
610 if (ctx_->selection) {
612 for (const auto index : planned->after.selected_objects) {
615 }
616 }
617 SetSelectedEntities(planned->after.selected_entities);
618 }
619 if (!ctx_->on_selection_edit) {
620 for (const auto [mask, domain] :
625 if (planned->domains & mask)
627 }
629 }
630 return ReportSelectionEditStatus(absl::OkStatus());
631}
632
633bool InteractionCoordinator::NudgeSelected(int delta_x, int delta_y) {
636 if (ctx_ && ctx_->selection)
639 if (request.objects.empty() && request.entities.empty())
640 return false;
641 if (request.objects.empty() && request.entities.size() == 1 &&
642 request.entities.front().type == EntityType::Door) {
643 // A door-only arrow command advances its authored wall slot. Mixed moves
644 // use one physical displacement and may not silently detach the door.
645 const auto* room = ctx_ ? ctx_->GetCurrentRoomConst() : nullptr;
646 const size_t index = request.entities.front().index;
647 if (!room || index >= room->GetDoors().size()) {
648 return CommitSelectionEdit(request).ok();
649 }
650 const auto& door = room->GetDoors()[index];
651 const bool horizontal = door.direction == zelda3::DoorDirection::North ||
652 door.direction == zelda3::DoorDirection::South;
653 const int64_t next_position =
654 static_cast<int64_t>(door.position) + (horizontal ? delta_x : delta_y);
655 if (next_position == door.position)
656 return false;
657 if (next_position < 0 ||
660 static_cast<uint8_t>(next_position), door.direction)) {
662 absl::OutOfRangeError("No door slot in that direction"));
663 return false;
664 }
665 auto next = door;
666 next.position = static_cast<uint8_t>(next_position);
667 const auto [old_x, old_y] = door.GetPixelCoords();
668 const auto [new_x, new_y] = next.GetPixelCoords();
669 request.delta_x_pixels = new_x - old_x;
670 request.delta_y_pixels = new_y - old_y;
671 return CommitSelectionEdit(request).ok();
672 }
673 const int step = SelectionMoveStepPixels();
674 const int64_t dx = static_cast<int64_t>(delta_x) * step;
675 const int64_t dy = static_cast<int64_t>(delta_y) * step;
676 if (dx < -512 || dx > 512 || dy < -512 || dy > 512) {
678 absl::OutOfRangeError("Selection movement leaves the room"));
679 return false;
680 }
681 request.delta_x_pixels = static_cast<int>(dx);
682 request.delta_y_pixels = static_cast<int>(dy);
683 return CommitSelectionEdit(request).ok();
684}
685
698
705
707 const {
708 if (!selected_entities_.empty()) {
709 switch (selected_entities_.front().type) {
710 case EntityType::Door:
711 return Mode::PlaceDoor;
712 case EntityType::Sprite:
713 return Mode::PlaceSprite;
714 case EntityType::Item:
715 return Mode::PlaceItem;
716 case EntityType::Object:
717 case EntityType::None:
718 default:
719 break;
720 }
721 }
723 return Mode::PlaceDoor;
724 }
726 return Mode::PlaceSprite;
727 }
729 return Mode::PlaceItem;
730 }
731 return Mode::Select;
732}
733
735 switch (current_mode_) {
736 case Mode::PlaceDoor:
737 return &door_handler_;
739 return &sprite_handler_;
740 case Mode::PlaceItem:
741 return &item_handler_;
742 case Mode::Select:
743 default:
744 return nullptr;
745 }
746}
747
750
751 if (entity.type == EntityType::Object) {
752 if (!ctx_ || !ctx_->selection) {
753 return false;
754 }
758 return true;
759 }
760
761 if (ctx_ && ctx_->selection) {
763 }
764
765 switch (entity.type) {
766 case EntityType::Door:
767 selected_entities_.push_back(entity);
769 return true;
770 case EntityType::Sprite:
771 selected_entities_.push_back(entity);
773 return true;
774 case EntityType::Item:
775 selected_entities_.push_back(entity);
777 return true;
778 case EntityType::Object:
779 case EntityType::None:
780 default:
781 return false;
782 }
783}
784
786 bool additive, bool toggle) {
787 if (entity.type == EntityType::Object || entity.type == EntityType::None) {
788 return false;
789 }
790
794
795 if (!additive && !toggle) {
796 selected_entities_.clear();
797 }
798
799 const auto existing =
800 std::find(selected_entities_.begin(), selected_entities_.end(), entity);
801 if (toggle) {
802 if (existing != selected_entities_.end()) {
803 selected_entities_.erase(existing);
804 } else {
805 selected_entities_.push_back(entity);
806 }
807 } else if (existing == selected_entities_.end()) {
808 selected_entities_.push_back(entity);
809 }
810
811 if (selected_entities_.size() == 1) {
812 const auto selected = selected_entities_.front();
813 switch (selected.type) {
814 case EntityType::Door:
815 door_handler_.SelectDoor(selected.index);
816 break;
817 case EntityType::Sprite:
818 sprite_handler_.SelectSprite(selected.index);
819 break;
820 case EntityType::Item:
821 item_handler_.SelectItem(selected.index);
822 break;
823 case EntityType::Object:
824 case EntityType::None:
825 default:
826 break;
827 }
828 } else if (ctx_) {
830 }
831
832 return true;
833}
834
836 SelectedEntity entity) const {
837 if (entity.type == EntityType::Object) {
838 return ctx_ && ctx_->selection &&
840 }
841 return std::find(selected_entities_.begin(), selected_entities_.end(),
842 entity) != selected_entities_.end() ||
843 GetSelectedEntity() == entity;
844}
845
847 const bool has_object_selection =
849 return !selected_entities_.empty() &&
850 (selected_entities_.size() > 1 || has_object_selection);
851}
852
865
868 return;
869 }
870
872 const ImVec2 drag_delta(
875
876 const int step = SelectionMoveStepPixels();
877 const int drag_dx = static_cast<int>(drag_delta.x) / step * step;
878 const int drag_dy = static_cast<int>(drag_delta.y) / step * step;
879 const int inc_dx = drag_dx - entity_group_drag_last_dx_;
880 const int inc_dy = drag_dy - entity_group_drag_last_dy_;
881 if (inc_dx == 0 && inc_dy == 0)
882 return;
883
886 if (ctx_ && ctx_->selection)
889 request.delta_x_pixels = inc_dx;
890 request.delta_y_pixels = inc_dy;
891 if (CommitSelectionEdit(request, true).ok()) {
894 }
895}
896
898 const std::tuple<int, int, int, int>& bounds, bool additive, bool toggle) {
899 auto* room = ctx_ ? ctx_->GetCurrentRoomConst() : nullptr;
900 if (!room) {
901 return;
902 }
903
904 const auto [raw_min_x, raw_min_y, raw_max_x, raw_max_y] = bounds;
905 const int min_x = std::min(raw_min_x, raw_max_x);
906 const int max_x = std::max(raw_min_x, raw_max_x);
907 const int min_y = std::min(raw_min_y, raw_max_y);
908 const int max_y = std::max(raw_min_y, raw_max_y);
909 const int rect_w = std::max(1, max_x - min_x);
910 const int rect_h = std::max(1, max_y - min_y);
911
912 std::vector<SelectedEntity> hits;
913 for (size_t i = 0; i < room->GetDoors().size(); ++i) {
914 const SelectedEntity entity{EntityType::Door, i};
915 if (auto entity_bounds = GetEntityBounds(*room, entity)) {
916 auto [x, y, w, h] = *entity_bounds;
917 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
918 hits.push_back(entity);
919 }
920 }
921 }
922 for (size_t i = 0; i < room->GetSprites().size(); ++i) {
923 const SelectedEntity entity{EntityType::Sprite, i};
924 if (auto entity_bounds = GetEntityBounds(*room, entity)) {
925 auto [x, y, w, h] = *entity_bounds;
926 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
927 hits.push_back(entity);
928 }
929 }
930 }
931 for (size_t i = 0; i < room->GetPotItems().size(); ++i) {
932 const SelectedEntity entity{EntityType::Item, i};
933 if (auto entity_bounds = GetEntityBounds(*room, entity)) {
934 auto [x, y, w, h] = *entity_bounds;
935 if (Intersects(x, y, w, h, min_x, min_y, rect_w, rect_h)) {
936 hits.push_back(entity);
937 }
938 }
939 }
940
944 if (!additive && !toggle) {
945 selected_entities_.clear();
946 }
947
948 for (const auto entity : hits) {
949 auto existing =
950 std::find(selected_entities_.begin(), selected_entities_.end(), entity);
951 if (toggle) {
952 if (existing != selected_entities_.end()) {
953 selected_entities_.erase(existing);
954 } else {
955 selected_entities_.push_back(entity);
956 }
957 } else if (existing == selected_entities_.end()) {
958 selected_entities_.push_back(entity);
959 }
960 }
961
962 if (selected_entities_.size() == 1) {
963 UpdateEntitySelection(selected_entities_.front(), /*additive=*/false,
964 /*toggle=*/false);
965 } else if (ctx_) {
967 }
968}
969
971 int canvas_x, int canvas_y, const std::vector<SelectedEntity>& hits) const {
972 constexpr int kSameSpotTolerancePx = 3;
973 if (std::abs(canvas_x - cycle_last_x_) > kSameSpotTolerancePx ||
974 std::abs(canvas_y - cycle_last_y_) > kSameSpotTolerancePx ||
975 hits.size() != cycle_last_hits_.size()) {
976 return false;
977 }
978
979 for (size_t i = 0; i < hits.size(); ++i) {
980 if (!(hits[i] == cycle_last_hits_[i])) {
981 return false;
982 }
983 }
984 return true;
985}
986
988 const std::vector<SelectedEntity>& hits) const {
989 for (size_t i = 0; i < hits.size(); ++i) {
990 const SelectedEntity hit = hits[i];
991 if (hit.type == EntityType::Object) {
992 if (ctx_ && ctx_->selection &&
994 return i;
995 }
996 continue;
997 }
998
999 if (std::find(selected_entities_.begin(), selected_entities_.end(), hit) !=
1000 selected_entities_.end()) {
1001 return i;
1002 }
1003
1004 const SelectedEntity selected = GetSelectedEntity();
1005 if (selected == hit) {
1006 return i;
1007 }
1008 }
1009
1010 return std::nullopt;
1011}
1012
1014 if (!ctx_ || !ctx_->canvas || !ctx_->canvas->IsMouseHovering()) {
1015 return;
1016 }
1017
1018 const ImGuiIO& io = ImGui::GetIO();
1019 if (!IsCycleModifierHeld(io)) {
1020 return;
1021 }
1022
1023 const DungeonCanvasTransform transform(ctx_->canvas->zero_point(),
1024 ctx_->canvas->scrolling(),
1026 const auto [canvas_x, canvas_y] =
1027 transform.ScreenToRoomPixelCoordinates(io.MousePos);
1028 const auto hits = GetEntitiesAtPosition(canvas_x, canvas_y);
1029 if (hits.size() < 2) {
1030 cycle_last_hits_.clear();
1032 cycle_hud_start_time_ = -1.0;
1033 return;
1034 }
1035
1036 if (!SameCycleTarget(canvas_x, canvas_y, hits)) {
1038 }
1039
1040 cycle_last_x_ = canvas_x;
1041 cycle_last_y_ = canvas_y;
1042 cycle_hud_screen_pos_ = io.MousePos;
1043 cycle_hud_start_time_ = ImGui::GetCurrentContext() ? ImGui::GetTime() : 0.0;
1044 cycle_last_hits_ = hits;
1045 if (const auto selected_index = FindSelectedCycleIndex(hits)) {
1046 cycle_active_index_ = *selected_index;
1047 } else {
1048 cycle_active_index_ = cycle_next_index_ % hits.size();
1049 }
1050}
1051
1053 if (ImGui::GetCurrentContext()) {
1055 }
1056
1057 if (!ImGui::GetCurrentContext() || cycle_last_hits_.size() < 2 ||
1058 cycle_hud_start_time_ < 0.0) {
1059 return;
1060 }
1061
1062 const double elapsed = ImGui::GetTime() - cycle_hud_start_time_;
1063 const ImGuiIO& io = ImGui::GetIO();
1064 const bool cycle_modifier_held = IsCycleModifierHeld(io);
1065 if (!cycle_modifier_held && elapsed > kCycleHudHoldSeconds) {
1066 return;
1067 }
1068
1069 const float alpha =
1070 cycle_modifier_held
1071 ? 1.0f
1072 : std::max(0.0f,
1073 1.0f - static_cast<float>(elapsed / kCycleHudHoldSeconds));
1074 const auto& theme = AgentUI::GetTheme();
1075 ImVec4 bg = theme.panel_bg_darker;
1076 bg.w *= 0.92f * alpha;
1077 ImVec4 border = theme.panel_border_color;
1078 border.w *= alpha;
1079 ImVec4 text = theme.text_primary;
1080 text.w *= alpha;
1081 ImVec4 secondary = theme.text_secondary_color;
1082 secondary.w *= alpha;
1083 ImVec4 active = theme.accent_color;
1084 active.w *= alpha;
1085
1086 ImGui::SetNextWindowPos(
1087 ImVec2(cycle_hud_screen_pos_.x + 14.0f, cycle_hud_screen_pos_.y + 14.0f),
1088 ImGuiCond_Always);
1089 ImGui::SetNextWindowBgAlpha(bg.w);
1090 gui::StyleColorGuard hud_colors({{ImGuiCol_WindowBg, bg},
1091 {ImGuiCol_Border, border},
1092 {ImGuiCol_Text, text}});
1093 gui::StyleVarGuard hud_vars(
1094 {{ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 6.0f)},
1095 {ImGuiStyleVar_WindowRounding, 4.0f}});
1096 constexpr ImGuiWindowFlags kFlags =
1097 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
1098 ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav |
1099 ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs;
1100 if (ImGui::Begin("##DungeonSelectionCycleHud", nullptr, kFlags)) {
1101 ImGui::TextColored(secondary, tr("Cycle"));
1102 for (size_t i = 0; i < cycle_last_hits_.size(); ++i) {
1103 const char* marker = (i == cycle_active_index_) ? "[X]" : "[ ]";
1104 ImGui::TextColored(i == cycle_active_index_ ? active : secondary, "%s",
1105 marker);
1106 ImGui::SameLine(0.0f, 5.0f);
1107 ImGui::TextUnformatted(
1109 }
1110 }
1111 ImGui::End();
1112}
1113
1115 auto* room = ctx_ ? ctx_->GetCurrentRoomConst() : nullptr;
1116 if (!room || !ctx_ || !ctx_->canvas) {
1117 return;
1118 }
1119
1120 const auto& theme = AgentUI::GetTheme();
1121 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1122 const DungeonCanvasTransform transform(ctx_->canvas->zero_point(),
1123 ctx_->canvas->scrolling(),
1125 const float scale = transform.scale();
1126 const float pulse =
1127 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 6.0f);
1128
1129 for (size_t i = 0; i < selected_entities_.size(); ++i) {
1130 const auto entity = selected_entities_[i];
1131 const auto bounds = GetEntityBounds(*room, entity);
1132 if (!bounds.has_value()) {
1133 continue;
1134 }
1135
1136 auto [x, y, w, h] = *bounds;
1137 ImVec2 start = transform.RoomPixelsToScreen(
1138 ImVec2(static_cast<float>(x), static_cast<float>(y)));
1139 const ImVec2 size = transform.RoomSizeToScreen(
1140 ImVec2(static_cast<float>(w), static_cast<float>(h)));
1141 ImVec2 end(start.x + size.x, start.y + size.y);
1142 constexpr float kMargin = 2.0f;
1143 start.x -= kMargin;
1144 start.y -= kMargin;
1145 end.x += kMargin;
1146 end.y += kMargin;
1147
1148 ImVec4 base = EntitySelectionColor(theme, entity.type);
1149 ImVec4 fill = base;
1150 fill.w = 0.14f + 0.10f * pulse;
1151 ImVec4 border = base;
1152 border.w = (i == 0) ? 0.95f : 0.72f;
1153
1154 draw_list->AddRectFilled(start, end, ImGui::GetColorU32(fill));
1155 draw_list->AddRect(start, end, ImGui::GetColorU32(border), 0.0f, 0,
1156 (i == 0) ? 2.2f : 1.6f);
1157 draw_list->AddText(ImVec2(start.x, start.y - 14.0f * scale),
1158 ImGui::GetColorU32(theme.text_primary),
1159 DescribeEntity(entity).c_str());
1160 }
1161}
1162
1164 SelectedEntity entity) const {
1165 const zelda3::Room* room = ctx_ ? ctx_->GetCurrentRoomConst() : nullptr;
1166 switch (entity.type) {
1167 case EntityType::Door: {
1168 if (room && entity.index < room->GetDoors().size()) {
1169 const auto& door = room->GetDoors()[entity.index];
1170 const std::string direction_name(
1171 zelda3::GetDoorDirectionName(door.direction));
1172 return absl::StrFormat("Door (%s)", direction_name);
1173 }
1174 return "Door";
1175 }
1176 case EntityType::Sprite:
1177 if (room && entity.index < room->GetSprites().size()) {
1178 const auto& sprite = room->GetSprites()[entity.index];
1179 return absl::StrFormat("Sprite (0x%02X - %s)", sprite.id(),
1180 zelda3::ResolveSpriteName(sprite.id()));
1181 }
1182 return "Sprite";
1183 case EntityType::Item:
1184 if (room && entity.index < room->GetPotItems().size()) {
1185 const auto& item = room->GetPotItems()[entity.index];
1186 return absl::StrFormat("Item (0x%02X)", item.item);
1187 }
1188 return "Item";
1189 case EntityType::Object:
1190 if (room && entity.index < room->GetTileObjects().size()) {
1191 const auto& object = room->GetTileObjects()[entity.index];
1192 return absl::StrFormat("Object (0x%03X - %s)", object.id_,
1193 zelda3::GetObjectName(object.id_));
1194 }
1195 return "Object";
1196 case EntityType::None:
1197 default:
1198 return "None";
1199 }
1200}
1201
1203 SelectedEntity entity) const {
1204 return TruncateCycleHudLabel(DescribeEntity(entity));
1205}
1206
1207} // namespace yaze::editor
Abstract base class for entity interaction handlers.
virtual bool HandleMouseWheel(float delta)
void SetContext(InteractionContext *ctx)
Set the interaction context.
bool HandleOverlayClick(int canvas_x, int canvas_y)
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
void DrawSnapIndicators()
Draw snap position indicators during door drag.
void SelectDoor(size_t index)
Select door at index.
void DrawGhostPreview() override
Draw ghost preview during placement.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
void CancelPlacement() override
Cancel current placement.
void BeginPlacement() override
Begin placement mode.
void HandleRelease() override
Handle mouse release.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
std::optional< size_t > GetSelectedIndex() const
Get selected door index.
bool IsPlacementActive() const override
Check if placement mode is active.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
bool HasSelection() const
Check if a door is selected.
std::pair< int, int > ScreenToRoomPixelCoordinates(ImVec2 screen) const
ImVec2 RoomSizeToScreen(ImVec2 room_size) const
std::vector< SelectedEntity > SelectedEntitiesForEdit() const
absl::Status ReportSelectionEditStatus(absl::Status status)
std::vector< SelectedEntity > GetEntitiesAtPosition(int canvas_x, int canvas_y) const
bool IsSelectionHitSelected(SelectedEntity entity) const
bool TrySelectEntityAtCursor(int canvas_x, int canvas_y)
Try to select entity at cursor position.
Mode GetSelectedEntityType() const
Get the type of currently selected entity.
std::string DescribeCycleHudEntity(SelectedEntity entity) const
void SelectEntitiesInRect(const std::tuple< int, int, int, int > &bounds, bool additive, bool toggle)
void CancelCurrentMode()
Cancel current mode and return to select mode.
void SetContext(InteractionContext *ctx)
Set the shared interaction context.
std::vector< SelectedEntity > selected_entities_
bool UpdateEntitySelection(SelectedEntity entity, bool additive, bool toggle)
std::optional< size_t > FindSelectedCycleIndex(const std::vector< SelectedEntity > &hits) const
BaseEntityHandler * GetActiveHandler()
Get active handler based on current mode.
bool IsPlacementActive() const
Check if any placement mode is active.
bool HandleClick(int canvas_x, int canvas_y)
Handle click at canvas position.
void SetSelectedEntities(std::vector< SelectedEntity > entities)
std::string DescribeEntity(SelectedEntity entity) const
void SelectEntity(EntityType type, size_t index)
void DrawPostPlacementOverlays()
Draw post-placement success toasts for all handlers (unconditional)
bool NudgeSelected(int delta_x, int delta_y)
void DrawSelectionHighlights()
Draw selection highlights for all entity types.
absl::Status DeleteSelectedEntity()
Delete currently selected entity.
void SetMode(Mode mode)
Set interaction mode.
void ClearAllEntitySelections()
Clear all entity selections.
absl::Status CommitSelectionEdit(const DungeonSelectionEditRequest &request, bool continuous=false)
void DrawGhostPreviews()
Draw ghost previews for active placement mode.
bool SameCycleTarget(int canvas_x, int canvas_y, const std::vector< SelectedEntity > &hits) const
std::vector< SelectedEntity > cycle_last_hits_
std::optional< SelectedEntity > GetEntityAtPosition(int canvas_x, int canvas_y) const
void HandleDrag(ImVec2 current_pos, ImVec2 delta)
Handle drag operation.
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
bool IsPlacementActive() const override
Check if placement mode is active.
void DrawGhostPreview() override
Draw ghost preview during placement.
void BeginPlacement() override
Begin placement mode.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
bool HasSelection() const
Check if an item is selected.
void HandleRelease() override
Handle mouse release.
std::optional< size_t > GetSelectedIndex() const
Get selected item index.
void SelectItem(size_t index)
Select item at index.
void CancelPlacement() override
Cancel current placement.
bool IsObjectSelected(size_t index) const
Check if an object is selected.
std::vector< size_t > GetSelectedIndices() const
Get all selected object indices.
size_t GetSelectionCount() const
Get the number of selected objects.
void SelectObject(size_t index, SelectionMode mode=SelectionMode::Single)
Select a single object by index.
void ClearSelection()
Clear all selections.
bool HasSelection() const
Check if any objects are selected.
bool IsPlacementActive() const override
Check if placement mode is active.
void CancelPlacement() override
Cancel current placement.
void HandleRelease() override
Handle mouse release.
void SelectSprite(size_t index)
Select sprite at index.
bool HasSelection() const
Check if a sprite is selected.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void DrawGhostPreview() override
Draw ghost preview during placement.
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
std::optional< size_t > GetSelectedIndex() const
Get selected sprite index.
void BeginPlacement() override
Begin placement mode.
void DrawGhostPreview() override
Draw ghost preview during placement.
bool HandleMouseWheel(float delta) override
void HandleRelease() override
Handle mouse release.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
bool IsPlacementActive() const override
Check if placement mode is active.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
void CancelPlacement() override
Cancel current placement.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
auto global_scale() const
Definition canvas.h:413
auto zero_point() const
Definition canvas.h:364
bool IsMouseHovering() const
Definition canvas.h:354
auto scrolling() const
Definition canvas.h:366
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
static constexpr int kMaxDoorPositions
static bool IsValidPosition(uint8_t position, DoorDirection direction)
Check if a position is valid for door placement.
const std::vector< Door > & GetDoors() const
Definition room.h:413
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
const std::vector< PotItem > & GetPotItems() const
Definition room.h:429
const AgentUITheme & GetTheme()
std::optional< std::tuple< int, int, int, int > > GetEntityBounds(const zelda3::Room &room, SelectedEntity entity)
ImVec4 EntitySelectionColor(const AgentUITheme &theme, EntityType type)
bool Intersects(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh)
bool IsWithinBounds(int canvas_x, int canvas_y, int margin=0)
Check if coordinates are within room bounds.
ImVec2 SnapToTileGrid(const ImVec2 &point)
Snap a point to the 8px tile grid.
Editors are the view controllers for the application.
absl::StatusOr< DungeonSelectionEditPlan > PlanDungeonSelectionEdit(const zelda3::Room &room, const DungeonSelectionEditRequest &request)
constexpr uint8_t kSelectionSprites
constexpr uint8_t kSelectionObjects
constexpr uint8_t kSelectionDoors
void ApplyDungeonSelectionEditState(zelda3::Room &room, const DungeonSelectionEditState &state, uint8_t domains)
EntityType
Type of entity that can be selected in the dungeon editor.
constexpr uint8_t kSelectionItems
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
std::string GetObjectName(int object_id)
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:287
Centralized theme colors for Agent UI components.
Definition agent_theme.h:19
Shared context for all interaction handlers.
const zelda3::Room * GetCurrentRoomConst() const
Get const pointer to current room.
void NotifyEntityChanged() const
Notify that entity has changed.
std::function< void(const absl::Status &) on_selection_edit_error)
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.
std::function< void()> on_selection_edit_finished
zelda3::Room * GetCurrentRoom() const
Get pointer to current room.
std::function< absl::Status(const DungeonSelectionEditPlan &, bool continuous) on_selection_edit)
Represents a selected entity in the dungeon editor.