yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tile_object_handler.cc
Go to the documentation of this file.
2#include <algorithm>
3#include <array>
4#include <cmath>
5#include <iterator>
6#include <unordered_set>
7#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
14#include "util/i18n/tr.h"
15#include "util/log.h"
23
25
26namespace yaze::editor {
27
28namespace {
29constexpr size_t kMaxLayerBatchMutation = 128;
30constexpr int kGhostPreviewBufferSize = 512;
31
33 const gfx::Bitmap* sources[] = {
34 &room.bg1_buffer().bitmap(), &room.object_bg1_buffer().bitmap(),
35 &room.bg2_buffer().bitmap(), &room.object_bg2_buffer().bitmap()};
36 for (const gfx::Bitmap* source : sources) {
37 SDL_Surface* surface = source->surface();
38 SDL_Palette* palette = platform::GetSurfacePalette(surface);
39 if (!palette || palette->ncolors <= 0) {
40 continue;
41 }
42
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};
47 ghost.SetPalette(colors);
48 if (ghost.surface()) {
49 SDL_SetColorKey(ghost.surface(), SDL_TRUE, 255);
50 SDL_SetSurfaceBlendMode(ghost.surface(), SDL_BLENDMODE_BLEND);
51 }
52 return true;
53 }
54 return false;
55}
56
59 bool selected = false;
60};
61
63 return std::clamp(static_cast<int>(object.GetLayerValue()), 0, 2);
64}
65
66std::unordered_set<size_t> MakeValidIndexSet(const std::vector<size_t>& indices,
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);
72 }
73 }
74 return index_set;
75}
76
77std::array<std::vector<LayerOrderEntry>, 3> BuildLayerBuckets(
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) {
82 buckets[LayerBucketIndex(objects[i])].push_back(
83 LayerOrderEntry{objects[i], selected_indices.count(i) > 0});
84 }
85 return buckets;
86}
87
89 ObjectSelection* selection,
90 const std::vector<size_t>& selected_indices_after_reorder) {
91 if (!selection) {
92 return;
93 }
94 selection->ClearSelection();
95 for (size_t index : selected_indices_after_reorder) {
97 }
98}
99
100void FlattenLayerBuckets(std::vector<zelda3::RoomObject>& objects,
101 std::array<std::vector<LayerOrderEntry>, 3>& buckets,
102 ObjectSelection* selection) {
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());
110 }
111 reordered.push_back(std::move(entry.object));
112 }
113 }
114 objects = std::move(reordered);
115 RestoreObjectSelection(selection, selected_indices_after_reorder);
116}
117
118struct GuideRect {
119 int left = 0;
120 int top = 0;
121 int right = 0;
122 int bottom = 0;
123 int center_x = 0;
124 int center_y = 0;
125};
126
128 auto [x, y, width, height] =
130 return GuideRect{x, y, x + width, y + height, x + width / 2, y + height / 2};
131}
132
133bool AddUniqueGuide(std::vector<int>& guides, int value) {
134 constexpr int kDuplicateTolerancePx = 2;
135 if (std::any_of(guides.begin(), guides.end(), [&](int guide) {
136 return std::abs(guide - value) <= kDuplicateTolerancePx;
137 })) {
138 return false;
139 }
140 guides.push_back(value);
141 return true;
142}
143
144void DrawDashedLine(ImDrawList* draw_list, ImVec2 start, ImVec2 end,
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;
149 const float total =
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);
153 if (vertical) {
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,
157 thickness);
158 } else {
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,
162 thickness);
163 }
164 }
165}
166
167} // namespace
168
171 if (!ctx_)
172 return GhostCapacityState::kNormal;
173 auto* room =
174 const_cast<TileObjectHandler*>(this)->GetRoom(ctx_->current_room_id);
175 const size_t current_obj_count = room ? room->GetTileObjects().size() : 0;
176 return GetPlacementCapacityState(current_obj_count, zelda3::kMaxTileObjects);
177}
178
180 if (!ctx_ || !ctx_->rooms)
181 return nullptr;
182 if (room_id < 0 || room_id >= static_cast<int>(ctx_->rooms->size())) {
183 return nullptr;
184 }
185 return &(*ctx_->rooms)[room_id];
186}
187
194
195// ========================================================================
196// BaseEntityHandler implementation
197// ========================================================================
198
203
207
208bool TileObjectHandler::HandleClick(int canvas_x, int canvas_y) {
209 if (!HasValidContext())
210 return false;
211
213 auto [room_x, room_y] = CanvasToRoom(canvas_x, canvas_y);
214 if (IsWithinBounds(canvas_x, canvas_y)) {
216 }
217 return true; // Placement click is handled even if outside bounds.
218 }
219
220 // Handle selection (click)
221 if (!ctx_ || !ctx_->selection)
222 return false;
223
224 auto hovered = GetEntityAtPosition(canvas_x, canvas_y);
225 if (!hovered.has_value())
226 return false;
227
228 const ImGuiIO& io = ImGui::GetIO();
229
230 // Alt-click is reserved for caller-specific behaviors (inspector/etc). Treat
231 // it as a handled click over an object without changing selection.
232 if (io.KeyAlt)
233 return true;
234
236 if (io.KeyShift) {
238 } else if (io.KeyCtrl || io.KeySuper) {
240 }
241
242 ctx_->selection->SelectObject(*hovered, mode);
243 return true;
244}
245
246void TileObjectHandler::InitDrag(const ImVec2& start_pos) {
247 is_dragging_ = true;
250 drag_last_dx_ = 0;
251 drag_last_dy_ = 0;
252 drag_has_duplicated_ = false;
254}
255
256void TileObjectHandler::BeginMarqueeSelection(const ImVec2& start_pos) {
257 if (!ctx_ || !ctx_->selection)
258 return;
259 ctx_->selection->BeginRectangleSelection(static_cast<int>(start_pos.x),
260 static_cast<int>(start_pos.y));
261}
262
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) {
266 (void)shift_down;
267 (void)toggle_down;
268 if (!ctx_ || !ctx_->selection)
269 return;
271 return;
272
273 // Update + draw while the drag is in progress.
274 if (mouse_left_down) {
275 ctx_->selection->UpdateRectangleSelection(static_cast<int>(mouse_pos.x),
276 static_cast<int>(mouse_pos.y));
277 if (draw_box) {
278 if (ctx_->canvas) {
280 }
281 }
282 }
283
284 // Finalize selection on release.
285 if (mouse_left_released) {
286 ctx_->selection->UpdateRectangleSelection(static_cast<int>(mouse_pos.x),
287 static_cast<int>(mouse_pos.y));
288
289 auto* room = GetRoom(ctx_->current_room_id);
290 if (!room) {
292 return;
293 }
294
295 constexpr int kMinRectPixels = 6;
296 if (alt_down || !ctx_->selection->IsRectangleLargeEnough(kMinRectPixels)) {
298 return;
299 }
300
302 room->GetTileObjects(), ObjectSelection::SelectionMode::Single,
303 [this](const zelda3::RoomObject& object) {
304 return ctx_->IsObjectVisibleForSelection(object);
305 });
306 }
307}
308
309void TileObjectHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
310 (void)delta;
311 if (!is_dragging_ || !ctx_ || !ctx_->selection)
312 return;
313
315
316 // Calculate total drag delta from start (snapped)
317 ImVec2 drag_delta =
319 drag_delta = ApplyDragModifiers(drag_delta);
320
321 const int tile_dx = static_cast<int>(drag_delta.x) / 8;
322 const int tile_dy = static_cast<int>(drag_delta.y) / 8;
323
324 // Calculate incremental move
325 const int inc_dx = tile_dx - drag_last_dx_;
326 const int inc_dy = tile_dy - drag_last_dy_;
327
328 if (inc_dx != 0 || inc_dy != 0) {
332 }
333
335 inc_dx, inc_dy,
336 /*notify_mutation=*/false);
337
338 drag_last_dx_ = tile_dx;
339 drag_last_dy_ = tile_dy;
340 }
341}
342
344 if (is_dragging_) {
345 const bool had_mutation = drag_mutation_started_;
346 is_dragging_ = false;
348 drag_has_duplicated_ = false;
349 // Drag operations mutate incrementally while the mouse is held down. The
350 // editor's undo capture wants to finalize after the drag ends (once the
351 // interaction mode has returned to Select), so emit one more invalidation
352 // on release if anything changed.
353 if (had_mutation && ctx_) {
355 }
356 }
357}
358
359ImVec2 TileObjectHandler::ApplyDragModifiers(const ImVec2& delta) const {
360 return delta;
361}
362
364 if (!HasValidContext() || !ctx_ || delta == 0.0f)
365 return false;
366
367 const int resize_delta = (delta > 0.0f) ? 1 : -1;
368 const bool horizontal = ImGui::GetCurrentContext() && ImGui::GetIO().KeyShift;
370 const uint8_t size = zelda3::ResizeRoomObjectByDelta(
371 preview_object_.id_, preview_object_.size_, resize_delta, horizontal);
372 if (size == preview_object_.size_) {
373 return false;
374 }
375 preview_object_.size_ = size;
378 return true;
379 }
380 if (!ctx_->selection)
381 return false;
382 auto indices = ctx_->selection->GetSelectedIndices();
383 if (indices.empty())
384 return false;
385
386 return ResizeObjects(ctx_->current_room_id, indices, resize_delta,
387 horizontal);
388}
389
392 return;
393
394 const auto pointer_screen_pos = GetPointerScreenPosition();
395 if (!pointer_screen_pos.has_value()) {
396 return;
397 }
398
399 const DungeonCanvasTransform transform = GetCanvasTransform();
400 const auto [canvas_x, canvas_y] =
401 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
402 auto [room_x, room_y] = CanvasToRoom(canvas_x, canvas_y);
403
404 if (!IsWithinBounds(canvas_x, canvas_y))
405 return;
406
411 }
412
413 auto [snap_canvas_x, snap_canvas_y] = RoomToCanvas(room_x, room_y);
414 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
415
416 ImDrawList* draw_list = ImGui::GetWindowDrawList();
417 const ImVec2 preview_start = transform.RoomPixelsToScreen(ImVec2(
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)));
420 const ImVec2 preview_size = transform.RoomSizeToScreen(
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);
425 const ImVec2 bitmap_start = transform.RoomPixelsToScreen(
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)));
430
431 const size_t current_obj_count = room ? room->GetTileObjects().size() : 0;
432 const auto capacity_state = GetPlacementGhostCapacityState();
433
434 const auto& theme = AgentUI::GetTheme();
435 const ImVec4 outline_color = GetPlacementAccentColor(
436 theme, capacity_state, theme.dungeon_selection_primary);
437 bool drew_bitmap = false;
438
440 auto& bitmap = ghost_preview_buffer_->bitmap();
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());
446 const ImVec2 bitmap_size = transform.RoomSizeToScreen(ImVec2(
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
453 ? theme.text_primary
454 : GetPlacementAccentColor(theme, capacity_state,
455 theme.text_primary);
456 tint.w = 0.70f;
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);
462 drew_bitmap = true;
463 }
464 }
465
466 if (!drew_bitmap) {
467 draw_list->AddRectFilled(
468 preview_start, preview_end,
469 ImGui::GetColorU32(
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);
473 }
474
475 // ID label
476 std::string id_text = absl::StrFormat("0x%02X", preview_object_.id_);
477 draw_list->AddText(ImVec2(preview_start.x + 2, preview_start.y + 1),
478 ImGui::GetColorU32(theme.text_primary), id_text.c_str());
479
480 // Capacity tooltip while hovering — proactive warning before user clicks.
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,
485 GetPlacementCapacityTooltipSuffix(capacity_state).data());
486 }
487
488 const std::string badge_text = absl::StrFormat(
489 "Objects %zu/%zu", current_obj_count, zelda3::kMaxTileObjects);
491 ImVec2(preview_start.x, preview_end.y + 6.0f),
492 theme, capacity_state, badge_text);
493}
494
496 if (!HasValidContext() || !ctx_->selection)
497 return;
498
499 auto* room = GetCurrentRoom();
500 if (!room)
501 return;
502
503 // Use ObjectSelection's rendering (handles pulsing border, corner handles)
505 ctx_->canvas, room->GetTileObjects(), [](const zelda3::RoomObject& obj) {
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());
510 });
511
512 if (is_dragging_) {
513 DrawSmartGuides(room->GetTileObjects());
514 }
515}
516
518 int canvas_x, int canvas_y) const {
519 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y)) {
520 return std::nullopt;
521 }
522 auto* room =
523 const_cast<TileObjectHandler*>(this)->GetRoom(ctx_->current_room_id);
524 if (!room)
525 return std::nullopt;
526
527 const auto& objects = room->GetTileObjects();
528 for (size_t i = objects.size(); i > 0; --i) {
529 size_t index = i - 1;
530 const auto& object = objects[index];
531
532 if (ctx_ && !ctx_->IsObjectVisibleForSelection(object)) {
533 continue;
534 }
535
536 // Respect layer filter if available in context
537 if (ctx_ && ctx_->selection &&
539 continue;
540 }
541
542 auto [obj_tile_x, obj_tile_y, width_tiles, height_tiles] =
544
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;
549
550 if (canvas_x >= obj_px && canvas_x < obj_px + w_px && canvas_y >= obj_py &&
551 canvas_y < obj_py + h_px) {
552 return index;
553 }
554 }
555 return std::nullopt;
556}
557
559 const std::vector<zelda3::RoomObject>& objects) const {
560 if (!ctx_ || !ctx_->canvas || !ctx_->selection ||
562 return;
563 }
564
565 const auto selected = ctx_->selection->GetSelectedIndices();
566 if (selected.empty()) {
567 return;
568 }
569
570 constexpr int kAlignmentTolerancePx = 2;
571 constexpr size_t kMaxGuidesPerAxis = 8;
572 std::vector<int> vertical_guides;
573 std::vector<int> horizontal_guides;
574
575 for (size_t selected_index : selected) {
576 if (selected_index >= objects.size()) {
577 continue;
578 }
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};
584
585 for (size_t other_index = 0; other_index < objects.size(); ++other_index) {
586 if (ctx_->selection->IsObjectSelected(other_index)) {
587 continue;
588 }
589 const GuideRect other_rect = GetGuideRect(objects[other_index]);
590 const std::array<int, 3> other_x = {other_rect.left, other_rect.center_x,
591 other_rect.right};
592 const std::array<int, 3> other_y = {other_rect.top, other_rect.center_y,
593 other_rect.bottom};
594
595 for (int sx : selected_x) {
596 for (int ox : other_x) {
597 if (std::abs(sx - ox) <= kAlignmentTolerancePx) {
598 AddUniqueGuide(vertical_guides, ox);
599 }
600 }
601 }
602 for (int sy : selected_y) {
603 for (int oy : other_y) {
604 if (std::abs(sy - oy) <= kAlignmentTolerancePx) {
605 AddUniqueGuide(horizontal_guides, oy);
606 }
607 }
608 }
609 if (vertical_guides.size() >= kMaxGuidesPerAxis &&
610 horizontal_guides.size() >= kMaxGuidesPerAxis) {
611 break;
612 }
613 }
614 }
615
616 if (vertical_guides.empty() && horizontal_guides.empty()) {
617 return;
618 }
619
620 const auto& theme = AgentUI::GetTheme();
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();
625 const DungeonCanvasTransform transform = GetCanvasTransform();
626 const ImVec2 canvas_pos = transform.room_origin_screen();
627 const ImVec2 room_size = transform.RoomSizeToScreen(ImVec2(
629
630 const size_t vertical_count =
631 std::min(vertical_guides.size(), kMaxGuidesPerAxis);
632 for (size_t i = 0; i < vertical_count; ++i) {
633 const float x =
634 transform.RoomPixelsToScreen(ImVec2(vertical_guides[i], 0.0f)).x;
635 DrawDashedLine(draw_list, ImVec2(x, canvas_pos.y),
636 ImVec2(x, canvas_pos.y + room_size.y), color, 1.2f);
637 }
638
639 const size_t horizontal_count =
640 std::min(horizontal_guides.size(), kMaxGuidesPerAxis);
641 for (size_t i = 0; i < horizontal_count; ++i) {
642 const float y =
643 transform.RoomPixelsToScreen(ImVec2(0.0f, horizontal_guides[i])).y;
644 DrawDashedLine(draw_list, ImVec2(canvas_pos.x, y),
645 ImVec2(canvas_pos.x + room_size.x, y), color, 1.2f);
646 }
647}
648
649// ========================================================================
650// Mutation Logic
651// ========================================================================
652
654 const std::vector<size_t>& indices,
655 int delta_x, int delta_y,
656 bool notify_mutation) {
657 auto* room = GetRoom(room_id);
658 if (!room || indices.empty())
659 return;
660 if (notify_mutation && ctx_)
662
663 auto& objects = room->GetTileObjects();
664 for (size_t index : indices) {
665 if (index < objects.size()) {
666 objects[index].x_ =
667 std::clamp(static_cast<int>(objects[index].x_ + delta_x), 0, 63);
668 objects[index].y_ =
669 std::clamp(static_cast<int>(objects[index].y_ + delta_y), 0, 63);
670 }
671 }
672
673 NotifyChange(room);
674}
675
677 const std::vector<size_t>& indices,
678 int16_t new_id) {
679 auto* room = GetRoom(room_id);
680 if (!room || indices.empty())
681 return;
682
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;
687 });
688 if (!has_change) {
689 return;
690 }
691 if (ctx_)
693
694 for (size_t index : indices) {
695 if (index < objects.size() && objects[index].id_ != new_id) {
696 const uint8_t canonical_size =
697 zelda3::CanonicalRoomObjectSize(new_id, objects[index].size_);
698 // Use the setter so derived flags + tile caches stay coherent.
699 objects[index].set_id(new_id);
700 objects[index].set_size(canonical_size);
701 }
702 }
703 NotifyChange(room);
704}
705
707 const std::vector<size_t>& indices,
708 uint8_t new_size) {
709 auto* room = GetRoom(room_id);
710 if (!room || indices.empty())
711 return;
712
713 auto& objects = room->GetTileObjects();
714 const auto can_update_size = [&](const zelda3::RoomObject& object) {
715 if (!zelda3::IsRoomObjectSizeEditable(object.id_)) {
716 return false;
717 }
718 if (!zelda3::IsRoomObjectResizable(object.id_)) {
719 const auto& manager = zelda3::CustomObjectManager::Get();
720 return new_size <= 0x0F &&
721 new_size < manager.GetSubtypeCount(object.id_) &&
722 !manager.ResolveFilename(object.id_, new_size).empty();
723 }
724 return true;
725 };
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])) {
729 return false;
730 }
731 return objects[index].size_ !=
732 zelda3::CanonicalRoomObjectSize(objects[index].id_, new_size);
733 });
734 if (!has_change) {
735 return;
736 }
737 if (ctx_)
738 ctx_->NotifyMutation(MutationDomain::kTileObjects);
739
740 for (size_t index : indices) {
741 if (index < objects.size() && can_update_size(objects[index])) {
742 const uint8_t canonical_size =
743 zelda3::CanonicalRoomObjectSize(objects[index].id_, new_size);
744 if (objects[index].size_ == canonical_size) {
745 continue;
746 }
747 objects[index].size_ = canonical_size;
748 objects[index].tiles_loaded_ = false;
749 }
750 }
751 NotifyChange(room);
752}
753
754bool TileObjectHandler::UpdateObjectsLayer(int room_id,
755 const std::vector<size_t>& indices,
756 int new_layer) {
757 auto* room = GetRoom(room_id);
758 if (!room || indices.empty())
759 return false;
760 if (new_layer < 0 || new_layer > 2) {
761 LOG_WARN("TileObjectHandler",
762 "Rejected layer update with invalid target layer: %d", new_layer);
763 return false;
764 }
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()) {
771 continue;
772 }
773 if (seen_indices.insert(index).second) {
774 deduped_indices.push_back(index);
775 }
776 }
777 if (deduped_indices.empty()) {
778 return false;
779 }
780
781 if (deduped_indices.size() > kMaxLayerBatchMutation) {
782 LOG_WARN("TileObjectHandler",
783 "Rejected layer batch mutation of %zu objects (max %zu)",
784 deduped_indices.size(), kMaxLayerBatchMutation);
785 return false;
786 }
787
788 auto candidate_objects = objects;
789 auto mutation = zelda3::ReassignObjectStorage(candidate_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());
794 return false;
795 }
796 if (!mutation->changed) {
797 return true;
798 }
799
800 if (ctx_)
801 ctx_->NotifyMutation(MutationDomain::kTileObjects);
802 objects = std::move(candidate_objects);
803 RestoreObjectSelection(ctx_ ? ctx_->selection : nullptr,
804 mutation->selected_indices);
805 NotifyChange(room);
806 return true;
807}
808
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())
814 return {};
815 if (notify_mutation && ctx_)
816 ctx_->NotifyMutation(MutationDomain::kTileObjects);
817
818 auto& objects = room->GetTileObjects();
819 std::vector<size_t> new_indices;
820
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()));
829 }
830 }
831
832 NotifyChange(room);
833 return new_indices;
834}
835
836void TileObjectHandler::DeleteObjects(int room_id,
837 std::vector<size_t> indices) {
838 auto* room = GetRoom(room_id);
839 if (!room || indices.empty())
840 return;
841 if (ctx_)
842 ctx_->NotifyMutation(MutationDomain::kTileObjects);
843
844 std::sort(indices.rbegin(), indices.rend());
845 for (size_t index : indices) {
846 room->RemoveTileObject(index);
847 }
848
849 NotifyChange(room);
850}
851
852void TileObjectHandler::DeleteAllObjects(int room_id) {
853 auto* room = GetRoom(room_id);
854 if (!room)
855 return;
856 if (ctx_)
857 ctx_->NotifyMutation(MutationDomain::kTileObjects);
858 room->ClearTileObjects();
859 NotifyChange(room);
860}
861
862void TileObjectHandler::SendToFront(int room_id,
863 const std::vector<size_t>& indices) {
864 auto* room = GetRoom(room_id);
865 if (!room || indices.empty())
866 return;
867 auto& objects = room->GetTileObjects();
868 auto selected_set = MakeValidIndexSet(indices, objects.size());
869 if (selected_set.empty()) {
870 return;
871 }
872 if (ctx_)
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));
883 } else {
884 other.push_back(std::move(entry));
885 }
886 }
887 bucket = std::move(other);
888 bucket.insert(bucket.end(), std::make_move_iterator(selected.begin()),
889 std::make_move_iterator(selected.end()));
890 }
891 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
892 NotifyChange(room);
893}
894
895void TileObjectHandler::SendToBack(int room_id,
896 const std::vector<size_t>& indices) {
897 auto* room = GetRoom(room_id);
898 if (!room || indices.empty())
899 return;
900 auto& objects = room->GetTileObjects();
901 auto selected_set = MakeValidIndexSet(indices, objects.size());
902 if (selected_set.empty()) {
903 return;
904 }
905 if (ctx_)
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));
916 } else {
917 other.push_back(std::move(entry));
918 }
919 }
920 bucket = std::move(selected);
921 bucket.insert(bucket.end(), std::make_move_iterator(other.begin()),
922 std::make_move_iterator(other.end()));
923 }
924 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
925 NotifyChange(room);
926}
927
928void TileObjectHandler::MoveForward(int room_id,
929 const std::vector<size_t>& indices) {
930 auto* room = GetRoom(room_id);
931 if (!room || indices.empty())
932 return;
933 auto& objects = room->GetTileObjects();
934 auto selected_set = MakeValidIndexSet(indices, objects.size());
935 if (selected_set.empty()) {
936 return;
937 }
938 if (ctx_)
939 ctx_->NotifyMutation(MutationDomain::kTileObjects);
940 auto buckets = BuildLayerBuckets(objects, selected_set);
941 for (auto& bucket : buckets) {
942 if (bucket.size() < 2) {
943 continue;
944 }
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]);
949 }
950 }
951 }
952 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
953 NotifyChange(room);
954}
955
956void TileObjectHandler::MoveBackward(int room_id,
957 const std::vector<size_t>& indices) {
958 auto* room = GetRoom(room_id);
959 if (!room || indices.empty())
960 return;
961 auto& objects = room->GetTileObjects();
962 auto selected_set = MakeValidIndexSet(indices, objects.size());
963 if (selected_set.empty()) {
964 return;
965 }
966 if (ctx_)
967 ctx_->NotifyMutation(MutationDomain::kTileObjects);
968 auto buckets = BuildLayerBuckets(objects, selected_set);
969 for (auto& bucket : buckets) {
970 if (bucket.size() < 2) {
971 continue;
972 }
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]);
977 }
978 }
979 }
980 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
981 NotifyChange(room);
982}
983
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())
989 return false;
990 auto& objects = room->GetTileObjects();
991 const auto resized_size = [&](const zelda3::RoomObject& object) {
992 return zelda3::ResizeRoomObjectByDelta(object.id_, object.size_, delta,
993 horizontal);
994 };
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]);
1000 });
1001 if (!has_change) {
1002 return false;
1003 }
1004 if (ctx_)
1005 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1006
1007 for (size_t index : indices) {
1008 if (index < objects.size() &&
1009 zelda3::IsRoomObjectResizable(objects[index].id_)) {
1010 const uint8_t new_size = resized_size(objects[index]);
1011 if (objects[index].size_ == new_size) {
1012 continue;
1013 }
1014 objects[index].size_ = new_size;
1015 objects[index].tiles_loaded_ = false;
1016 }
1017 }
1018 NotifyChange(room);
1019 return true;
1020}
1021
1022bool TileObjectHandler::PlaceObjectAt(int room_id,
1023 const zelda3::RoomObject& object, int x,
1024 int y) {
1025 auto* room = GetRoom(room_id);
1026 if (!room) {
1027 placement_block_reason_ = PlacementBlockReason::kInvalidRoom;
1028 return false;
1029 }
1030
1031 // Hard-stop: enforce ROM object limit before committing placement.
1032 if (room->GetTileObjects().size() >= zelda3::kMaxTileObjects) {
1033 placement_block_reason_ = PlacementBlockReason::kObjectLimit;
1034 return false;
1035 }
1036
1037 placement_block_reason_ = PlacementBlockReason::kNone;
1038 if (ctx_)
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);
1044 NotifyChange(room);
1045 TriggerSuccessToast();
1046 return true;
1047}
1048
1049void TileObjectHandler::SetPreviewObject(const zelda3::RoomObject& object) {
1050 preview_object_ = object;
1051 RefreshPreviewGraphics();
1052}
1053
1054void TileObjectHandler::RefreshPreviewGraphics() {
1055 if (object_placement_mode_) {
1056 RenderGhostPreviewBitmap();
1057 }
1058}
1059
1060void TileObjectHandler::RenderGhostPreviewBitmap() {
1061 ghost_preview_bitmap_ready_ = false;
1062 if (!ctx_ || !ctx_->rom || !ctx_->rom->is_loaded())
1063 return;
1064
1065 auto* room = GetRoom(ctx_->current_room_id);
1066 if (!room || !room->IsLoaded())
1067 return;
1068
1069 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
1070
1071 // Keep one room-sized scratch texture for the handler lifetime. Replacing a
1072 // textured bitmap during ImGui frame construction can invalidate draw-list
1073 // texture handles, and repeatedly allocating previews leaks those handles.
1074 if (!ghost_preview_buffer_) {
1075 ghost_preview_buffer_ = std::make_unique<gfx::BackgroundBuffer>(
1076 kGhostPreviewBufferSize, kGhostPreviewBufferSize);
1077 }
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();
1085
1086 zelda3::ObjectDrawer drawer(ctx_->rom, ctx_->current_room_id, gfx_data);
1087 drawer.InitializeDrawRoutines();
1088 drawer.SetRoomFloorGraphics(room->floor1(), room->floor2());
1089
1090 // Replay at the same safe anchor ObjectGeometry uses for measurement so
1091 // routines that draw upward or leftward do not clip against buffer origin.
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;
1095 // A placement preview is a flattened visual. Force the replay through BG1
1096 // so a sampled BG2-stream object cannot mask pixels in the same scratch
1097 // buffer that it just rendered.
1098 render_object.layer_ = zelda3::RoomObject::LayerType::BG1;
1099 auto status =
1100 drawer.DrawObject(render_object, *ghost_preview_buffer_,
1101 *ghost_preview_buffer_, ctx_->current_palette_group);
1102 if (!status.ok()) {
1103 return;
1104 }
1105
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;
1116 }
1118 if (bitmap.texture()) {
1119 ghost_preview_create_queued_ = false;
1120 }
1121 ghost_preview_bitmap_ready_ = true;
1122 ghost_preview_room_id_ = ctx_->current_room_id;
1123 ghost_preview_graphics_revision_ = room->graphics_revision();
1124 }
1125}
1126
1128TileObjectHandler::CalculateGhostPreviewGeometry(
1129 const zelda3::RoomObject& object) {
1130 const auto dimensions = zelda3::DimensionService::Get().GetDimensions(object);
1131 const auto [resolved_anchor_x, resolved_anchor_y] =
1132 zelda3::ObjectGeometry::Get().ResolveAnchor(object.id_, object.size_);
1133 // ResolveAnchor supplies routine-specific headroom. Dimension offsets cover
1134 // fallback/custom geometry too, so keep enough room for either source.
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;
1141 return GhostPreviewGeometry{
1142 .render_anchor_x_tiles = anchor_x,
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,
1150 };
1151}
1152
1153// ========================================================================
1154// Clipboard Operations
1155// ========================================================================
1156
1157void TileObjectHandler::CopyObjectsToClipboard(
1158 int room_id, const std::vector<size_t>& indices) {
1159 auto* room = GetRoom(room_id);
1160 if (!room || indices.empty())
1161 return;
1162
1163 clipboard_.clear();
1164 const auto& objects = room->GetTileObjects();
1165
1166 for (size_t idx : indices) {
1167 if (idx < objects.size()) {
1168 clipboard_.push_back(objects[idx]);
1169 }
1170 }
1171}
1172
1173std::vector<size_t> TileObjectHandler::PasteFromClipboard(int room_id,
1174 int offset_x,
1175 int offset_y) {
1176 auto* room = GetRoom(room_id);
1177 if (!room || clipboard_.empty())
1178 return {};
1179 if (ctx_)
1180 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1181
1182 std::vector<size_t> new_indices;
1183 size_t base_index = room->GetTileObjects().size();
1184
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++);
1192 }
1193
1194 NotifyChange(room);
1195 return new_indices;
1196}
1197
1198std::vector<size_t> TileObjectHandler::PasteFromClipboardAt(int room_id,
1199 int target_x,
1200 int target_y) {
1201 if (clipboard_.empty())
1202 return {};
1203
1204 int offset_x = target_x - clipboard_[0].x_;
1205 int offset_y = target_y - clipboard_[0].y_;
1206
1207 return PasteFromClipboard(room_id, offset_x, offset_y);
1208}
1209
1210} // namespace yaze::editor
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.
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.
std::pair< int, int > ScreenToRoomPixelCoordinates(ImVec2 screen) const
ImVec2 RoomSizeToScreen(ImVec2 room_size) const
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.
void DrawGhostPreview() override
Draw ghost preview during placement.
void DrawSmartGuides(const std::vector< zelda3::RoomObject > &objects) const
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)
ImVec2 ApplyDragModifiers(const ImVec2 &delta) const
void HandleRelease() override
Handle mouse release.
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.
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.
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)
Definition arena.cc:39
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:394
SDL_Surface * surface() const
Definition bitmap.h:402
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()
Definition room.h:461
auto & object_bg2_buffer()
Definition room.h:1045
uint64_t graphics_revision() const
Definition room.h:1024
auto & bg1_buffer()
Definition room.h:1039
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:405
auto & object_bg1_buffer()
Definition room.h:1043
auto & bg2_buffer()
Definition room.h:1040
#define LOG_WARN(category, format,...)
Definition log.h:108
const AgentUITheme & GetTheme()
void DrawDashedLine(ImDrawList *draw_list, ImVec2 start, ImVec2 end, ImU32 color, float thickness)
void RestoreObjectSelection(ObjectSelection *selection, const std::vector< size_t > &selected_indices_after_reorder)
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)
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)
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)
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:392
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.
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.