6#include <unordered_set>
7#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
35 SDL_Surface* surface = source->surface();
37 if (!palette || palette->ncolors <= 0) {
41 std::vector<SDL_Color> colors(256, {0, 0, 0, 0});
42 const int color_count = std::min(palette->ncolors, 256);
43 std::copy_n(palette->colors, color_count, colors.begin());
44 colors[255] = {0, 0, 0, 0};
47 SDL_SetColorKey(ghost.
surface(), SDL_TRUE, 255);
48 SDL_SetSurfaceBlendMode(ghost.
surface(), SDL_BLENDMODE_BLEND);
57 bool selected =
false;
61 return std::clamp(
static_cast<int>(
object.GetLayerValue()), 0, 2);
65 size_t object_count) {
66 std::unordered_set<size_t> index_set;
67 for (
size_t index : indices) {
68 if (index < object_count) {
69 index_set.insert(index);
76 const std::vector<zelda3::RoomObject>& objects,
77 const std::unordered_set<size_t>& selected_indices) {
78 std::array<std::vector<LayerOrderEntry>, 3> buckets;
79 for (
size_t i = 0; i < objects.size(); ++i) {
88 const std::vector<size_t>& selected_indices_after_reorder) {
93 for (
size_t index : selected_indices_after_reorder) {
99 std::array<std::vector<LayerOrderEntry>, 3>& buckets,
101 std::vector<zelda3::RoomObject> reordered;
102 std::vector<size_t> selected_indices_after_reorder;
103 reordered.reserve(objects.size());
104 for (
auto& bucket : buckets) {
105 for (
auto& entry : bucket) {
106 if (entry.selected) {
107 selected_indices_after_reorder.push_back(reordered.size());
109 reordered.push_back(std::move(entry.object));
112 objects = std::move(reordered);
126 auto [x, y, width, height] =
128 return GuideRect{x, y, x + width, y + height, x + width / 2, y + height / 2};
132 constexpr int kDuplicateTolerancePx = 2;
133 if (std::any_of(guides.begin(), guides.end(), [&](
int guide) {
134 return std::abs(guide - value) <= kDuplicateTolerancePx;
138 guides.push_back(value);
143 ImU32 color,
float thickness) {
144 constexpr float kDash = 6.0f;
145 constexpr float kGap = 4.0f;
146 const bool vertical = std::abs(start.x - end.x) < 0.5f;
148 vertical ? std::abs(end.y - start.y) : std::abs(end.x - start.x);
149 for (
float offset = 0.0f; offset < total; offset += kDash + kGap) {
150 const float segment_end = std::min(offset + kDash, total);
152 const float y0 = start.y + offset;
153 const float y1 = start.y + segment_end;
154 draw_list->AddLine(ImVec2(start.x, y0), ImVec2(start.x, y1), color,
157 const float x0 = start.x + offset;
158 const float x1 = start.x + segment_end;
159 draw_list->AddLine(ImVec2(x0, start.y), ImVec2(x1, start.y), color,
170 return GhostCapacityState::kNormal;
173 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
180 if (room_id < 0 || room_id >=
static_cast<int>(
ctx_->
rooms->
size())) {
211 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
223 if (!hovered.has_value())
226 const ImGuiIO& io = ImGui::GetIO();
236 }
else if (io.KeyCtrl || io.KeySuper) {
258 static_cast<int>(start_pos.y));
262 const ImVec2& mouse_pos,
bool mouse_left_down,
bool mouse_left_released,
263 bool shift_down,
bool toggle_down,
bool alt_down,
bool draw_box) {
272 if (mouse_left_down) {
274 static_cast<int>(mouse_pos.y));
283 if (mouse_left_released) {
285 static_cast<int>(mouse_pos.y));
293 constexpr int kMinRectPixels = 6;
316 const int tile_dx =
static_cast<int>(drag_delta.x) / 8;
317 const int tile_dy =
static_cast<int>(drag_delta.y) / 8;
323 if (inc_dx != 0 || inc_dy != 0) {
348 if (had_mutation &&
ctx_) {
366 int resize_delta = (delta > 0.0f) ? 1 : -1;
375 if (!pointer_screen_pos.has_value()) {
380 const auto [canvas_x, canvas_y] =
382 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
387 auto [snap_canvas_x, snap_canvas_y] =
RoomToCanvas(room_x, room_y);
390 ImDrawList* draw_list = ImGui::GetWindowDrawList();
392 static_cast<float>(snap_canvas_x + preview_geometry.offset_x_tiles * 8),
393 static_cast<float>(snap_canvas_y + preview_geometry.offset_y_tiles * 8)));
395 ImVec2(
static_cast<float>(preview_geometry.width_pixels),
396 static_cast<float>(preview_geometry.height_pixels)));
397 const ImVec2 preview_end(preview_start.x + preview_size.x,
398 preview_start.y + preview_size.y);
400 ImVec2(
static_cast<float>(snap_canvas_x -
401 preview_geometry.render_anchor_x_tiles * 8),
402 static_cast<float>(snap_canvas_y -
403 preview_geometry.render_anchor_y_tiles * 8)));
406 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
411 theme, capacity_state, theme.dungeon_selection_primary);
412 bool drew_bitmap =
false;
416 if (bitmap.texture()) {
417 const int crop_width =
418 std::min(preview_geometry.buffer_width_pixels, bitmap.width());
419 const int crop_height =
420 std::min(preview_geometry.buffer_height_pixels, bitmap.height());
422 static_cast<float>(crop_width),
static_cast<float>(crop_height)));
423 const ImVec2 bitmap_end(bitmap_start.x + bitmap_size.x,
424 bitmap_start.y + bitmap_size.y);
425 const ImVec2 uv_end(
static_cast<float>(crop_width) / bitmap.width(),
426 static_cast<float>(crop_height) / bitmap.height());
427 ImVec4 tint = capacity_state == GhostCapacityState::kNormal
432 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), bitmap_start,
433 bitmap_end, ImVec2(0, 0), uv_end,
434 ImGui::GetColorU32(tint));
435 draw_list->AddRect(preview_start, preview_end,
436 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
442 draw_list->AddRectFilled(
443 preview_start, preview_end,
445 ImVec4(outline_color.x, outline_color.y, outline_color.z, 0.25f)));
446 draw_list->AddRect(preview_start, preview_end,
447 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
452 draw_list->AddText(ImVec2(preview_start.x + 2, preview_start.y + 1),
453 ImGui::GetColorU32(theme.text_primary), id_text.c_str());
456 if (capacity_state != GhostCapacityState::kNormal &&
457 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
458 ImGui::SetTooltip(tr(
"Objects: %zu/%zu\n%s"), current_obj_count,
463 const std::string badge_text = absl::StrFormat(
466 ImVec2(preview_start.x, preview_end.y + 6.0f),
467 theme, capacity_state, badge_text);
481 auto result = zelda3::DimensionService::Get().GetDimensions(obj);
482 return std::make_tuple(result.offset_x_tiles * 8,
483 result.offset_y_tiles * 8, result.width_pixels(),
484 result.height_pixels());
493 int canvas_x,
int canvas_y)
const {
503 for (
size_t i = objects.size(); i > 0; --i) {
504 size_t index = i - 1;
505 const auto&
object = objects[index];
513 auto [obj_tile_x, obj_tile_y, width_tiles, height_tiles] =
516 int obj_px = obj_tile_x * 8;
517 int obj_py = obj_tile_y * 8;
518 int w_px = width_tiles * 8;
519 int h_px = height_tiles * 8;
521 if (canvas_x >= obj_px && canvas_x < obj_px + w_px && canvas_y >= obj_py &&
522 canvas_y < obj_py + h_px) {
530 const std::vector<zelda3::RoomObject>& objects)
const {
537 if (selected.empty()) {
541 constexpr int kAlignmentTolerancePx = 2;
542 constexpr size_t kMaxGuidesPerAxis = 8;
543 std::vector<int> vertical_guides;
544 std::vector<int> horizontal_guides;
546 for (
size_t selected_index : selected) {
547 if (selected_index >= objects.size()) {
550 const GuideRect selected_rect = GetGuideRect(objects[selected_index]);
551 const std::array<int, 3> selected_x = {
552 selected_rect.left, selected_rect.center_x, selected_rect.right};
553 const std::array<int, 3> selected_y = {
554 selected_rect.top, selected_rect.center_y, selected_rect.bottom};
556 for (
size_t other_index = 0; other_index < objects.size(); ++other_index) {
560 const GuideRect other_rect = GetGuideRect(objects[other_index]);
561 const std::array<int, 3> other_x = {other_rect.left, other_rect.center_x,
563 const std::array<int, 3> other_y = {other_rect.top, other_rect.center_y,
566 for (
int sx : selected_x) {
567 for (
int ox : other_x) {
568 if (std::abs(sx - ox) <= kAlignmentTolerancePx) {
569 AddUniqueGuide(vertical_guides, ox);
573 for (
int sy : selected_y) {
574 for (
int oy : other_y) {
575 if (std::abs(sy - oy) <= kAlignmentTolerancePx) {
576 AddUniqueGuide(horizontal_guides, oy);
580 if (vertical_guides.size() >= kMaxGuidesPerAxis &&
581 horizontal_guides.size() >= kMaxGuidesPerAxis) {
587 if (vertical_guides.empty() && horizontal_guides.empty()) {
592 ImVec4 guide_color = theme.accent_color;
593 guide_color.w = 0.78f;
594 const ImU32 color = ImGui::GetColorU32(guide_color);
595 ImDrawList* draw_list = ImGui::GetWindowDrawList();
601 const size_t vertical_count =
602 std::min(vertical_guides.size(), kMaxGuidesPerAxis);
603 for (
size_t i = 0; i < vertical_count; ++i) {
606 DrawDashedLine(draw_list, ImVec2(x, canvas_pos.y),
607 ImVec2(x, canvas_pos.y + room_size.y), color, 1.2f);
610 const size_t horizontal_count =
611 std::min(horizontal_guides.size(), kMaxGuidesPerAxis);
612 for (
size_t i = 0; i < horizontal_count; ++i) {
615 DrawDashedLine(draw_list, ImVec2(canvas_pos.x, y),
616 ImVec2(canvas_pos.x + room_size.x, y), color, 1.2f);
625 const std::vector<size_t>& indices,
626 int delta_x,
int delta_y,
627 bool notify_mutation) {
629 if (!room || indices.empty())
631 if (notify_mutation &&
ctx_)
634 auto& objects = room->GetTileObjects();
635 for (
size_t index : indices) {
636 if (index < objects.size()) {
638 std::clamp(
static_cast<int>(objects[index].x_ + delta_x), 0, 63);
640 std::clamp(
static_cast<int>(objects[index].y_ + delta_y), 0, 63);
648 const std::vector<size_t>& indices,
651 if (!room || indices.empty())
654 auto& objects = room->GetTileObjects();
655 const bool has_change =
656 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
657 return index < objects.size() && objects[index].id_ != new_id;
665 for (
size_t index : indices) {
666 if (index < objects.size() && objects[index].id_ != new_id) {
667 const uint8_t canonical_size =
670 objects[index].set_id(new_id);
671 objects[index].set_size(canonical_size);
678 const std::vector<size_t>& indices,
681 if (!room || indices.empty())
684 auto& objects = room->GetTileObjects();
685 const bool has_change =
686 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
687 if (index >= objects.size() ||
688 !zelda3::IsRoomObjectSizeEditable(objects[index].id_)) {
691 return objects[index].size_ !=
700 for (
size_t index : indices) {
701 if (index < objects.size() &&
703 const uint8_t canonical_size =
705 if (objects[index].size_ == canonical_size) {
708 objects[index].size_ = canonical_size;
709 objects[index].tiles_loaded_ =
false;
715bool TileObjectHandler::UpdateObjectsLayer(
int room_id,
716 const std::vector<size_t>& indices,
718 auto* room = GetRoom(room_id);
719 if (!room || indices.empty())
721 if (new_layer < 0 || new_layer > 2) {
723 "Rejected layer update with invalid target layer: %d", new_layer);
726 auto& objects = room->GetTileObjects();
727 std::vector<size_t> deduped_indices;
728 deduped_indices.reserve(indices.size());
729 std::unordered_set<size_t> seen_indices;
730 for (
size_t index : indices) {
731 if (index >= objects.size()) {
734 if (seen_indices.insert(index).second) {
735 deduped_indices.push_back(index);
738 if (deduped_indices.empty()) {
742 if (deduped_indices.size() > kMaxLayerBatchMutation) {
744 "Rejected layer batch mutation of %zu objects (max %zu)",
745 deduped_indices.size(), kMaxLayerBatchMutation);
749 auto candidate_objects = objects;
751 deduped_indices, new_layer);
752 if (!mutation.ok()) {
753 LOG_WARN(
"TileObjectHandler",
"Rejected object stream mutation: %s",
754 std::string(mutation.status().message()).c_str());
757 if (!mutation->changed) {
762 ctx_->NotifyMutation(MutationDomain::kTileObjects);
763 objects = std::move(candidate_objects);
764 RestoreObjectSelection(ctx_ ? ctx_->selection :
nullptr,
765 mutation->selected_indices);
770std::vector<size_t> TileObjectHandler::DuplicateObjects(
771 int room_id,
const std::vector<size_t>& indices,
int delta_x,
int delta_y,
772 bool notify_mutation) {
773 auto* room = GetRoom(room_id);
774 if (!room || indices.empty())
776 if (notify_mutation && ctx_)
777 ctx_->NotifyMutation(MutationDomain::kTileObjects);
779 auto& objects = room->GetTileObjects();
780 std::vector<size_t> new_indices;
782 const size_t base_index = objects.size();
783 for (
size_t index : indices) {
784 if (index < objects.size()) {
785 auto clone = objects[index].CopyForNewPlacement();
786 clone.x_ = std::clamp(
static_cast<int>(clone.x_ + delta_x), 0, 63);
787 clone.y_ = std::clamp(
static_cast<int>(clone.y_ + delta_y), 0, 63);
788 objects.push_back(clone);
789 new_indices.push_back(base_index + (new_indices.size()));
797void TileObjectHandler::DeleteObjects(
int room_id,
798 std::vector<size_t> indices) {
799 auto* room = GetRoom(room_id);
800 if (!room || indices.empty())
803 ctx_->NotifyMutation(MutationDomain::kTileObjects);
805 std::sort(indices.rbegin(), indices.rend());
806 for (
size_t index : indices) {
807 room->RemoveTileObject(index);
813void TileObjectHandler::DeleteAllObjects(
int room_id) {
814 auto* room = GetRoom(room_id);
818 ctx_->NotifyMutation(MutationDomain::kTileObjects);
819 room->ClearTileObjects();
823void TileObjectHandler::SendToFront(
int room_id,
824 const std::vector<size_t>& indices) {
825 auto* room = GetRoom(room_id);
826 if (!room || indices.empty())
828 auto& objects = room->GetTileObjects();
829 auto selected_set = MakeValidIndexSet(indices, objects.size());
830 if (selected_set.empty()) {
834 ctx_->NotifyMutation(MutationDomain::kTileObjects);
835 auto buckets = BuildLayerBuckets(objects, selected_set);
836 for (
auto& bucket : buckets) {
837 std::vector<LayerOrderEntry> other;
838 std::vector<LayerOrderEntry> selected;
839 other.reserve(bucket.size());
840 selected.reserve(bucket.size());
841 for (
auto& entry : bucket) {
842 if (entry.selected) {
843 selected.push_back(std::move(entry));
845 other.push_back(std::move(entry));
848 bucket = std::move(other);
849 bucket.insert(bucket.end(), std::make_move_iterator(selected.begin()),
850 std::make_move_iterator(selected.end()));
852 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
856void TileObjectHandler::SendToBack(
int room_id,
857 const std::vector<size_t>& indices) {
858 auto* room = GetRoom(room_id);
859 if (!room || indices.empty())
861 auto& objects = room->GetTileObjects();
862 auto selected_set = MakeValidIndexSet(indices, objects.size());
863 if (selected_set.empty()) {
867 ctx_->NotifyMutation(MutationDomain::kTileObjects);
868 auto buckets = BuildLayerBuckets(objects, selected_set);
869 for (
auto& bucket : buckets) {
870 std::vector<LayerOrderEntry> selected;
871 std::vector<LayerOrderEntry> other;
872 selected.reserve(bucket.size());
873 other.reserve(bucket.size());
874 for (
auto& entry : bucket) {
875 if (entry.selected) {
876 selected.push_back(std::move(entry));
878 other.push_back(std::move(entry));
881 bucket = std::move(selected);
882 bucket.insert(bucket.end(), std::make_move_iterator(other.begin()),
883 std::make_move_iterator(other.end()));
885 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
889void TileObjectHandler::MoveForward(
int room_id,
890 const std::vector<size_t>& indices) {
891 auto* room = GetRoom(room_id);
892 if (!room || indices.empty())
894 auto& objects = room->GetTileObjects();
895 auto selected_set = MakeValidIndexSet(indices, objects.size());
896 if (selected_set.empty()) {
900 ctx_->NotifyMutation(MutationDomain::kTileObjects);
901 auto buckets = BuildLayerBuckets(objects, selected_set);
902 for (
auto& bucket : buckets) {
903 if (bucket.size() < 2) {
906 for (
size_t i = bucket.size() - 1; i > 0; --i) {
907 const size_t previous = i - 1;
908 if (bucket[previous].selected && !bucket[i].selected) {
909 std::swap(bucket[previous], bucket[i]);
913 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
917void TileObjectHandler::MoveBackward(
int room_id,
918 const std::vector<size_t>& indices) {
919 auto* room = GetRoom(room_id);
920 if (!room || indices.empty())
922 auto& objects = room->GetTileObjects();
923 auto selected_set = MakeValidIndexSet(indices, objects.size());
924 if (selected_set.empty()) {
928 ctx_->NotifyMutation(MutationDomain::kTileObjects);
929 auto buckets = BuildLayerBuckets(objects, selected_set);
930 for (
auto& bucket : buckets) {
931 if (bucket.size() < 2) {
934 for (
size_t i = 1; i < bucket.size(); ++i) {
935 const size_t previous = i - 1;
936 if (bucket[i].selected && !bucket[previous].selected) {
937 std::swap(bucket[i], bucket[previous]);
941 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
945bool TileObjectHandler::ResizeObjects(
int room_id,
946 const std::vector<size_t>& indices,
948 auto* room = GetRoom(room_id);
949 if (!room || indices.empty())
951 auto& objects = room->GetTileObjects();
953 const int requested_size =
static_cast<int>(
object.size_) + delta;
955 object.id_,
static_cast<uint8_t
>(std::clamp(requested_size, 0, 255)));
957 const bool has_change =
958 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
959 return index < objects.size() &&
960 zelda3::IsRoomObjectSizeEditable(objects[index].id_) &&
961 objects[index].size_ != resized_size(objects[index]);
967 ctx_->NotifyMutation(MutationDomain::kTileObjects);
969 for (
size_t index : indices) {
970 if (index < objects.size() &&
972 const uint8_t new_size = resized_size(objects[index]);
973 if (objects[index].size_ == new_size) {
976 objects[index].size_ = new_size;
977 objects[index].tiles_loaded_ =
false;
984bool TileObjectHandler::PlaceObjectAt(
int room_id,
987 auto* room = GetRoom(room_id);
989 placement_block_reason_ = PlacementBlockReason::kInvalidRoom;
995 placement_block_reason_ = PlacementBlockReason::kObjectLimit;
999 placement_block_reason_ = PlacementBlockReason::kNone;
1001 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1002 auto new_obj =
object.CopyForNewPlacement();
1003 new_obj.x_ = std::clamp(x, 0, 63);
1004 new_obj.y_ = std::clamp(y, 0, 63);
1005 room->AddTileObject(new_obj);
1007 TriggerSuccessToast();
1012 preview_object_ = object;
1013 if (object_placement_mode_) {
1014 RenderGhostPreviewBitmap();
1018void TileObjectHandler::RenderGhostPreviewBitmap() {
1019 ghost_preview_bitmap_ready_ =
false;
1020 if (!ctx_ || !ctx_->rom || !ctx_->rom->is_loaded())
1023 auto* room = GetRoom(ctx_->current_room_id);
1024 if (!room || !room->IsLoaded())
1027 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
1032 if (!ghost_preview_buffer_) {
1033 ghost_preview_buffer_ = std::make_unique<gfx::BackgroundBuffer>(
1034 kGhostPreviewBufferSize, kGhostPreviewBufferSize);
1036 ghost_preview_buffer_->EnsureBitmapInitialized();
1037 ghost_preview_buffer_->ClearBuffer();
1038 auto& bitmap = ghost_preview_buffer_->bitmap();
1039 ApplyRoomPaletteToGhost(*room, bitmap);
1040 std::fill(bitmap.mutable_data().begin(), bitmap.mutable_data().end(), 255);
1041 bitmap.set_modified(
true);
1042 const uint8_t* gfx_data = room->get_gfx_buffer().data();
1049 auto render_object = preview_object_;
1050 render_object.x_ = preview_geometry.render_anchor_x_tiles;
1051 render_object.y_ = preview_geometry.render_anchor_y_tiles;
1057 drawer.
DrawObject(render_object, *ghost_preview_buffer_,
1058 *ghost_preview_buffer_, ctx_->current_palette_group);
1063 if (bitmap.size() > 0) {
1064 bitmap.UpdateSurfacePixels();
1065 if (bitmap.texture()) {
1066 ghost_preview_create_queued_ =
false;
1069 }
else if (!ghost_preview_create_queued_) {
1072 ghost_preview_create_queued_ =
true;
1075 if (bitmap.texture()) {
1076 ghost_preview_create_queued_ =
false;
1078 ghost_preview_bitmap_ready_ =
true;
1083TileObjectHandler::CalculateGhostPreviewGeometry(
1086 const auto [resolved_anchor_x, resolved_anchor_y] =
1090 const int anchor_x =
1091 std::max({0, resolved_anchor_x, -dimensions.offset_x_tiles});
1092 const int anchor_y =
1093 std::max({0, resolved_anchor_y, -dimensions.offset_y_tiles});
1094 const int leading_x_tiles = anchor_x + dimensions.offset_x_tiles;
1095 const int leading_y_tiles = anchor_y + dimensions.offset_y_tiles;
1098 .render_anchor_y_tiles = anchor_y,
1099 .offset_x_tiles = dimensions.offset_x_tiles,
1100 .offset_y_tiles = dimensions.offset_y_tiles,
1101 .width_pixels = dimensions.width_pixels(),
1102 .height_pixels = dimensions.height_pixels(),
1103 .buffer_width_pixels = (leading_x_tiles + dimensions.width_tiles) * 8,
1104 .buffer_height_pixels = (leading_y_tiles + dimensions.height_tiles) * 8,
1112void TileObjectHandler::CopyObjectsToClipboard(
1113 int room_id,
const std::vector<size_t>& indices) {
1114 auto* room = GetRoom(room_id);
1115 if (!room || indices.empty())
1119 const auto& objects = room->GetTileObjects();
1121 for (
size_t idx : indices) {
1122 if (idx < objects.size()) {
1123 clipboard_.push_back(objects[idx]);
1128std::vector<size_t> TileObjectHandler::PasteFromClipboard(
int room_id,
1131 auto* room = GetRoom(room_id);
1132 if (!room || clipboard_.empty())
1135 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1137 std::vector<size_t> new_indices;
1138 size_t base_index = room->GetTileObjects().size();
1140 for (
auto obj : clipboard_) {
1141 obj = obj.CopyForNewPlacement();
1142 obj.x_ = std::clamp(obj.x_ + offset_x, 0, 63);
1143 obj.y_ = std::clamp(obj.y_ + offset_y, 0, 63);
1144 obj.tiles_loaded_ =
false;
1145 room->AddTileObject(obj);
1146 new_indices.push_back(base_index++);
1153std::vector<size_t> TileObjectHandler::PasteFromClipboardAt(
int room_id,
1156 if (clipboard_.empty())
1159 int offset_x = target_x - clipboard_[0].x_;
1160 int offset_y = target_y - clipboard_[0].y_;
1162 return PasteFromClipboard(room_id, offset_x, offset_y);
std::optional< ImVec2 > GetPointerScreenPosition() const
DungeonCanvasTransform GetCanvasTransform() const
std::pair< int, int > CanvasToRoom(int canvas_x, int canvas_y) const
Convert canvas pixel coordinates to room tile coordinates.
InteractionContext * ctx_
bool IsWithinBounds(int canvas_x, int canvas_y) const
Check if coordinates are within room bounds.
zelda3::Room * GetCurrentRoom() const
Get current room (convenience method)
bool HasValidContext() const
Check if context is valid.
std::pair< int, int > RoomToCanvas(int room_x, int room_y) const
Convert room tile coordinates to canvas pixel coordinates.
Manages object selection state and operations for the dungeon editor.
bool IsRectangleSelectionActive() const
Check if a rectangle selection is in progress.
void UpdateRectangleSelection(int canvas_x, int canvas_y)
Update rectangle selection endpoint.
void DrawSelectionHighlights(gui::Canvas *canvas, const std::vector< zelda3::RoomObject > &objects, std::function< std::tuple< int, int, int, int >(const zelda3::RoomObject &)> bounds_calculator)
Draw selection highlights for all selected objects.
bool IsObjectSelected(size_t index) const
Check if an object is selected.
std::vector< size_t > GetSelectedIndices() const
Get all selected object indices.
bool PassesLayerFilterForObject(const zelda3::RoomObject &object) const
Check if an object passes the current layer filter.
bool IsRectangleLargeEnough(int min_pixels) const
Check if rectangle selection exceeds a minimum pixel size.
void SelectObject(size_t index, SelectionMode mode=SelectionMode::Single)
Select a single object by index.
void ClearSelection()
Clear all selections.
void EndRectangleSelection(const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single)
Complete rectangle selection operation.
void BeginRectangleSelection(int canvas_x, int canvas_y)
Begin a rectangle selection operation.
void CancelRectangleSelection()
Cancel rectangle selection without modifying selection.
void DrawRectangleSelectionBox(gui::Canvas *canvas)
Draw the active rectangle selection box.
bool HasSelection() const
Check if any objects are selected.
Handles functional mutations and queries for tile objects.
zelda3::RoomObject preview_object_
void DrawGhostPreview() override
Draw ghost preview during placement.
void DrawSmartGuides(const std::vector< zelda3::RoomObject > &objects) const
bool ghost_preview_bitmap_ready_
void HandleMarqueeSelection(const ImVec2 &mouse_pos, bool mouse_left_down, bool mouse_left_released, bool shift_down, bool toggle_down, bool alt_down, bool draw_box=true)
void BeginPlacement() override
Begin placement mode.
bool HandleMouseWheel(float delta) override
std::unique_ptr< gfx::BackgroundBuffer > ghost_preview_buffer_
bool ResizeObjects(int room_id, const std::vector< size_t > &indices, int delta)
Resize objects by a delta.
void UpdateObjectsSize(int room_id, const std::vector< size_t > &indices, uint8_t new_size)
void RenderGhostPreviewBitmap()
ImVec2 ApplyDragModifiers(const ImVec2 &delta) const
void HandleRelease() override
Handle mouse release.
bool drag_has_duplicated_
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
bool PlaceObjectAt(int room_id, const zelda3::RoomObject &object, int x, int y)
Place a new object. Returns false if blocked by ROM limits.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
bool object_placement_mode_
void BeginMarqueeSelection(const ImVec2 &start_pos)
void MoveObjects(int room_id, const std::vector< size_t > &indices, int delta_x, int delta_y, bool notify_mutation=true)
Move a set of objects by a tile delta.
void CancelPlacement() override
Cancel current placement.
bool drag_mutation_started_
static GhostPreviewGeometry CalculateGhostPreviewGeometry(const zelda3::RoomObject &object)
Resolve the render anchor and visual extent used by placement previews.
void UpdateObjectsId(int room_id, const std::vector< size_t > &indices, int16_t new_id)
zelda3::Room * GetRoom(int room_id)
PlacementCapacityState GhostCapacityState
void NotifyChange(zelda3::Room *room)
GhostCapacityState GetPlacementGhostCapacityState() const
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void InitDrag(const ImVec2 &start_pos)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
void ProcessTextureQueue(IRenderer *renderer)
Represents a bitmap image optimized for SNES ROM hacking.
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
SDL_Surface * surface() const
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
DimensionResult GetDimensions(const RoomObject &obj) const
std::tuple< int, int, int, int > GetHitTestBounds(const RoomObject &obj) const
Draws dungeon objects to background buffers using game patterns.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr)
Draw a room object to background buffers.
std::pair< int, int > ResolveAnchor(int16_t object_id, uint8_t size_byte) const
Resolve the canvas anchor (x, y) for a given object's draw routine.
static ObjectGeometry & Get()
void MarkTileObjectCollectionDirty()
auto & object_bg2_buffer()
const std::vector< RoomObject > & GetTileObjects() const
auto & object_bg1_buffer()
#define LOG_WARN(category, format,...)
const AgentUITheme & GetTheme()
void DrawDashedLine(ImDrawList *draw_list, ImVec2 start, ImVec2 end, ImU32 color, float thickness)
constexpr int kGhostPreviewBufferSize
void RestoreObjectSelection(ObjectSelection *selection, const std::vector< size_t > &selected_indices_after_reorder)
constexpr size_t kMaxLayerBatchMutation
bool ApplyRoomPaletteToGhost(const zelda3::Room &room, gfx::Bitmap &ghost)
std::unordered_set< size_t > MakeValidIndexSet(const std::vector< size_t > &indices, size_t object_count)
GuideRect GetGuideRect(const zelda3::RoomObject &object)
int LayerBucketIndex(const zelda3::RoomObject &object)
void FlattenLayerBuckets(std::vector< zelda3::RoomObject > &objects, std::array< std::vector< LayerOrderEntry >, 3 > &buckets, ObjectSelection *selection)
bool AddUniqueGuide(std::vector< int > &guides, int value)
std::array< std::vector< LayerOrderEntry >, 3 > BuildLayerBuckets(const std::vector< zelda3::RoomObject > &objects, const std::unordered_set< size_t > &selected_indices)
constexpr int kRoomPixelHeight
constexpr int kRoomPixelWidth
ImVec2 SnapToTileGrid(const ImVec2 &point)
Snap a point to the 8px tile grid.
Editors are the view controllers for the application.
void DrawPlacementCapacityBadge(ImDrawList *draw_list, const ImVec2 &badge_min, const AgentUITheme &theme, PlacementCapacityState state, std::string_view primary_text)
ImVec4 GetPlacementAccentColor(const AgentUITheme &theme, PlacementCapacityState state, const ImVec4 &normal_color)
std::string_view GetPlacementCapacityTooltipSuffix(PlacementCapacityState state)
PlacementCapacityState GetPlacementCapacityState(size_t current_count, size_t max_count)
bool IsRoomObjectSizeEditable(int object_id)
constexpr size_t kMaxTileObjects
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
absl::StatusOr< ObjectStorageMutationResult > ReassignObjectStorage(std::vector< RoomObject > &objects, const std::vector< size_t > &indices, int target_value)
SDL2/SDL3 compatibility layer.
void NotifyInvalidateCache(MutationDomain domain=MutationDomain::kUnknown) const
Notify that cache invalidation is needed.
ObjectSelection * selection
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.
int render_anchor_x_tiles
zelda3::RoomObject object