10#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
38 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor)
39 : renderer_(renderer),
41 canvas_viewer_(canvas_viewer),
42 object_selector_(rom),
43 object_editor_(object_editor) {
95 auto indices = interaction.GetSelectedObjectIndices();
100 for (
size_t idx : indices) {
118 if (tile_handler.was_placement_blocked()) {
120 tile_handler.clear_placement_blocked();
124 "Object limit reached (%d max) - placement blocked",
137 auto& sprite_handler = coordinator.sprite_handler();
138 if (sprite_handler.was_placement_blocked()) {
139 const auto reason = sprite_handler.placement_block_reason();
140 sprite_handler.clear_placement_blocked();
144 "Sprite limit reached (%d max) - placement blocked",
157 auto& door_handler = coordinator.door_handler();
158 if (door_handler.was_placement_blocked()) {
159 const auto reason = door_handler.placement_block_reason();
160 door_handler.clear_placement_blocked();
164 "Door limit reached (%d max) - placement blocked", max_doors));
185 ImGuiTreeNodeFlags_DefaultOpen)) {
192 float available_height = ImGui::GetContentRegionAvail().y;
194 float reserved_height = 60.0f;
197 reserved_height += 200.0f;
199 float browser_height = std::max(150.0f, available_height - reserved_height);
201 ImGui::BeginChild(
"ObjectBrowserRegion", ImVec2(0, browser_height),
true);
221 ImGui::TextColored(theme.status_warning,
231 " Selection Mode - Click to select, drag to multi-select");
256 bool preview_open = ImGui::CollapsingHeader(
ICON_MD_MONITOR " Preview");
260 ImGui::PushID(
"PreviewSection");
303 static constexpr std::array<zelda3::DoorType, 20> kDoorTypes = {{
329 theme.status_warning,
345 constexpr float kPreviewSize = 32.0f;
346 constexpr int kItemsPerRow = 5;
347 float panel_width = ImGui::GetContentRegionAvail().x;
349 std::max(1,
static_cast<int>(panel_width / (kPreviewSize + 8)));
351 ImGui::BeginChild(
"##DoorTypeGrid", ImVec2(0, 80),
true,
352 ImGuiWindowFlags_HorizontalScrollbar);
355 for (
size_t i = 0; i < kDoorTypes.size(); ++i) {
356 auto door_type = kDoorTypes[i];
359 ImGui::PushID(
static_cast<int>(i));
364 int type_val =
static_cast<int>(door_type);
365 if (type_val <= 0x12) {
366 button_color = ImVec4(0.3f, 0.5f, 0.7f, 1.0f);
367 }
else if (type_val <= 0x1E) {
368 button_color = ImVec4(0.7f, 0.5f, 0.3f, 1.0f);
370 button_color = ImVec4(0.5f, 0.7f, 0.3f, 1.0f);
374 button_color.x += 0.2f;
375 button_color.y += 0.2f;
376 button_color.z += 0.2f;
381 {ImGuiCol_Button, button_color},
382 {ImGuiCol_ButtonHovered,
383 ImVec4(button_color.x + 0.1f, button_color.y + 0.1f,
384 button_color.z + 0.1f, 1.0f)},
385 {ImGuiCol_ButtonActive,
386 ImVec4(button_color.x + 0.2f, button_color.y + 0.2f,
387 button_color.z + 0.2f, 1.0f)},
391 std::string label = absl::StrFormat(
"%02X", type_val);
392 if (ImGui::Button(label.c_str(), ImVec2(kPreviewSize, kPreviewSize))) {
403 if (ImGui::IsItemHovered()) {
404 ImGui::SetTooltip(
"%s (0x%02X)\nClick to select for placement",
411 ImVec2 min = ImGui::GetItemRectMin();
412 ImVec2 max = ImGui::GetItemRectMax();
413 ImGui::GetWindowDrawList()->AddRect(min, max, IM_COL32(255, 255, 0, 255),
420 if (col < items_per_row && i < kDoorTypes.size() - 1) {
433 const auto& doors = room.GetDoors();
435 if (!doors.empty()) {
436 ImGui::Text(
ICON_MD_LIST " Room Doors (%zu):", doors.size());
438 ImGui::BeginChild(
"##DoorList", ImVec2(0, 80),
true);
439 for (
size_t i = 0; i < doors.size(); ++i) {
440 const auto& door = doors[i];
441 auto [tile_x, tile_y] = door.GetTileCoords();
443 ImGui::PushID(
static_cast<int>(i));
448 ImGui::Text(
"[%zu] %s (%s)", i, type_name.c_str(), dir_name.c_str());
450 ImGui::TextColored(theme.text_secondary_gray,
"@ (%d,%d)", tile_x,
456 mutable_room.RemoveDoor(i);
463 ImGui::TextColored(theme.text_secondary_gray,
472 ImGui::TextColored(theme.text_secondary_gray,
475 "Uses SNES emulation to render objects accurately.\n"
476 "May impact performance.");
480 ImGui::BeginChild(
"##EmulatorPreviewRegion", ImVec2(0, 260),
true);
493 if (!selected.empty()) {
494 ImGui::TextColored(theme.status_success,
498 if (selected.size() == 1) {
501 if (selected[0] < objects.size()) {
502 const auto& obj = objects[selected[0]];
504 ImGui::Text(
"Object #%zu (ID: 0x%02X)", selected[0], obj.id_);
506 theme.text_secondary_gray,
507 " Position: (%d, %d) Size: 0x%02X Layer: %s Draws: %s",
508 obj.x_, obj.y_, obj.size_,
515 ImGui::Text(
"1 object");
518 ImGui::Text(
"%zu objects", selected.size());
520 ImGui::TextColored(theme.text_secondary_gray,
521 "(Shift+click to add, Ctrl+click to toggle)");
529 ImGui::TextColored(theme.text_info,
ICON_MD_INFO " Current:");
536 ImGui::Text(
"Layer: %s Draws: %s",
577 if (!room.IsLoaded()) {
578 room.LoadRoomGraphics(room.blockset());
591 if (preview_obj.
tiles().empty()) {
596 const uint8_t* gfx_data = room.get_gfx_buffer().data();
606 std::vector<SDL_Color> colors(256);
607 size_t color_index = 0;
608 for (
size_t pal_idx = 0;
609 pal_idx < palette_group.
size() && color_index < 256; ++pal_idx) {
610 const auto& pal = palette_group[pal_idx];
611 for (
size_t i = 0; i < pal.size() && color_index < 256; ++i) {
612 ImVec4 rgb = pal[i].rgb();
613 colors[color_index++] = {
static_cast<Uint8
>(rgb.x),
614 static_cast<Uint8
>(rgb.y),
615 static_cast<Uint8
>(rgb.z), 255};
618 colors[255] = {0, 0, 0, 0};
619 bitmap.SetPalette(colors);
620 if (bitmap.surface()) {
621 SDL_SetColorKey(bitmap.surface(), SDL_TRUE, 255);
622 SDL_SetSurfaceBlendMode(bitmap.surface(), SDL_BLENDMODE_BLEND);
634 if (bitmap.modified() && bitmap.surface() &&
635 bitmap.mutable_data().size() > 0) {
636 SDL_LockSurface(bitmap.surface());
637 size_t surface_size = bitmap.surface()->h * bitmap.surface()->pitch;
638 size_t data_size = bitmap.mutable_data().size();
639 if (surface_size >= data_size) {
640 memcpy(bitmap.surface()->pixels, bitmap.mutable_data().data(),
643 SDL_UnlockSurface(bitmap.surface());
668 {ImGuiCol_Header, ImVec4(0.15f, 0.25f, 0.35f, 1.0f)},
669 {ImGuiCol_HeaderHovered, ImVec4(0.20f, 0.30f, 0.40f, 1.0f)},
672 bool header_open = ImGui::CollapsingHeader(
677 ImGuiTreeNodeFlags_DefaultOpen);
684 if (ImGui::BeginTable(
"StaticEditorLayout", 2,
685 ImGuiTableFlags_BordersInnerV)) {
686 ImGui::TableSetupColumn(
"Info", ImGuiTableColumnFlags_WidthFixed, 200);
687 ImGui::TableSetupColumn(
"Preview", ImGuiTableColumnFlags_WidthStretch);
689 ImGui::TableNextRow();
692 ImGui::TableNextColumn();
695 ImGui::TextColored(theme.text_info,
ICON_MD_TAG " Object ID");
703 ImGui::TextColored(theme.text_info,
ICON_MD_BRUSH " Draw Routine");
715 ImGui::Text(
"Orientation: %s",
720 ImGui::TextColored(theme.status_warning,
ICON_MD_LAYERS " Both BG");
730 ImGui::SetClipboardText(
733 if (ImGui::IsItemHovered()) {
734 ImGui::SetTooltip(
"Copy object ID to clipboard");
737 if (ImGui::Button(
ICON_MD_CODE " Export ASM", ImVec2(-1, 0))) {
740 if (ImGui::IsItemHovered()) {
741 ImGui::SetTooltip(
"Export object draw routine as ASM (Phase 5)");
750 if (ImGui::IsItemHovered()) {
751 ImGui::SetTooltip(
"Open tile editor to rearrange 8x8 tiles");
763 ImGui::TableNextColumn();
765 ImGui::TextColored(theme.text_secondary_gray,
"Preview:");
792 ImVec2(24, 56),
"No preview available",
793 ImGui::GetColorU32(theme.text_secondary_gray));
799 ImGui::TextColored(theme.text_secondary_gray,
ICON_MD_INFO
800 " Double-click objects in browser\n"
801 "to view their draw routine info.");
824 size_t object_count = room.GetTileObjects().size();
825 size_t sprite_count = room.GetSprites().size();
826 size_t door_count = room.GetDoors().size();
830 for (
const auto& obj : room.GetTileObjects()) {
831 if (obj.id_ >= 0xF9 && obj.id_ <= 0xFD) {
843 auto usage_color = [&](
size_t count,
int max_val) -> ImVec4 {
844 float ratio =
static_cast<float>(count) /
static_cast<float>(max_val);
846 return theme.status_error;
848 if (ratio >= 0.75f) {
849 return theme.status_warning;
851 return theme.text_secondary_gray;
855 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
857 ImGui::TextColored(usage_color(object_count, kMaxObjects),
859 if (ImGui::IsItemHovered()) {
860 ImGui::SetTooltip(
"Objects: %zu of %d maximum", object_count, kMaxObjects);
864 ImGui::TextColored(usage_color(sprite_count, kMaxSprites),
866 if (ImGui::IsItemHovered()) {
867 ImGui::SetTooltip(
"Sprites: %zu of %d maximum", sprite_count, kMaxSprites);
871 ImGui::TextColored(usage_color(door_count, kMaxDoors),
873 if (ImGui::IsItemHovered()) {
874 ImGui::SetTooltip(
"Doors: %zu of %d maximum", door_count, kMaxDoors);
878 ImGui::TextColored(usage_color(chest_count, kMaxChests),
880 if (ImGui::IsItemHovered()) {
881 ImGui::SetTooltip(
"Chests: %d of %d maximum", chest_count, kMaxChests);
884 ImGui::PopStyleVar();
889 if (!result.errors.empty() || !result.warnings.empty()) {
890 for (
const auto& err : result.errors) {
891 ImGui::TextColored(theme.status_error,
ICON_MD_ERROR " %s", err.c_str());
893 for (
const auto& warn : result.warnings) {
911 ImGui::SetNextWindowSize(ImVec2(340, 0), ImGuiCond_Appearing);
913 ImGuiWindowFlags_NoCollapse)) {
916 auto shortcut_row = [&](
const char* keys,
const char* desc) {
917 ImGui::TextColored(theme.status_warning,
"%-18s", keys);
919 ImGui::TextUnformatted(desc);
924 shortcut_row(
"Ctrl+A",
"Select all objects");
925 shortcut_row(
"Ctrl+Shift+A",
"Deselect all");
926 shortcut_row(
"Tab / Shift+Tab",
"Cycle selection");
927 shortcut_row(
"Escape",
"Cancel placement / deselect");
930 ImGui::TextColored(theme.status_success,
ICON_MD_EDIT " Editing");
932 shortcut_row(
"Delete",
"Remove selected");
933 shortcut_row(
"Ctrl+D",
"Duplicate selected");
934 shortcut_row(
"Ctrl+C",
"Copy selected");
935 shortcut_row(
"Ctrl+V",
"Paste");
936 shortcut_row(
"Ctrl+Z",
"Undo");
937 shortcut_row(
"Ctrl+Shift+Z",
"Redo");
942 shortcut_row(
"Arrow Keys",
"Nudge selected (1px)");
947 shortcut_row(
"G",
"Toggle grid");
948 shortcut_row(
"I",
"Toggle object ID labels");
949 shortcut_row(
"?",
"Show this help");
959 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
963 const ImGuiIO& io = ImGui::GetIO();
966 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
971 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
976 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
981 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
986 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
991 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
996 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
1003 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
1004 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
1011 if (ImGui::IsKeyPressed(ImGuiKey_G) && !io.KeyCtrl) {
1016 if (ImGui::IsKeyPressed(ImGuiKey_I) && !io.KeyCtrl) {
1023 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow))
1025 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow))
1027 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow))
1029 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow))
1032 if (dx != 0 || dy != 0) {
1038 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
1043 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
1053 if (ImGui::IsKeyPressed(ImGuiKey_Slash) && io.KeyShift) {
1072 std::vector<size_t> all_indices;
1074 for (
size_t i = 0; i < objects.size(); ++i) {
1075 all_indices.push_back(i);
1094 if (selected.empty())
1098 if (selected.size() > 5) {
1113 if (selected.empty())
1117 std::vector<size_t> sorted_indices(selected.begin(), selected.end());
1118 std::sort(sorted_indices.rbegin(), sorted_indices.rend());
1120 for (
size_t idx : sorted_indices) {
1124 interaction.ClearSelection();
1134 if (selected.empty())
1137 std::vector<size_t> new_indices;
1139 for (
size_t idx : selected) {
1141 if (new_idx.has_value()) {
1142 new_indices.push_back(*new_idx);
1146 interaction.SetSelectedObjects(new_indices);
1156 if (selected.empty())
1168 if (!new_indices.empty()) {
1180 if (selected.empty())
1183 for (
size_t idx : selected) {
1196 size_t total_objects = objects.size();
1197 if (total_objects == 0)
1200 size_t current_idx = selected.empty() ? 0 : selected.front();
1201 size_t next_idx = (current_idx + direction + total_objects) % total_objects;
1203 interaction.SetSelectedObjects({next_idx});
1212 if (index >= objects.size())
1215 const auto& obj = objects[index];
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
DungeonObjectInteraction & object_interaction()
void ClearPreviewObject()
void SetPreviewObject(const zelda3::RoomObject &object)
void SetObjectInteractionEnabled(bool enabled)
void ScrollToTile(int tile_x, int tile_y)
void SetSelectedObjects(const std::vector< size_t > &indices)
std::vector< size_t > GetSelectedObjectIndices() const
void SetSelectionChangeCallback(std::function< void()> callback)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
size_t GetSelectionCount() const
bool IsObjectLoaded() const
void SetDoorPlacementMode(bool enabled, zelda3::DoorType type=zelda3::DoorType::NormalDoor)
void SetObjectDoubleClickCallback(std::function< void(int)> callback)
zelda3::GameData * game_data() const
std::array< zelda3::Room, 0x128 > * get_rooms()
void SelectObject(int obj_id, int subtype=-1)
void SetStaticEditorObjectId(int obj_id)
void DrawObjectAssetBrowser()
void SetObjectSelectedCallback(std::function< void(const zelda3::RoomObject &)> callback)
TileObjectHandler & tile_handler()
DungeonCanvasViewer * canvas_viewer_
void DrawStaticObjectEditor()
zelda3::RoomObject preview_object_
void SetPlacementError(const std::string &message)
bool show_delete_confirmation_modal_
bool show_emulator_preview_
void SelectObject(int obj_id)
gui::Canvas static_preview_canvas_
ObjectEditorPanel(gfx::IRenderer *renderer, Rom *rom, DungeonCanvasViewer *canvas_viewer, std::shared_ptr< zelda3::DungeonObjectEditor > object_editor=nullptr)
void DeleteSelectedObjects()
void DrawRoomValidationBar()
void OnSelectionChanged()
double placement_error_time_
void DrawObjectSelector()
size_t cached_selection_count_
static constexpr double kPlacementErrorDuration
DungeonObjectSelector object_selector_
std::unique_ptr< zelda3::ObjectParser > object_parser_
void DeselectAllObjects()
void NudgeSelectedObjects(int dx, int dy)
bool static_preview_rendered_
void DrawSelectedObjectInfo()
zelda3::DoorType selected_door_type_
gui::DungeonObjectEmulatorPreview emulator_preview_
void CloseStaticObjectEditor()
gfx::IRenderer * renderer_
bool door_placement_mode_
std::string last_placement_error_
bool selection_callbacks_setup_
void DrawKeyboardShortcutHelp()
void ScrollToObject(size_t index)
gfx::BackgroundBuffer static_preview_buffer_
void OpenStaticObjectEditor(int object_id)
void Draw(bool *p_open) override
Draw the panel content.
TileEditorCallback tile_editor_callback_
std::shared_ptr< zelda3::DungeonObjectEditor > object_editor_
void CycleObjectSelection(int direction)
void DrawEmulatorPreview()
void CopySelectedObjects()
void SetupSelectionCallbacks()
void SetAgentOptimizedLayout(bool enabled)
int static_editor_object_id_
void DuplicateSelectedObjects()
zelda3::ObjectDrawInfo static_editor_draw_info_
void HandleKeyboardShortcuts()
PlacementBlockReason placement_block_reason() const
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
void ProcessTextureQueue(IRenderer *renderer)
void EnsureBitmapInitialized()
Defines an abstract interface for all rendering operations.
void AddTextAt(ImVec2 local_pos, const std::string &text, uint32_t color)
void Initialize(gfx::IRenderer *renderer, Rom *rom, zelda3::GameData *game_data=nullptr, emu::render::EmulatorRenderService *render_service=nullptr)
RAII guard for ImGui style colors.
RAII guard for ImGui style vars.
ValidationResult ValidateRoom(const Room &room)
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< gfx::TileInfo > & tiles() const
#define ICON_MD_GRID_VIEW
#define ICON_MD_CONSTRUCTION
#define ICON_MD_OPEN_WITH
#define ICON_MD_VISIBILITY
#define ICON_MD_DOOR_FRONT
#define ICON_MD_CHECK_CIRCLE
#define ICON_MD_PEST_CONTROL
#define ICON_MD_HELP_OUTLINE
#define ICON_MD_CONTENT_COPY
#define ICON_MD_INVENTORY_2
#define ICON_MD_ADD_CIRCLE
const AgentUITheme & GetTheme()
void EndCanvas(Canvas &canvas)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
bool DangerButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a danger action button (error color).
void HelpMarker(const char *desc)
bool RenderPreviewPanel(const CanvasRuntime &rt, gfx::Bitmap &bmp, const PreviewPanelOpts &opts)
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
@ FancyDungeonExit
Fancy dungeon exit.
@ SmallKeyDoor
Small key door.
@ SmallKeyStairsDown
Small key stairs (downwards)
@ SmallKeyStairsUp
Small key stairs (upwards)
@ DungeonSwapMarker
Dungeon swap marker.
@ NormalDoor
Normal door (upper layer)
@ BombableDoor
Bombable door.
@ LayerSwapMarker
Layer swap marker.
@ ExplodingWall
Exploding wall.
@ TopSidedShutter
Top-sided shutter door.
@ NormalDoorLower
Normal door (lower layer)
@ BottomSidedShutter
Bottom-sided shutter door.
@ CurtainDoor
Curtain door.
@ WaterfallDoor
Waterfall door.
@ BigKeyDoor
Big key door.
@ EyeWatchDoor
Eye watch door.
@ DoubleSidedShutter
Double sided shutter door.
constexpr size_t kMaxTileObjects
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
constexpr size_t kMaxDoors
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
constexpr int kNumberOfRooms
constexpr size_t kMaxChests
constexpr size_t kMaxTotalSprites
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
PaletteGroup dungeon_main
Represents a group of palettes.
std::optional< float > grid_step
gfx::PaletteGroupMap palette_groups