4#include <unordered_set>
5#include "absl/strings/str_format.h"
6#include "imgui/imgui.h"
28 room_id >=
static_cast<int>(
ctx_->
rooms->size())) {
35 if (!room || !
ctx_)
return;
58 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
69 if (!hovered.has_value())
return false;
71 const ImGuiIO& io = ImGui::GetIO();
75 if (io.KeyAlt)
return true;
80 }
else if (io.KeyCtrl || io.KeySuper) {
101 static_cast<int>(start_pos.y));
105 bool mouse_left_down,
106 bool mouse_left_released,
115 if (mouse_left_down) {
117 static_cast<int>(mouse_pos.y));
126 if (mouse_left_released) {
128 static_cast<int>(mouse_pos.y));
136 constexpr int kMinRectPixels = 6;
145 }
else if (toggle_down) {
158 const bool alt_down = ImGui::GetIO().KeyAlt;
165 const int tile_dx =
static_cast<int>(drag_delta.x) / 8;
166 const int tile_dy =
static_cast<int>(drag_delta.y) / 8;
183 for (
size_t idx : new_indices) {
193 if (inc_dx != 0 || inc_dy != 0) {
218 if (had_mutation &&
ctx_) {
225 const ImGuiIO& io = ImGui::GetIO();
226 if (!io.KeyShift)
return delta;
227 if (std::abs(delta.x) >= std::abs(delta.y))
return ImVec2(delta.x, 0.0f);
228 return ImVec2(0.0f, delta.y);
236 if (indices.empty())
return false;
238 int resize_delta = (delta > 0.0f) ? 1 : -1;
246 const ImGuiIO& io = ImGui::GetIO();
248 ImVec2 mouse_pos = io.MousePos;
251 ImVec2 canvas_mouse_pos = ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
252 auto [room_x, room_y] =
CanvasToRoom(
static_cast<int>(canvas_mouse_pos.x),
static_cast<int>(canvas_mouse_pos.y));
254 if (!
IsWithinBounds(
static_cast<int>(canvas_mouse_pos.x),
static_cast<int>(canvas_mouse_pos.y)))
return;
256 auto [snap_canvas_x, snap_canvas_y] =
RoomToCanvas(room_x, room_y);
259 ImDrawList* draw_list = ImGui::GetWindowDrawList();
260 ImVec2 preview_start(canvas_pos.x + snap_canvas_x * scale, canvas_pos.y + snap_canvas_y * scale);
261 ImVec2 preview_end(preview_start.x + obj_width * scale, preview_start.y + obj_height * scale);
264 size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
269 ImVec4 outline_color = theme.dungeon_selection_primary;
271 outline_color = theme.status_error;
272 }
else if (near_obj_limit) {
273 outline_color = theme.status_warning;
275 bool drew_bitmap =
false;
279 if (bitmap.texture()) {
280 ImVec2 bitmap_end(preview_start.x + bitmap.width() * scale, preview_start.y + bitmap.height() * scale);
281 ImVec4 tint = at_obj_limit ? theme.status_error : theme.text_primary;
283 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), preview_start,
284 bitmap_end, ImVec2(0, 0), ImVec2(1, 1),
285 ImGui::GetColorU32(tint));
286 draw_list->AddRect(preview_start, bitmap_end, ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
292 draw_list->AddRectFilled(preview_start, preview_end,
293 ImGui::GetColorU32(ImVec4(outline_color.x, outline_color.y, outline_color.z, 0.25f)));
294 draw_list->AddRect(preview_start, preview_end, ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
299 draw_list->AddText(ImVec2(preview_start.x + 2, preview_start.y + 1), ImGui::GetColorU32(theme.text_primary), id_text.c_str());
302 if ((at_obj_limit || near_obj_limit) &&
303 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
305 at_obj_limit ?
"\nPlacement blocked" :
"\nNear limit");
319 auto result = zelda3::DimensionService::Get().GetDimensions(obj);
320 return std::make_tuple(result.offset_x_tiles * 8,
321 result.offset_y_tiles * 8,
322 result.width_pixels(), result.height_pixels());
328 if (!room)
return std::nullopt;
331 for (
size_t i = objects.size(); i > 0; --i) {
332 size_t index = i - 1;
333 const auto&
object = objects[index];
342 int obj_px = obj_tile_x * 8;
343 int obj_py = obj_tile_y * 8;
344 int w_px = width_tiles * 8;
345 int h_px = height_tiles * 8;
347 if (canvas_x >= obj_px && canvas_x < obj_px + w_px &&
348 canvas_y >= obj_py && canvas_y < obj_py + h_px) {
360 const std::vector<size_t>& indices,
361 int delta_x,
int delta_y,
362 bool notify_mutation) {
364 if (!room || indices.empty())
return;
367 auto& objects = room->GetTileObjects();
368 for (
size_t index : indices) {
369 if (index < objects.size()) {
370 objects[index].x_ = std::clamp(
static_cast<int>(objects[index].x_ + delta_x), 0, 63);
371 objects[index].y_ = std::clamp(
static_cast<int>(objects[index].y_ + delta_y), 0, 63);
380 if (!room || indices.empty())
return;
383 auto& objects = room->GetTileObjects();
384 for (
size_t index : indices) {
385 if (index < objects.size()) {
387 objects[index].set_id(new_id);
395 if (!room || indices.empty())
return;
398 auto& objects = room->GetTileObjects();
399 for (
size_t index : indices) {
400 if (index < objects.size()) {
401 objects[index].size_ = new_size;
402 objects[index].tiles_loaded_ =
false;
410 if (!room || indices.empty())
return;
411 if (new_layer < 0 || new_layer > 2) {
413 "Rejected layer update with invalid target layer: %d", new_layer);
416 auto& objects = room->GetTileObjects();
417 std::vector<size_t> deduped_indices;
418 deduped_indices.reserve(indices.size());
419 std::unordered_set<size_t> seen_indices;
420 for (
size_t index : indices) {
421 if (index >= objects.size()) {
424 if (seen_indices.insert(index).second) {
425 deduped_indices.push_back(index);
428 if (deduped_indices.empty()) {
432 if (deduped_indices.size() > kMaxLayerBatchMutation) {
434 "Rejected layer batch mutation of %zu objects (max %zu)",
435 deduped_indices.size(), kMaxLayerBatchMutation);
439 int current_bg3_count = 0;
440 for (
const auto&
object : objects) {
441 if (
object.GetLayerValue() == 2) {
446 int moving_to_bg3 = 0;
447 int moving_from_bg3 = 0;
449 for (
size_t index : deduped_indices) {
450 const auto&
object = objects[index];
452 if (semantics.draws_to_both_bgs &&
456 if (
object.layer_ == target_layer) {
459 if (
object.GetLayerValue() == 2) {
462 if (new_layer == 2) {
466 const int projected_bg3_count =
467 current_bg3_count - moving_from_bg3 + moving_to_bg3;
470 "Rejected layer mutation: projected BG3 count %d exceeds max %d",
475 bool changed =
false;
476 for (
size_t index : deduped_indices) {
477 auto&
object = objects[index];
479 if (semantics.draws_to_both_bgs &&
483 if (
object.layer_ == target_layer) {
486 object.layer_ = target_layer;
498 int room_id,
const std::vector<size_t>& indices,
int delta_x,
int delta_y,
499 bool notify_mutation) {
501 if (!room || indices.empty())
return {};
504 auto& objects = room->GetTileObjects();
505 std::vector<size_t> new_indices;
507 const size_t base_index = objects.size();
508 for (
size_t index : indices) {
509 if (index < objects.size()) {
510 auto clone = objects[index];
511 clone.x_ = std::clamp(
static_cast<int>(clone.x_ + delta_x), 0, 63);
512 clone.y_ = std::clamp(
static_cast<int>(clone.y_ + delta_y), 0, 63);
513 objects.push_back(clone);
514 new_indices.push_back(base_index + (new_indices.size()));
524 if (!room || indices.empty())
return;
527 std::sort(indices.rbegin(), indices.rend());
528 for (
size_t index : indices) {
529 room->RemoveTileObject(index);
539 room->ClearTileObjects();
545 if (!room || indices.empty())
return;
548 auto& objects = room->GetTileObjects();
549 std::vector<zelda3::RoomObject> selected, other;
551 for (
size_t i = 0; i < objects.size(); ++i) {
552 if (std::find(indices.begin(), indices.end(), i) != indices.end())
553 selected.push_back(objects[i]);
555 other.push_back(objects[i]);
558 objects = std::move(other);
559 objects.insert(objects.end(), selected.begin(), selected.end());
565 if (!room || indices.empty())
return;
568 auto& objects = room->GetTileObjects();
569 std::vector<zelda3::RoomObject> selected, other;
571 for (
size_t i = 0; i < objects.size(); ++i) {
572 if (std::find(indices.begin(), indices.end(), i) != indices.end())
573 selected.push_back(objects[i]);
575 other.push_back(objects[i]);
578 objects = std::move(selected);
579 objects.insert(objects.end(), other.begin(), other.end());
585 if (!room || indices.empty())
return;
588 auto& objects = room->GetTileObjects();
589 auto sorted_indices = indices;
590 std::sort(sorted_indices.rbegin(), sorted_indices.rend());
592 for (
size_t idx : sorted_indices) {
593 if (idx < objects.size() - 1) {
594 std::swap(objects[idx], objects[idx + 1]);
602 if (!room || indices.empty())
return;
605 auto& objects = room->GetTileObjects();
606 auto sorted_indices = indices;
607 std::sort(sorted_indices.begin(), sorted_indices.end());
609 for (
size_t idx : sorted_indices) {
611 std::swap(objects[idx], objects[idx - 1]);
619 if (!room || indices.empty())
return;
621 auto& objects = room->GetTileObjects();
622 for (
size_t index : indices) {
623 if (index < objects.size()) {
624 int new_size = std::clamp(
static_cast<int>(objects[index].size_) + delta, 0, 15);
625 objects[index].size_ =
static_cast<uint8_t
>(new_size);
626 objects[index].tiles_loaded_ =
false;
647 auto new_obj = object;
648 new_obj.
x_ = std::clamp(x, 0, 63);
649 new_obj.y_ = std::clamp(y, 0, 63);
650 room->AddTileObject(new_obj);
668 if (!room || !room->IsLoaded())
return;
671 width = std::max(width, 16);
672 height = std::max(height, 16);
675 const uint8_t* gfx_data = room->get_gfx_buffer().data();
687 if (bitmap.size() > 0) {
704 int room_id,
const std::vector<size_t>& indices) {
706 if (!room || indices.empty())
return;
709 const auto& objects = room->GetTileObjects();
711 for (
size_t idx : indices) {
712 if (idx < objects.size()) {
719 int room_id,
int offset_x,
int offset_y) {
724 std::vector<size_t> new_indices;
725 size_t base_index = room->GetTileObjects().size();
728 obj.x_ = std::clamp(obj.x_ + offset_x, 0, 63);
729 obj.y_ = std::clamp(obj.y_ + offset_y, 0, 63);
730 obj.tiles_loaded_ =
false;
731 room->AddTileObject(obj);
732 new_indices.push_back(base_index++);
740 int room_id,
int target_x,
int target_y) {
std::pair< int, int > CanvasToRoom(int canvas_x, int canvas_y) const
Convert canvas pixel coordinates to room tile coordinates.
void TriggerSuccessToast()
InteractionContext * ctx_
bool IsWithinBounds(int canvas_x, int canvas_y) const
Check if coordinates are within room bounds.
float GetCanvasScale() const
Get canvas global scale.
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.
ImVec2 GetCanvasZeroPoint() const
Get canvas zero point (for screen coordinate conversion)
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.
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.
Handles functional mutations and queries for tile objects.
zelda3::RoomObject preview_object_
void DrawGhostPreview() override
Draw ghost preview during placement.
std::vector< zelda3::RoomObject > clipboard_
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)
void SendToFront(int room_id, const std::vector< size_t > &indices)
Reorder objects.
void RenderGhostPreviewBitmap()
std::vector< size_t > DuplicateObjects(int room_id, const std::vector< size_t > &indices, int delta_x, int delta_y, bool notify_mutation=true)
Clone a set of objects and move them by a tile delta.
void DeleteAllObjects(int room_id)
Delete all objects in a room.
void MoveBackward(int room_id, const std::vector< size_t > &indices)
ImVec2 ApplyDragModifiers(const ImVec2 &delta) const
void HandleRelease() override
Handle mouse release.
void CopyObjectsToClipboard(int room_id, const std::vector< size_t > &indices)
Copy objects to internal clipboard.
bool drag_has_duplicated_
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
void UpdateObjectsLayer(int room_id, const std::vector< size_t > &indices, int new_layer)
std::vector< size_t > PasteFromClipboardAt(int room_id, int target_x, int target_y)
Paste objects from clipboard at target location. Use first clipboard item as origin.
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.
void SendToBack(int room_id, const std::vector< size_t > &indices)
std::vector< size_t > PasteFromClipboard(int room_id, int offset_x, int offset_y)
Paste objects from clipboard with offset.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
bool object_placement_mode_
void SetPreviewObject(const zelda3::RoomObject &object)
Set object for placement.
void BeginMarqueeSelection(const ImVec2 &start_pos)
void MoveForward(int room_id, const std::vector< size_t > &indices)
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.
PlacementBlockReason placement_block_reason_
void DeleteObjects(int room_id, std::vector< size_t > indices)
Delete objects by indices.
bool drag_mutation_started_
void UpdateObjectsId(int room_id, const std::vector< size_t > &indices, int16_t new_id)
zelda3::Room * GetRoom(int room_id)
std::pair< int, int > CalculateObjectBounds(const zelda3::RoomObject &object)
void NotifyChange(zelda3::Room *room)
void ResizeObjects(int room_id, const std::vector< size_t > &indices, int delta)
Resize objects by a delta.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void InitDrag(const ImVec2 &start_pos)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
void ProcessTextureQueue(IRenderer *renderer)
static DimensionService & Get()
std::tuple< int, int, int, int > GetHitTestBounds(const RoomObject &obj) const
std::pair< int, int > GetPixelDimensions(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.
const std::vector< RoomObject > & GetTileObjects() const
#define LOG_WARN(category, format,...)
const AgentUITheme & GetTheme()
constexpr size_t kMaxLayerBatchMutation
ImVec2 SnapToTileGrid(const ImVec2 &point)
Snap a point to the 8px tile grid.
Editors are the view controllers for the application.
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
constexpr size_t kMaxBg3Objects
constexpr size_t kMaxTileObjects
void NotifyInvalidateCache(MutationDomain domain=MutationDomain::kUnknown) const
Notify that cache invalidation is needed.
std::array< zelda3::Room, dungeon_coords::kRoomCount > * rooms
ObjectSelection * selection
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.
gfx::PaletteGroup current_palette_group