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"
21
23
24namespace yaze::editor {
25
26namespace {
27constexpr size_t kMaxLayerBatchMutation = 128;
28constexpr int kGhostPreviewBufferSize = 512;
29
31 const gfx::Bitmap* sources[] = {
32 &room.bg1_buffer().bitmap(), &room.object_bg1_buffer().bitmap(),
33 &room.bg2_buffer().bitmap(), &room.object_bg2_buffer().bitmap()};
34 for (const gfx::Bitmap* source : sources) {
35 SDL_Surface* surface = source->surface();
36 SDL_Palette* palette = platform::GetSurfacePalette(surface);
37 if (!palette || palette->ncolors <= 0) {
38 continue;
39 }
40
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};
45 ghost.SetPalette(colors);
46 if (ghost.surface()) {
47 SDL_SetColorKey(ghost.surface(), SDL_TRUE, 255);
48 SDL_SetSurfaceBlendMode(ghost.surface(), SDL_BLENDMODE_BLEND);
49 }
50 return true;
51 }
52 return false;
53}
54
57 bool selected = false;
58};
59
61 return std::clamp(static_cast<int>(object.GetLayerValue()), 0, 2);
62}
63
64std::unordered_set<size_t> MakeValidIndexSet(const std::vector<size_t>& indices,
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);
70 }
71 }
72 return index_set;
73}
74
75std::array<std::vector<LayerOrderEntry>, 3> BuildLayerBuckets(
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) {
80 buckets[LayerBucketIndex(objects[i])].push_back(
81 LayerOrderEntry{objects[i], selected_indices.count(i) > 0});
82 }
83 return buckets;
84}
85
87 ObjectSelection* selection,
88 const std::vector<size_t>& selected_indices_after_reorder) {
89 if (!selection) {
90 return;
91 }
92 selection->ClearSelection();
93 for (size_t index : selected_indices_after_reorder) {
95 }
96}
97
98void FlattenLayerBuckets(std::vector<zelda3::RoomObject>& objects,
99 std::array<std::vector<LayerOrderEntry>, 3>& buckets,
100 ObjectSelection* selection) {
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());
108 }
109 reordered.push_back(std::move(entry.object));
110 }
111 }
112 objects = std::move(reordered);
113 RestoreObjectSelection(selection, selected_indices_after_reorder);
114}
115
116struct GuideRect {
117 int left = 0;
118 int top = 0;
119 int right = 0;
120 int bottom = 0;
121 int center_x = 0;
122 int center_y = 0;
123};
124
126 auto [x, y, width, height] =
128 return GuideRect{x, y, x + width, y + height, x + width / 2, y + height / 2};
129}
130
131bool AddUniqueGuide(std::vector<int>& guides, int value) {
132 constexpr int kDuplicateTolerancePx = 2;
133 if (std::any_of(guides.begin(), guides.end(), [&](int guide) {
134 return std::abs(guide - value) <= kDuplicateTolerancePx;
135 })) {
136 return false;
137 }
138 guides.push_back(value);
139 return true;
140}
141
142void DrawDashedLine(ImDrawList* draw_list, ImVec2 start, ImVec2 end,
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;
147 const float total =
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);
151 if (vertical) {
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,
155 thickness);
156 } else {
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,
160 thickness);
161 }
162 }
163}
164
165} // namespace
166
169 if (!ctx_)
170 return GhostCapacityState::kNormal;
171 auto* room =
172 const_cast<TileObjectHandler*>(this)->GetRoom(ctx_->current_room_id);
173 const size_t current_obj_count = room ? room->GetTileObjects().size() : 0;
174 return GetPlacementCapacityState(current_obj_count, zelda3::kMaxTileObjects);
175}
176
178 if (!ctx_ || !ctx_->rooms)
179 return nullptr;
180 if (room_id < 0 || room_id >= static_cast<int>(ctx_->rooms->size())) {
181 return nullptr;
182 }
183 return &(*ctx_->rooms)[room_id];
184}
185
192
193// ========================================================================
194// BaseEntityHandler implementation
195// ========================================================================
196
201
205
206bool TileObjectHandler::HandleClick(int canvas_x, int canvas_y) {
207 if (!HasValidContext())
208 return false;
209
211 auto [room_x, room_y] = CanvasToRoom(canvas_x, canvas_y);
212 if (IsWithinBounds(canvas_x, canvas_y)) {
214 }
215 return true; // Placement click is handled even if outside bounds.
216 }
217
218 // Handle selection (click)
219 if (!ctx_ || !ctx_->selection)
220 return false;
221
222 auto hovered = GetEntityAtPosition(canvas_x, canvas_y);
223 if (!hovered.has_value())
224 return false;
225
226 const ImGuiIO& io = ImGui::GetIO();
227
228 // Alt-click is reserved for caller-specific behaviors (inspector/etc). Treat
229 // it as a handled click over an object without changing selection.
230 if (io.KeyAlt)
231 return true;
232
234 if (io.KeyShift) {
236 } else if (io.KeyCtrl || io.KeySuper) {
238 }
239
240 ctx_->selection->SelectObject(*hovered, mode);
241 return true;
242}
243
244void TileObjectHandler::InitDrag(const ImVec2& start_pos) {
245 is_dragging_ = true;
248 drag_last_dx_ = 0;
249 drag_last_dy_ = 0;
250 drag_has_duplicated_ = false;
252}
253
254void TileObjectHandler::BeginMarqueeSelection(const ImVec2& start_pos) {
255 if (!ctx_ || !ctx_->selection)
256 return;
257 ctx_->selection->BeginRectangleSelection(static_cast<int>(start_pos.x),
258 static_cast<int>(start_pos.y));
259}
260
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) {
264 (void)shift_down;
265 (void)toggle_down;
266 if (!ctx_ || !ctx_->selection)
267 return;
269 return;
270
271 // Update + draw while the drag is in progress.
272 if (mouse_left_down) {
273 ctx_->selection->UpdateRectangleSelection(static_cast<int>(mouse_pos.x),
274 static_cast<int>(mouse_pos.y));
275 if (draw_box) {
276 if (ctx_->canvas) {
278 }
279 }
280 }
281
282 // Finalize selection on release.
283 if (mouse_left_released) {
284 ctx_->selection->UpdateRectangleSelection(static_cast<int>(mouse_pos.x),
285 static_cast<int>(mouse_pos.y));
286
287 auto* room = GetRoom(ctx_->current_room_id);
288 if (!room) {
290 return;
291 }
292
293 constexpr int kMinRectPixels = 6;
294 if (alt_down || !ctx_->selection->IsRectangleLargeEnough(kMinRectPixels)) {
296 return;
297 }
298
300 room->GetTileObjects(), ObjectSelection::SelectionMode::Single);
301 }
302}
303
304void TileObjectHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
305 (void)delta;
306 if (!is_dragging_ || !ctx_ || !ctx_->selection)
307 return;
308
310
311 // Calculate total drag delta from start (snapped)
312 ImVec2 drag_delta =
314 drag_delta = ApplyDragModifiers(drag_delta);
315
316 const int tile_dx = static_cast<int>(drag_delta.x) / 8;
317 const int tile_dy = static_cast<int>(drag_delta.y) / 8;
318
319 // Calculate incremental move
320 const int inc_dx = tile_dx - drag_last_dx_;
321 const int inc_dy = tile_dy - drag_last_dy_;
322
323 if (inc_dx != 0 || inc_dy != 0) {
327 }
328
330 inc_dx, inc_dy,
331 /*notify_mutation=*/false);
332
333 drag_last_dx_ = tile_dx;
334 drag_last_dy_ = tile_dy;
335 }
336}
337
339 if (is_dragging_) {
340 const bool had_mutation = drag_mutation_started_;
341 is_dragging_ = false;
343 drag_has_duplicated_ = false;
344 // Drag operations mutate incrementally while the mouse is held down. The
345 // editor's undo capture wants to finalize after the drag ends (once the
346 // interaction mode has returned to Select), so emit one more invalidation
347 // on release if anything changed.
348 if (had_mutation && ctx_) {
350 }
351 }
352}
353
354ImVec2 TileObjectHandler::ApplyDragModifiers(const ImVec2& delta) const {
355 return delta;
356}
357
359 if (!HasValidContext() || !ctx_ || !ctx_->selection || delta == 0.0f)
360 return false;
361
362 auto indices = ctx_->selection->GetSelectedIndices();
363 if (indices.empty())
364 return false;
365
366 int resize_delta = (delta > 0.0f) ? 1 : -1;
367 return ResizeObjects(ctx_->current_room_id, indices, resize_delta);
368}
369
372 return;
373
374 const auto pointer_screen_pos = GetPointerScreenPosition();
375 if (!pointer_screen_pos.has_value()) {
376 return;
377 }
378
379 const DungeonCanvasTransform transform = GetCanvasTransform();
380 const auto [canvas_x, canvas_y] =
381 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
382 auto [room_x, room_y] = CanvasToRoom(canvas_x, canvas_y);
383
384 if (!IsWithinBounds(canvas_x, canvas_y))
385 return;
386
387 auto [snap_canvas_x, snap_canvas_y] = RoomToCanvas(room_x, room_y);
388 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
389
390 ImDrawList* draw_list = ImGui::GetWindowDrawList();
391 const ImVec2 preview_start = transform.RoomPixelsToScreen(ImVec2(
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)));
394 const ImVec2 preview_size = transform.RoomSizeToScreen(
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);
399 const ImVec2 bitmap_start = transform.RoomPixelsToScreen(
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)));
404
406 const size_t current_obj_count = room ? room->GetTileObjects().size() : 0;
407 const auto capacity_state = GetPlacementGhostCapacityState();
408
409 const auto& theme = AgentUI::GetTheme();
410 const ImVec4 outline_color = GetPlacementAccentColor(
411 theme, capacity_state, theme.dungeon_selection_primary);
412 bool drew_bitmap = false;
413
415 auto& bitmap = ghost_preview_buffer_->bitmap();
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());
421 const ImVec2 bitmap_size = transform.RoomSizeToScreen(ImVec2(
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
428 ? theme.text_primary
429 : GetPlacementAccentColor(theme, capacity_state,
430 theme.text_primary);
431 tint.w = 0.70f;
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);
437 drew_bitmap = true;
438 }
439 }
440
441 if (!drew_bitmap) {
442 draw_list->AddRectFilled(
443 preview_start, preview_end,
444 ImGui::GetColorU32(
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);
448 }
449
450 // ID label
451 std::string id_text = absl::StrFormat("0x%02X", preview_object_.id_);
452 draw_list->AddText(ImVec2(preview_start.x + 2, preview_start.y + 1),
453 ImGui::GetColorU32(theme.text_primary), id_text.c_str());
454
455 // Capacity tooltip while hovering — proactive warning before user clicks.
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,
460 GetPlacementCapacityTooltipSuffix(capacity_state).data());
461 }
462
463 const std::string badge_text = absl::StrFormat(
464 "Objects %zu/%zu", current_obj_count, zelda3::kMaxTileObjects);
466 ImVec2(preview_start.x, preview_end.y + 6.0f),
467 theme, capacity_state, badge_text);
468}
469
471 if (!HasValidContext() || !ctx_->selection)
472 return;
473
474 auto* room = GetCurrentRoom();
475 if (!room)
476 return;
477
478 // Use ObjectSelection's rendering (handles pulsing border, corner handles)
480 ctx_->canvas, room->GetTileObjects(), [](const zelda3::RoomObject& obj) {
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());
485 });
486
487 if (is_dragging_) {
488 DrawSmartGuides(room->GetTileObjects());
489 }
490}
491
493 int canvas_x, int canvas_y) const {
494 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y)) {
495 return std::nullopt;
496 }
497 auto* room =
498 const_cast<TileObjectHandler*>(this)->GetRoom(ctx_->current_room_id);
499 if (!room)
500 return std::nullopt;
501
502 const auto& objects = room->GetTileObjects();
503 for (size_t i = objects.size(); i > 0; --i) {
504 size_t index = i - 1;
505 const auto& object = objects[index];
506
507 // Respect layer filter if available in context
508 if (ctx_ && ctx_->selection &&
510 continue;
511 }
512
513 auto [obj_tile_x, obj_tile_y, width_tiles, height_tiles] =
515
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;
520
521 if (canvas_x >= obj_px && canvas_x < obj_px + w_px && canvas_y >= obj_py &&
522 canvas_y < obj_py + h_px) {
523 return index;
524 }
525 }
526 return std::nullopt;
527}
528
530 const std::vector<zelda3::RoomObject>& objects) const {
531 if (!ctx_ || !ctx_->canvas || !ctx_->selection ||
533 return;
534 }
535
536 const auto selected = ctx_->selection->GetSelectedIndices();
537 if (selected.empty()) {
538 return;
539 }
540
541 constexpr int kAlignmentTolerancePx = 2;
542 constexpr size_t kMaxGuidesPerAxis = 8;
543 std::vector<int> vertical_guides;
544 std::vector<int> horizontal_guides;
545
546 for (size_t selected_index : selected) {
547 if (selected_index >= objects.size()) {
548 continue;
549 }
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};
555
556 for (size_t other_index = 0; other_index < objects.size(); ++other_index) {
557 if (ctx_->selection->IsObjectSelected(other_index)) {
558 continue;
559 }
560 const GuideRect other_rect = GetGuideRect(objects[other_index]);
561 const std::array<int, 3> other_x = {other_rect.left, other_rect.center_x,
562 other_rect.right};
563 const std::array<int, 3> other_y = {other_rect.top, other_rect.center_y,
564 other_rect.bottom};
565
566 for (int sx : selected_x) {
567 for (int ox : other_x) {
568 if (std::abs(sx - ox) <= kAlignmentTolerancePx) {
569 AddUniqueGuide(vertical_guides, ox);
570 }
571 }
572 }
573 for (int sy : selected_y) {
574 for (int oy : other_y) {
575 if (std::abs(sy - oy) <= kAlignmentTolerancePx) {
576 AddUniqueGuide(horizontal_guides, oy);
577 }
578 }
579 }
580 if (vertical_guides.size() >= kMaxGuidesPerAxis &&
581 horizontal_guides.size() >= kMaxGuidesPerAxis) {
582 break;
583 }
584 }
585 }
586
587 if (vertical_guides.empty() && horizontal_guides.empty()) {
588 return;
589 }
590
591 const auto& theme = AgentUI::GetTheme();
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();
596 const DungeonCanvasTransform transform = GetCanvasTransform();
597 const ImVec2 canvas_pos = transform.room_origin_screen();
598 const ImVec2 room_size = transform.RoomSizeToScreen(ImVec2(
600
601 const size_t vertical_count =
602 std::min(vertical_guides.size(), kMaxGuidesPerAxis);
603 for (size_t i = 0; i < vertical_count; ++i) {
604 const float x =
605 transform.RoomPixelsToScreen(ImVec2(vertical_guides[i], 0.0f)).x;
606 DrawDashedLine(draw_list, ImVec2(x, canvas_pos.y),
607 ImVec2(x, canvas_pos.y + room_size.y), color, 1.2f);
608 }
609
610 const size_t horizontal_count =
611 std::min(horizontal_guides.size(), kMaxGuidesPerAxis);
612 for (size_t i = 0; i < horizontal_count; ++i) {
613 const float y =
614 transform.RoomPixelsToScreen(ImVec2(0.0f, horizontal_guides[i])).y;
615 DrawDashedLine(draw_list, ImVec2(canvas_pos.x, y),
616 ImVec2(canvas_pos.x + room_size.x, y), color, 1.2f);
617 }
618}
619
620// ========================================================================
621// Mutation Logic
622// ========================================================================
623
625 const std::vector<size_t>& indices,
626 int delta_x, int delta_y,
627 bool notify_mutation) {
628 auto* room = GetRoom(room_id);
629 if (!room || indices.empty())
630 return;
631 if (notify_mutation && ctx_)
633
634 auto& objects = room->GetTileObjects();
635 for (size_t index : indices) {
636 if (index < objects.size()) {
637 objects[index].x_ =
638 std::clamp(static_cast<int>(objects[index].x_ + delta_x), 0, 63);
639 objects[index].y_ =
640 std::clamp(static_cast<int>(objects[index].y_ + delta_y), 0, 63);
641 }
642 }
643
644 NotifyChange(room);
645}
646
648 const std::vector<size_t>& indices,
649 int16_t new_id) {
650 auto* room = GetRoom(room_id);
651 if (!room || indices.empty())
652 return;
653
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;
658 });
659 if (!has_change) {
660 return;
661 }
662 if (ctx_)
664
665 for (size_t index : indices) {
666 if (index < objects.size() && objects[index].id_ != new_id) {
667 const uint8_t canonical_size =
668 zelda3::CanonicalRoomObjectSize(new_id, objects[index].size_);
669 // Use the setter so derived flags + tile caches stay coherent.
670 objects[index].set_id(new_id);
671 objects[index].set_size(canonical_size);
672 }
673 }
674 NotifyChange(room);
675}
676
678 const std::vector<size_t>& indices,
679 uint8_t new_size) {
680 auto* room = GetRoom(room_id);
681 if (!room || indices.empty())
682 return;
683
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_)) {
689 return false;
690 }
691 return objects[index].size_ !=
692 zelda3::CanonicalRoomObjectSize(objects[index].id_, new_size);
693 });
694 if (!has_change) {
695 return;
696 }
697 if (ctx_)
698 ctx_->NotifyMutation(MutationDomain::kTileObjects);
699
700 for (size_t index : indices) {
701 if (index < objects.size() &&
702 zelda3::IsRoomObjectSizeEditable(objects[index].id_)) {
703 const uint8_t canonical_size =
704 zelda3::CanonicalRoomObjectSize(objects[index].id_, new_size);
705 if (objects[index].size_ == canonical_size) {
706 continue;
707 }
708 objects[index].size_ = canonical_size;
709 objects[index].tiles_loaded_ = false;
710 }
711 }
712 NotifyChange(room);
713}
714
715bool TileObjectHandler::UpdateObjectsLayer(int room_id,
716 const std::vector<size_t>& indices,
717 int new_layer) {
718 auto* room = GetRoom(room_id);
719 if (!room || indices.empty())
720 return false;
721 if (new_layer < 0 || new_layer > 2) {
722 LOG_WARN("TileObjectHandler",
723 "Rejected layer update with invalid target layer: %d", new_layer);
724 return false;
725 }
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()) {
732 continue;
733 }
734 if (seen_indices.insert(index).second) {
735 deduped_indices.push_back(index);
736 }
737 }
738 if (deduped_indices.empty()) {
739 return false;
740 }
741
742 if (deduped_indices.size() > kMaxLayerBatchMutation) {
743 LOG_WARN("TileObjectHandler",
744 "Rejected layer batch mutation of %zu objects (max %zu)",
745 deduped_indices.size(), kMaxLayerBatchMutation);
746 return false;
747 }
748
749 auto candidate_objects = objects;
750 auto mutation = zelda3::ReassignObjectStorage(candidate_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());
755 return false;
756 }
757 if (!mutation->changed) {
758 return true;
759 }
760
761 if (ctx_)
762 ctx_->NotifyMutation(MutationDomain::kTileObjects);
763 objects = std::move(candidate_objects);
764 RestoreObjectSelection(ctx_ ? ctx_->selection : nullptr,
765 mutation->selected_indices);
766 NotifyChange(room);
767 return true;
768}
769
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())
775 return {};
776 if (notify_mutation && ctx_)
777 ctx_->NotifyMutation(MutationDomain::kTileObjects);
778
779 auto& objects = room->GetTileObjects();
780 std::vector<size_t> new_indices;
781
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()));
790 }
791 }
792
793 NotifyChange(room);
794 return new_indices;
795}
796
797void TileObjectHandler::DeleteObjects(int room_id,
798 std::vector<size_t> indices) {
799 auto* room = GetRoom(room_id);
800 if (!room || indices.empty())
801 return;
802 if (ctx_)
803 ctx_->NotifyMutation(MutationDomain::kTileObjects);
804
805 std::sort(indices.rbegin(), indices.rend());
806 for (size_t index : indices) {
807 room->RemoveTileObject(index);
808 }
809
810 NotifyChange(room);
811}
812
813void TileObjectHandler::DeleteAllObjects(int room_id) {
814 auto* room = GetRoom(room_id);
815 if (!room)
816 return;
817 if (ctx_)
818 ctx_->NotifyMutation(MutationDomain::kTileObjects);
819 room->ClearTileObjects();
820 NotifyChange(room);
821}
822
823void TileObjectHandler::SendToFront(int room_id,
824 const std::vector<size_t>& indices) {
825 auto* room = GetRoom(room_id);
826 if (!room || indices.empty())
827 return;
828 auto& objects = room->GetTileObjects();
829 auto selected_set = MakeValidIndexSet(indices, objects.size());
830 if (selected_set.empty()) {
831 return;
832 }
833 if (ctx_)
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));
844 } else {
845 other.push_back(std::move(entry));
846 }
847 }
848 bucket = std::move(other);
849 bucket.insert(bucket.end(), std::make_move_iterator(selected.begin()),
850 std::make_move_iterator(selected.end()));
851 }
852 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
853 NotifyChange(room);
854}
855
856void TileObjectHandler::SendToBack(int room_id,
857 const std::vector<size_t>& indices) {
858 auto* room = GetRoom(room_id);
859 if (!room || indices.empty())
860 return;
861 auto& objects = room->GetTileObjects();
862 auto selected_set = MakeValidIndexSet(indices, objects.size());
863 if (selected_set.empty()) {
864 return;
865 }
866 if (ctx_)
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));
877 } else {
878 other.push_back(std::move(entry));
879 }
880 }
881 bucket = std::move(selected);
882 bucket.insert(bucket.end(), std::make_move_iterator(other.begin()),
883 std::make_move_iterator(other.end()));
884 }
885 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
886 NotifyChange(room);
887}
888
889void TileObjectHandler::MoveForward(int room_id,
890 const std::vector<size_t>& indices) {
891 auto* room = GetRoom(room_id);
892 if (!room || indices.empty())
893 return;
894 auto& objects = room->GetTileObjects();
895 auto selected_set = MakeValidIndexSet(indices, objects.size());
896 if (selected_set.empty()) {
897 return;
898 }
899 if (ctx_)
900 ctx_->NotifyMutation(MutationDomain::kTileObjects);
901 auto buckets = BuildLayerBuckets(objects, selected_set);
902 for (auto& bucket : buckets) {
903 if (bucket.size() < 2) {
904 continue;
905 }
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]);
910 }
911 }
912 }
913 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
914 NotifyChange(room);
915}
916
917void TileObjectHandler::MoveBackward(int room_id,
918 const std::vector<size_t>& indices) {
919 auto* room = GetRoom(room_id);
920 if (!room || indices.empty())
921 return;
922 auto& objects = room->GetTileObjects();
923 auto selected_set = MakeValidIndexSet(indices, objects.size());
924 if (selected_set.empty()) {
925 return;
926 }
927 if (ctx_)
928 ctx_->NotifyMutation(MutationDomain::kTileObjects);
929 auto buckets = BuildLayerBuckets(objects, selected_set);
930 for (auto& bucket : buckets) {
931 if (bucket.size() < 2) {
932 continue;
933 }
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]);
938 }
939 }
940 }
941 FlattenLayerBuckets(objects, buckets, ctx_ ? ctx_->selection : nullptr);
942 NotifyChange(room);
943}
944
945bool TileObjectHandler::ResizeObjects(int room_id,
946 const std::vector<size_t>& indices,
947 int delta) {
948 auto* room = GetRoom(room_id);
949 if (!room || indices.empty())
950 return false;
951 auto& objects = room->GetTileObjects();
952 const auto resized_size = [&](const zelda3::RoomObject& object) {
953 const int requested_size = static_cast<int>(object.size_) + delta;
955 object.id_, static_cast<uint8_t>(std::clamp(requested_size, 0, 255)));
956 };
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]);
962 });
963 if (!has_change) {
964 return false;
965 }
966 if (ctx_)
967 ctx_->NotifyMutation(MutationDomain::kTileObjects);
968
969 for (size_t index : indices) {
970 if (index < objects.size() &&
971 zelda3::IsRoomObjectSizeEditable(objects[index].id_)) {
972 const uint8_t new_size = resized_size(objects[index]);
973 if (objects[index].size_ == new_size) {
974 continue;
975 }
976 objects[index].size_ = new_size;
977 objects[index].tiles_loaded_ = false;
978 }
979 }
980 NotifyChange(room);
981 return true;
982}
983
984bool TileObjectHandler::PlaceObjectAt(int room_id,
985 const zelda3::RoomObject& object, int x,
986 int y) {
987 auto* room = GetRoom(room_id);
988 if (!room) {
989 placement_block_reason_ = PlacementBlockReason::kInvalidRoom;
990 return false;
991 }
992
993 // Hard-stop: enforce ROM object limit before committing placement.
994 if (room->GetTileObjects().size() >= zelda3::kMaxTileObjects) {
995 placement_block_reason_ = PlacementBlockReason::kObjectLimit;
996 return false;
997 }
998
999 placement_block_reason_ = PlacementBlockReason::kNone;
1000 if (ctx_)
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);
1006 NotifyChange(room);
1007 TriggerSuccessToast();
1008 return true;
1009}
1010
1011void TileObjectHandler::SetPreviewObject(const zelda3::RoomObject& object) {
1012 preview_object_ = object;
1013 if (object_placement_mode_) {
1014 RenderGhostPreviewBitmap();
1015 }
1016}
1017
1018void TileObjectHandler::RenderGhostPreviewBitmap() {
1019 ghost_preview_bitmap_ready_ = false;
1020 if (!ctx_ || !ctx_->rom || !ctx_->rom->is_loaded())
1021 return;
1022
1023 auto* room = GetRoom(ctx_->current_room_id);
1024 if (!room || !room->IsLoaded())
1025 return;
1026
1027 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
1028
1029 // Keep one room-sized scratch texture for the handler lifetime. Replacing a
1030 // textured bitmap during ImGui frame construction can invalidate draw-list
1031 // texture handles, and repeatedly allocating previews leaks those handles.
1032 if (!ghost_preview_buffer_) {
1033 ghost_preview_buffer_ = std::make_unique<gfx::BackgroundBuffer>(
1034 kGhostPreviewBufferSize, kGhostPreviewBufferSize);
1035 }
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();
1043
1044 zelda3::ObjectDrawer drawer(ctx_->rom, ctx_->current_room_id, gfx_data);
1045 drawer.InitializeDrawRoutines();
1046
1047 // Replay at the same safe anchor ObjectGeometry uses for measurement so
1048 // routines that draw upward or leftward do not clip against buffer origin.
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;
1052 // A placement preview is a flattened visual. Force the replay through BG1
1053 // so a sampled BG2-stream object cannot mask pixels in the same scratch
1054 // buffer that it just rendered.
1055 render_object.layer_ = zelda3::RoomObject::LayerType::BG1;
1056 auto status =
1057 drawer.DrawObject(render_object, *ghost_preview_buffer_,
1058 *ghost_preview_buffer_, ctx_->current_palette_group);
1059 if (!status.ok()) {
1060 return;
1061 }
1062
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;
1073 }
1075 if (bitmap.texture()) {
1076 ghost_preview_create_queued_ = false;
1077 }
1078 ghost_preview_bitmap_ready_ = true;
1079 }
1080}
1081
1083TileObjectHandler::CalculateGhostPreviewGeometry(
1084 const zelda3::RoomObject& object) {
1085 const auto dimensions = zelda3::DimensionService::Get().GetDimensions(object);
1086 const auto [resolved_anchor_x, resolved_anchor_y] =
1087 zelda3::ObjectGeometry::Get().ResolveAnchor(object.id_, object.size_);
1088 // ResolveAnchor supplies routine-specific headroom. Dimension offsets cover
1089 // fallback/custom geometry too, so keep enough room for either source.
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;
1096 return GhostPreviewGeometry{
1097 .render_anchor_x_tiles = anchor_x,
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,
1105 };
1106}
1107
1108// ========================================================================
1109// Clipboard Operations
1110// ========================================================================
1111
1112void TileObjectHandler::CopyObjectsToClipboard(
1113 int room_id, const std::vector<size_t>& indices) {
1114 auto* room = GetRoom(room_id);
1115 if (!room || indices.empty())
1116 return;
1117
1118 clipboard_.clear();
1119 const auto& objects = room->GetTileObjects();
1120
1121 for (size_t idx : indices) {
1122 if (idx < objects.size()) {
1123 clipboard_.push_back(objects[idx]);
1124 }
1125 }
1126}
1127
1128std::vector<size_t> TileObjectHandler::PasteFromClipboard(int room_id,
1129 int offset_x,
1130 int offset_y) {
1131 auto* room = GetRoom(room_id);
1132 if (!room || clipboard_.empty())
1133 return {};
1134 if (ctx_)
1135 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1136
1137 std::vector<size_t> new_indices;
1138 size_t base_index = room->GetTileObjects().size();
1139
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++);
1147 }
1148
1149 NotifyChange(room);
1150 return new_indices;
1151}
1152
1153std::vector<size_t> TileObjectHandler::PasteFromClipboardAt(int room_id,
1154 int target_x,
1155 int target_y) {
1156 if (clipboard_.empty())
1157 return {};
1158
1159 int offset_x = target_x - clipboard_[0].x_;
1160 int offset_y = target_y - clipboard_[0].y_;
1161
1162 return PasteFromClipboard(room_id, offset_x, offset_y);
1163}
1164
1165} // 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.
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.
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_
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)
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.
void InitDrag(const ImVec2 &start_pos)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
SDL_Surface * surface() const
Definition bitmap.h:400
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()
Definition room.h:439
auto & object_bg2_buffer()
Definition room.h:982
auto & bg1_buffer()
Definition room.h:976
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:383
auto & object_bg1_buffer()
Definition room.h:980
auto & bg2_buffer()
Definition room.h:977
#define LOG_WARN(category, format,...)
Definition log.h:107
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
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.
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.