6#include <unordered_set>
7#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
37 SDL_Surface* surface = source->surface();
39 if (!palette || palette->ncolors <= 0) {
43 std::vector<SDL_Color> colors(256, {0, 0, 0, 0});
44 const int color_count = std::min(palette->ncolors, 256);
45 std::copy_n(palette->colors, color_count, colors.begin());
46 colors[255] = {0, 0, 0, 0};
49 SDL_SetColorKey(ghost.
surface(), SDL_TRUE, 255);
50 SDL_SetSurfaceBlendMode(ghost.
surface(), SDL_BLENDMODE_BLEND);
59 bool selected =
false;
63 return std::clamp(
static_cast<int>(
object.GetLayerValue()), 0, 2);
67 size_t object_count) {
68 std::unordered_set<size_t> index_set;
69 for (
size_t index : indices) {
70 if (index < object_count) {
71 index_set.insert(index);
78 const std::vector<zelda3::RoomObject>& objects,
79 const std::unordered_set<size_t>& selected_indices) {
80 std::array<std::vector<LayerOrderEntry>, 3> buckets;
81 for (
size_t i = 0; i < objects.size(); ++i) {
90 const std::vector<size_t>& selected_indices_after_reorder) {
95 for (
size_t index : selected_indices_after_reorder) {
101 std::array<std::vector<LayerOrderEntry>, 3>& buckets,
103 std::vector<zelda3::RoomObject> reordered;
104 std::vector<size_t> selected_indices_after_reorder;
105 reordered.reserve(objects.size());
106 for (
auto& bucket : buckets) {
107 for (
auto& entry : bucket) {
108 if (entry.selected) {
109 selected_indices_after_reorder.push_back(reordered.size());
111 reordered.push_back(std::move(entry.object));
114 objects = std::move(reordered);
128 auto [x, y, width, height] =
130 return GuideRect{x, y, x + width, y + height, x + width / 2, y + height / 2};
134 constexpr int kDuplicateTolerancePx = 2;
135 if (std::any_of(guides.begin(), guides.end(), [&](
int guide) {
136 return std::abs(guide - value) <= kDuplicateTolerancePx;
140 guides.push_back(value);
145 ImU32 color,
float thickness) {
146 constexpr float kDash = 6.0f;
147 constexpr float kGap = 4.0f;
148 const bool vertical = std::abs(start.x - end.x) < 0.5f;
150 vertical ? std::abs(end.y - start.y) : std::abs(end.x - start.x);
151 for (
float offset = 0.0f; offset < total; offset += kDash + kGap) {
152 const float segment_end = std::min(offset + kDash, total);
154 const float y0 = start.y + offset;
155 const float y1 = start.y + segment_end;
156 draw_list->AddLine(ImVec2(start.x, y0), ImVec2(start.x, y1), color,
159 const float x0 = start.x + offset;
160 const float x1 = start.x + segment_end;
161 draw_list->AddLine(ImVec2(x0, start.y), ImVec2(x1, start.y), color,
172 return GhostCapacityState::kNormal;
175 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
182 if (room_id < 0 || room_id >=
static_cast<int>(
ctx_->
rooms->
size())) {
213 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
225 if (!hovered.has_value())
228 const ImGuiIO& io = ImGui::GetIO();
238 }
else if (io.KeyCtrl || io.KeySuper) {
260 static_cast<int>(start_pos.y));
264 const ImVec2& mouse_pos,
bool mouse_left_down,
bool mouse_left_released,
265 bool shift_down,
bool toggle_down,
bool alt_down,
bool draw_box) {
274 if (mouse_left_down) {
276 static_cast<int>(mouse_pos.y));
285 if (mouse_left_released) {
287 static_cast<int>(mouse_pos.y));
295 constexpr int kMinRectPixels = 6;
304 return ctx_->IsObjectVisibleForSelection(object);
321 const int tile_dx =
static_cast<int>(drag_delta.x) / 8;
322 const int tile_dy =
static_cast<int>(drag_delta.y) / 8;
328 if (inc_dx != 0 || inc_dy != 0) {
353 if (had_mutation &&
ctx_) {
367 const int resize_delta = (delta > 0.0f) ? 1 : -1;
368 const bool horizontal = ImGui::GetCurrentContext() && ImGui::GetIO().KeyShift;
395 if (!pointer_screen_pos.has_value()) {
400 const auto [canvas_x, canvas_y] =
402 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
413 auto [snap_canvas_x, snap_canvas_y] =
RoomToCanvas(room_x, room_y);
416 ImDrawList* draw_list = ImGui::GetWindowDrawList();
418 static_cast<float>(snap_canvas_x + preview_geometry.offset_x_tiles * 8),
419 static_cast<float>(snap_canvas_y + preview_geometry.offset_y_tiles * 8)));
421 ImVec2(
static_cast<float>(preview_geometry.width_pixels),
422 static_cast<float>(preview_geometry.height_pixels)));
423 const ImVec2 preview_end(preview_start.x + preview_size.x,
424 preview_start.y + preview_size.y);
426 ImVec2(
static_cast<float>(snap_canvas_x -
427 preview_geometry.render_anchor_x_tiles * 8),
428 static_cast<float>(snap_canvas_y -
429 preview_geometry.render_anchor_y_tiles * 8)));
431 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
436 theme, capacity_state, theme.dungeon_selection_primary);
437 bool drew_bitmap =
false;
441 if (bitmap.texture()) {
442 const int crop_width =
443 std::min(preview_geometry.buffer_width_pixels, bitmap.width());
444 const int crop_height =
445 std::min(preview_geometry.buffer_height_pixels, bitmap.height());
447 static_cast<float>(crop_width),
static_cast<float>(crop_height)));
448 const ImVec2 bitmap_end(bitmap_start.x + bitmap_size.x,
449 bitmap_start.y + bitmap_size.y);
450 const ImVec2 uv_end(
static_cast<float>(crop_width) / bitmap.width(),
451 static_cast<float>(crop_height) / bitmap.height());
452 ImVec4 tint = capacity_state == GhostCapacityState::kNormal
457 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), bitmap_start,
458 bitmap_end, ImVec2(0, 0), uv_end,
459 ImGui::GetColorU32(tint));
460 draw_list->AddRect(preview_start, preview_end,
461 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
467 draw_list->AddRectFilled(
468 preview_start, preview_end,
470 ImVec4(outline_color.x, outline_color.y, outline_color.z, 0.25f)));
471 draw_list->AddRect(preview_start, preview_end,
472 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
477 draw_list->AddText(ImVec2(preview_start.x + 2, preview_start.y + 1),
478 ImGui::GetColorU32(theme.text_primary), id_text.c_str());
481 if (capacity_state != GhostCapacityState::kNormal &&
482 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
483 ImGui::SetTooltip(tr(
"Objects: %zu/%zu\n%s"), current_obj_count,
488 const std::string badge_text = absl::StrFormat(
491 ImVec2(preview_start.x, preview_end.y + 6.0f),
492 theme, capacity_state, badge_text);
506 auto result = zelda3::DimensionService::Get().GetDimensions(obj);
507 return std::make_tuple(result.offset_x_tiles * 8,
508 result.offset_y_tiles * 8, result.width_pixels(),
509 result.height_pixels());
518 int canvas_x,
int canvas_y)
const {
528 for (
size_t i = objects.size(); i > 0; --i) {
529 size_t index = i - 1;
530 const auto&
object = objects[index];
542 auto [obj_tile_x, obj_tile_y, width_tiles, height_tiles] =
545 int obj_px = obj_tile_x * 8;
546 int obj_py = obj_tile_y * 8;
547 int w_px = width_tiles * 8;
548 int h_px = height_tiles * 8;
550 if (canvas_x >= obj_px && canvas_x < obj_px + w_px && canvas_y >= obj_py &&
551 canvas_y < obj_py + h_px) {
559 const std::vector<zelda3::RoomObject>& objects)
const {
566 if (selected.empty()) {
570 constexpr int kAlignmentTolerancePx = 2;
571 constexpr size_t kMaxGuidesPerAxis = 8;
572 std::vector<int> vertical_guides;
573 std::vector<int> horizontal_guides;
575 for (
size_t selected_index : selected) {
576 if (selected_index >= objects.size()) {
579 const GuideRect selected_rect = GetGuideRect(objects[selected_index]);
580 const std::array<int, 3> selected_x = {
581 selected_rect.left, selected_rect.center_x, selected_rect.right};
582 const std::array<int, 3> selected_y = {
583 selected_rect.top, selected_rect.center_y, selected_rect.bottom};
585 for (
size_t other_index = 0; other_index < objects.size(); ++other_index) {
589 const GuideRect other_rect = GetGuideRect(objects[other_index]);
590 const std::array<int, 3> other_x = {other_rect.left, other_rect.center_x,
592 const std::array<int, 3> other_y = {other_rect.top, other_rect.center_y,
595 for (
int sx : selected_x) {
596 for (
int ox : other_x) {
597 if (std::abs(sx - ox) <= kAlignmentTolerancePx) {
598 AddUniqueGuide(vertical_guides, ox);
602 for (
int sy : selected_y) {
603 for (
int oy : other_y) {
604 if (std::abs(sy - oy) <= kAlignmentTolerancePx) {
605 AddUniqueGuide(horizontal_guides, oy);
609 if (vertical_guides.size() >= kMaxGuidesPerAxis &&
610 horizontal_guides.size() >= kMaxGuidesPerAxis) {
616 if (vertical_guides.empty() && horizontal_guides.empty()) {
621 ImVec4 guide_color = theme.accent_color;
622 guide_color.w = 0.78f;
623 const ImU32 color = ImGui::GetColorU32(guide_color);
624 ImDrawList* draw_list = ImGui::GetWindowDrawList();
630 const size_t vertical_count =
631 std::min(vertical_guides.size(), kMaxGuidesPerAxis);
632 for (
size_t i = 0; i < vertical_count; ++i) {
635 DrawDashedLine(draw_list, ImVec2(x, canvas_pos.y),
636 ImVec2(x, canvas_pos.y + room_size.y), color, 1.2f);
639 const size_t horizontal_count =
640 std::min(horizontal_guides.size(), kMaxGuidesPerAxis);
641 for (
size_t i = 0; i < horizontal_count; ++i) {
644 DrawDashedLine(draw_list, ImVec2(canvas_pos.x, y),
645 ImVec2(canvas_pos.x + room_size.x, y), color, 1.2f);
654 const std::vector<size_t>& indices,
655 int delta_x,
int delta_y,
656 bool notify_mutation) {
658 if (!room || indices.empty())
660 if (notify_mutation &&
ctx_)
663 auto& objects = room->GetTileObjects();
664 for (
size_t index : indices) {
665 if (index < objects.size()) {
667 std::clamp(
static_cast<int>(objects[index].x_ + delta_x), 0, 63);
669 std::clamp(
static_cast<int>(objects[index].y_ + delta_y), 0, 63);
677 const std::vector<size_t>& indices,
680 if (!room || indices.empty())
683 auto& objects = room->GetTileObjects();
684 const bool has_change =
685 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
686 return index < objects.size() && objects[index].id_ != new_id;
694 for (
size_t index : indices) {
695 if (index < objects.size() && objects[index].id_ != new_id) {
696 const uint8_t canonical_size =
699 objects[index].set_id(new_id);
700 objects[index].set_size(canonical_size);
707 const std::vector<size_t>& indices,
710 if (!room || indices.empty())
713 auto& objects = room->GetTileObjects();
720 return new_size <= 0x0F &&
721 new_size < manager.GetSubtypeCount(
object.id_) &&
722 !manager.ResolveFilename(
object.id_, new_size).empty();
726 const bool has_change =
727 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
728 if (index >= objects.size() || !can_update_size(objects[index])) {
731 return objects[index].size_ !=
740 for (
size_t index : indices) {
741 if (index < objects.size() && can_update_size(objects[index])) {
742 const uint8_t canonical_size =
744 if (objects[index].size_ == canonical_size) {
747 objects[index].size_ = canonical_size;
748 objects[index].tiles_loaded_ =
false;
754bool TileObjectHandler::UpdateObjectsLayer(
int room_id,
755 const std::vector<size_t>& indices,
757 auto* room = GetRoom(room_id);
758 if (!room || indices.empty())
760 if (new_layer < 0 || new_layer > 2) {
762 "Rejected layer update with invalid target layer: %d", new_layer);
765 auto& objects = room->GetTileObjects();
766 std::vector<size_t> deduped_indices;
767 deduped_indices.reserve(indices.size());
768 std::unordered_set<size_t> seen_indices;
769 for (
size_t index : indices) {
770 if (index >= objects.size()) {
773 if (seen_indices.insert(index).second) {
774 deduped_indices.push_back(index);
777 if (deduped_indices.empty()) {
781 if (deduped_indices.size() > kMaxLayerBatchMutation) {
783 "Rejected layer batch mutation of %zu objects (max %zu)",
784 deduped_indices.size(), kMaxLayerBatchMutation);
788 auto candidate_objects = objects;
790 deduped_indices, new_layer);
791 if (!mutation.ok()) {
792 LOG_WARN(
"TileObjectHandler",
"Rejected object stream mutation: %s",
793 std::string(mutation.status().message()).c_str());
796 if (!mutation->changed) {
801 ctx_->NotifyMutation(MutationDomain::kTileObjects);
802 objects = std::move(candidate_objects);
803 RestoreObjectSelection(ctx_ ? ctx_->selection :
nullptr,
804 mutation->selected_indices);
809std::vector<size_t> TileObjectHandler::DuplicateObjects(
810 int room_id,
const std::vector<size_t>& indices,
int delta_x,
int delta_y,
811 bool notify_mutation) {
812 auto* room = GetRoom(room_id);
813 if (!room || indices.empty())
815 if (notify_mutation && ctx_)
816 ctx_->NotifyMutation(MutationDomain::kTileObjects);
818 auto& objects = room->GetTileObjects();
819 std::vector<size_t> new_indices;
821 const size_t base_index = objects.size();
822 for (
size_t index : indices) {
823 if (index < objects.size()) {
824 auto clone = objects[index].CopyForNewPlacement();
825 clone.x_ = std::clamp(
static_cast<int>(clone.x_ + delta_x), 0, 63);
826 clone.y_ = std::clamp(
static_cast<int>(clone.y_ + delta_y), 0, 63);
827 objects.push_back(clone);
828 new_indices.push_back(base_index + (new_indices.size()));
836void TileObjectHandler::DeleteObjects(
int room_id,
837 std::vector<size_t> indices) {
838 auto* room = GetRoom(room_id);
839 if (!room || indices.empty())
842 ctx_->NotifyMutation(MutationDomain::kTileObjects);
844 std::sort(indices.rbegin(), indices.rend());
845 for (
size_t index : indices) {
846 room->RemoveTileObject(index);
852void TileObjectHandler::DeleteAllObjects(
int room_id) {
853 auto* room = GetRoom(room_id);
857 ctx_->NotifyMutation(MutationDomain::kTileObjects);
858 room->ClearTileObjects();
862void TileObjectHandler::SendToFront(
int room_id,
863 const std::vector<size_t>& indices) {
864 auto* room = GetRoom(room_id);
865 if (!room || indices.empty())
867 auto& objects = room->GetTileObjects();
868 auto selected_set = MakeValidIndexSet(indices, objects.size());
869 if (selected_set.empty()) {
873 ctx_->NotifyMutation(MutationDomain::kTileObjects);
874 auto buckets = BuildLayerBuckets(objects, selected_set);
875 for (
auto& bucket : buckets) {
876 std::vector<LayerOrderEntry> other;
877 std::vector<LayerOrderEntry> selected;
878 other.reserve(bucket.size());
879 selected.reserve(bucket.size());
880 for (
auto& entry : bucket) {
881 if (entry.selected) {
882 selected.push_back(std::move(entry));
884 other.push_back(std::move(entry));
887 bucket = std::move(other);
888 bucket.insert(bucket.end(), std::make_move_iterator(selected.begin()),
889 std::make_move_iterator(selected.end()));
891 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
895void TileObjectHandler::SendToBack(
int room_id,
896 const std::vector<size_t>& indices) {
897 auto* room = GetRoom(room_id);
898 if (!room || indices.empty())
900 auto& objects = room->GetTileObjects();
901 auto selected_set = MakeValidIndexSet(indices, objects.size());
902 if (selected_set.empty()) {
906 ctx_->NotifyMutation(MutationDomain::kTileObjects);
907 auto buckets = BuildLayerBuckets(objects, selected_set);
908 for (
auto& bucket : buckets) {
909 std::vector<LayerOrderEntry> selected;
910 std::vector<LayerOrderEntry> other;
911 selected.reserve(bucket.size());
912 other.reserve(bucket.size());
913 for (
auto& entry : bucket) {
914 if (entry.selected) {
915 selected.push_back(std::move(entry));
917 other.push_back(std::move(entry));
920 bucket = std::move(selected);
921 bucket.insert(bucket.end(), std::make_move_iterator(other.begin()),
922 std::make_move_iterator(other.end()));
924 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
928void TileObjectHandler::MoveForward(
int room_id,
929 const std::vector<size_t>& indices) {
930 auto* room = GetRoom(room_id);
931 if (!room || indices.empty())
933 auto& objects = room->GetTileObjects();
934 auto selected_set = MakeValidIndexSet(indices, objects.size());
935 if (selected_set.empty()) {
939 ctx_->NotifyMutation(MutationDomain::kTileObjects);
940 auto buckets = BuildLayerBuckets(objects, selected_set);
941 for (
auto& bucket : buckets) {
942 if (bucket.size() < 2) {
945 for (
size_t i = bucket.size() - 1; i > 0; --i) {
946 const size_t previous = i - 1;
947 if (bucket[previous].selected && !bucket[i].selected) {
948 std::swap(bucket[previous], bucket[i]);
952 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
956void TileObjectHandler::MoveBackward(
int room_id,
957 const std::vector<size_t>& indices) {
958 auto* room = GetRoom(room_id);
959 if (!room || indices.empty())
961 auto& objects = room->GetTileObjects();
962 auto selected_set = MakeValidIndexSet(indices, objects.size());
963 if (selected_set.empty()) {
967 ctx_->NotifyMutation(MutationDomain::kTileObjects);
968 auto buckets = BuildLayerBuckets(objects, selected_set);
969 for (
auto& bucket : buckets) {
970 if (bucket.size() < 2) {
973 for (
size_t i = 1; i < bucket.size(); ++i) {
974 const size_t previous = i - 1;
975 if (bucket[i].selected && !bucket[previous].selected) {
976 std::swap(bucket[i], bucket[previous]);
980 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection :
nullptr);
984bool TileObjectHandler::ResizeObjects(
int room_id,
985 const std::vector<size_t>& indices,
986 int delta,
bool horizontal) {
987 auto* room = GetRoom(room_id);
988 if (!room || indices.empty())
990 auto& objects = room->GetTileObjects();
995 const bool has_change =
996 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
997 return index < objects.size() &&
998 zelda3::IsRoomObjectResizable(objects[index].id_) &&
999 objects[index].size_ != resized_size(objects[index]);
1005 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1007 for (
size_t index : indices) {
1008 if (index < objects.size() &&
1010 const uint8_t new_size = resized_size(objects[index]);
1011 if (objects[index].size_ == new_size) {
1014 objects[index].size_ = new_size;
1015 objects[index].tiles_loaded_ =
false;
1022bool TileObjectHandler::PlaceObjectAt(
int room_id,
1025 auto* room = GetRoom(room_id);
1027 placement_block_reason_ = PlacementBlockReason::kInvalidRoom;
1033 placement_block_reason_ = PlacementBlockReason::kObjectLimit;
1037 placement_block_reason_ = PlacementBlockReason::kNone;
1039 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1040 auto new_obj =
object.CopyForNewPlacement();
1041 new_obj.x_ = std::clamp(x, 0, 63);
1042 new_obj.y_ = std::clamp(y, 0, 63);
1043 room->AddTileObject(new_obj);
1045 TriggerSuccessToast();
1050 preview_object_ = object;
1051 RefreshPreviewGraphics();
1054void TileObjectHandler::RefreshPreviewGraphics() {
1055 if (object_placement_mode_) {
1056 RenderGhostPreviewBitmap();
1060void TileObjectHandler::RenderGhostPreviewBitmap() {
1061 ghost_preview_bitmap_ready_ =
false;
1062 if (!ctx_ || !ctx_->rom || !ctx_->rom->is_loaded())
1065 auto* room = GetRoom(ctx_->current_room_id);
1066 if (!room || !room->IsLoaded())
1069 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
1074 if (!ghost_preview_buffer_) {
1075 ghost_preview_buffer_ = std::make_unique<gfx::BackgroundBuffer>(
1076 kGhostPreviewBufferSize, kGhostPreviewBufferSize);
1078 ghost_preview_buffer_->EnsureBitmapInitialized();
1079 ghost_preview_buffer_->ClearBuffer();
1080 auto& bitmap = ghost_preview_buffer_->bitmap();
1081 ApplyRoomPaletteToGhost(*room, bitmap);
1082 std::fill(bitmap.mutable_data().begin(), bitmap.mutable_data().end(), 255);
1083 bitmap.set_modified(
true);
1084 const uint8_t* gfx_data = room->get_gfx_buffer().data();
1092 auto render_object = preview_object_;
1093 render_object.x_ = preview_geometry.render_anchor_x_tiles;
1094 render_object.y_ = preview_geometry.render_anchor_y_tiles;
1100 drawer.
DrawObject(render_object, *ghost_preview_buffer_,
1101 *ghost_preview_buffer_, ctx_->current_palette_group);
1106 if (bitmap.size() > 0) {
1107 bitmap.UpdateSurfacePixels();
1108 if (bitmap.texture()) {
1109 ghost_preview_create_queued_ =
false;
1112 }
else if (!ghost_preview_create_queued_) {
1115 ghost_preview_create_queued_ =
true;
1118 if (bitmap.texture()) {
1119 ghost_preview_create_queued_ =
false;
1121 ghost_preview_bitmap_ready_ =
true;
1122 ghost_preview_room_id_ = ctx_->current_room_id;
1123 ghost_preview_graphics_revision_ = room->graphics_revision();
1128TileObjectHandler::CalculateGhostPreviewGeometry(
1131 const auto [resolved_anchor_x, resolved_anchor_y] =
1135 const int anchor_x =
1136 std::max({0, resolved_anchor_x, -dimensions.offset_x_tiles});
1137 const int anchor_y =
1138 std::max({0, resolved_anchor_y, -dimensions.offset_y_tiles});
1139 const int leading_x_tiles = anchor_x + dimensions.offset_x_tiles;
1140 const int leading_y_tiles = anchor_y + dimensions.offset_y_tiles;
1143 .render_anchor_y_tiles = anchor_y,
1144 .offset_x_tiles = dimensions.offset_x_tiles,
1145 .offset_y_tiles = dimensions.offset_y_tiles,
1146 .width_pixels = dimensions.width_pixels(),
1147 .height_pixels = dimensions.height_pixels(),
1148 .buffer_width_pixels = (leading_x_tiles + dimensions.width_tiles) * 8,
1149 .buffer_height_pixels = (leading_y_tiles + dimensions.height_tiles) * 8,
1157void TileObjectHandler::CopyObjectsToClipboard(
1158 int room_id,
const std::vector<size_t>& indices) {
1159 auto* room = GetRoom(room_id);
1160 if (!room || indices.empty())
1164 const auto& objects = room->GetTileObjects();
1166 for (
size_t idx : indices) {
1167 if (idx < objects.size()) {
1168 clipboard_.push_back(objects[idx]);
1173std::vector<size_t> TileObjectHandler::PasteFromClipboard(
int room_id,
1176 auto* room = GetRoom(room_id);
1177 if (!room || clipboard_.empty())
1180 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1182 std::vector<size_t> new_indices;
1183 size_t base_index = room->GetTileObjects().size();
1185 for (
auto obj : clipboard_) {
1186 obj = obj.CopyForNewPlacement();
1187 obj.x_ = std::clamp(obj.x_ + offset_x, 0, 63);
1188 obj.y_ = std::clamp(obj.y_ + offset_y, 0, 63);
1189 obj.tiles_loaded_ =
false;
1190 room->AddTileObject(obj);
1191 new_indices.push_back(base_index++);
1198std::vector<size_t> TileObjectHandler::PasteFromClipboardAt(
int room_id,
1201 if (clipboard_.empty())
1204 int offset_x = target_x - clipboard_[0].x_;
1205 int offset_y = target_y - clipboard_[0].y_;
1207 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.
void EndRectangleSelection(const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single, std::function< bool(const zelda3::RoomObject &)> is_object_visible={})
Complete rectangle selection operation.
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 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_
void UpdateObjectsSize(int room_id, const std::vector< size_t > &indices, uint8_t new_size)
int ghost_preview_room_id_
void RenderGhostPreviewBitmap()
ImVec2 ApplyDragModifiers(const ImVec2 &delta) const
void HandleRelease() override
Handle mouse release.
bool drag_has_duplicated_
uint64_t ghost_preview_graphics_revision_
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.
bool ResizeObjects(int room_id, const std::vector< size_t > &indices, int delta, bool horizontal=false)
Resize objects by a delta; horizontal selects packed-floor width.
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 CustomObjectManager & Get()
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.
void SetRoomFloorGraphics(uint8_t floor1, uint8_t floor2)
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, gfx::BackgroundBuffer *layout_bg2=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()
uint64_t graphics_revision() const
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)
uint8_t ResizeRoomObjectByDelta(int object_id, uint8_t size, int delta, bool horizontal)
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)
bool IsRoomObjectResizable(int object_id)
SDL2/SDL3 compatibility layer.
bool IsObjectVisibleForSelection(const zelda3::RoomObject &object) const
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