10#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
33 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor)
34 : renderer_(renderer),
36 canvas_viewer_(canvas_viewer),
37 object_selector_(rom),
38 object_editor_(object_editor) {
90 auto indices = interaction.GetSelectedObjectIndices();
95 for (
size_t idx : indices) {
106 ImGuiTreeNodeFlags_DefaultOpen)) {
113 float available_height = ImGui::GetContentRegionAvail().y;
115 float reserved_height = 60.0f;
118 reserved_height += 200.0f;
120 float browser_height = std::max(150.0f, available_height - reserved_height);
122 ImGui::BeginChild(
"ObjectBrowserRegion", ImVec2(0, browser_height),
true);
142 ImGui::TextColored(theme.status_warning,
152 " Selection Mode - Click to select, drag to multi-select");
153 ImGui::TextColored(theme.text_secondary_gray,
ICON_MD_MENU
154 " Right-click the canvas for Cut/Copy/Paste options");
164 bool preview_open = ImGui::CollapsingHeader(
ICON_MD_MONITOR " Preview");
168 ImGui::PushID(
"PreviewSection");
195 static constexpr std::array<zelda3::DoorType, 20> kDoorTypes = {{
221 theme.status_warning,
237 constexpr float kPreviewSize = 32.0f;
238 constexpr int kItemsPerRow = 5;
239 float panel_width = ImGui::GetContentRegionAvail().x;
241 std::max(1,
static_cast<int>(panel_width / (kPreviewSize + 8)));
243 ImGui::BeginChild(
"##DoorTypeGrid", ImVec2(0, 80),
true,
244 ImGuiWindowFlags_HorizontalScrollbar);
247 for (
size_t i = 0; i < kDoorTypes.size(); ++i) {
248 auto door_type = kDoorTypes[i];
251 ImGui::PushID(
static_cast<int>(i));
256 int type_val =
static_cast<int>(door_type);
257 if (type_val <= 0x12) {
258 button_color = ImVec4(0.3f, 0.5f, 0.7f, 1.0f);
259 }
else if (type_val <= 0x1E) {
260 button_color = ImVec4(0.7f, 0.5f, 0.3f, 1.0f);
262 button_color = ImVec4(0.5f, 0.7f, 0.3f, 1.0f);
266 button_color.x += 0.2f;
267 button_color.y += 0.2f;
268 button_color.z += 0.2f;
271 ImGui::PushStyleColor(ImGuiCol_Button, button_color);
272 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
273 ImVec4(button_color.x + 0.1f, button_color.y + 0.1f,
274 button_color.z + 0.1f, 1.0f));
275 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
276 ImVec4(button_color.x + 0.2f, button_color.y + 0.2f,
277 button_color.z + 0.2f, 1.0f));
280 std::string label = absl::StrFormat(
"%02X", type_val);
281 if (ImGui::Button(label.c_str(), ImVec2(kPreviewSize, kPreviewSize))) {
290 ImGui::PopStyleColor(3);
293 if (ImGui::IsItemHovered()) {
294 ImGui::SetTooltip(
"%s (0x%02X)\nClick to select for placement",
301 ImVec2 min = ImGui::GetItemRectMin();
302 ImVec2 max = ImGui::GetItemRectMax();
303 ImGui::GetWindowDrawList()->AddRect(min, max, IM_COL32(255, 255, 0, 255),
310 if (col < items_per_row && i < kDoorTypes.size() - 1) {
323 const auto& doors = room.GetDoors();
325 if (!doors.empty()) {
326 ImGui::Text(
ICON_MD_LIST " Room Doors (%zu):", doors.size());
328 ImGui::BeginChild(
"##DoorList", ImVec2(0, 80),
true);
329 for (
size_t i = 0; i < doors.size(); ++i) {
330 const auto& door = doors[i];
331 auto [tile_x, tile_y] = door.GetTileCoords();
333 ImGui::PushID(
static_cast<int>(i));
338 ImGui::Text(
"[%zu] %s (%s)", i, type_name.c_str(), dir_name.c_str());
340 ImGui::TextColored(theme.text_secondary_gray,
"@ (%d,%d)", tile_x,
346 mutable_room.RemoveDoor(i);
353 ImGui::TextColored(theme.text_secondary_gray,
362 ImGui::TextColored(theme.text_secondary_gray,
365 "Uses SNES emulation to render objects accurately.\n"
366 "May impact performance.");
370 ImGui::BeginChild(
"##EmulatorPreviewRegion", ImVec2(0, 260),
true);
383 if (!selected.empty()) {
384 ImGui::TextColored(theme.status_success,
388 if (selected.size() == 1) {
391 if (selected[0] < objects.size()) {
392 const auto& obj = objects[selected[0]];
393 ImGui::Text(
"Object #%zu (ID: 0x%02X)", selected[0], obj.id_);
394 ImGui::TextColored(theme.text_secondary_gray,
395 " Position: (%d, %d) Size: 0x%02X Layer: %s",
396 obj.x_, obj.y_, obj.size_,
402 ImGui::Text(
"1 object");
405 ImGui::Text(
"%zu objects", selected.size());
407 ImGui::TextColored(theme.text_secondary_gray,
408 "(Shift+click to add, Ctrl+click to toggle)");
416 ImGui::TextColored(theme.text_info,
ICON_MD_INFO " Current:");
422 ImGui::Text(
"Layer: %s",
461 if (!room.IsLoaded()) {
462 room.LoadRoomGraphics(room.blockset);
475 if (preview_obj.
tiles().empty()) {
480 const uint8_t* gfx_data = room.get_gfx_buffer().data();
486 if (game_data && !game_data->palette_groups.dungeon_main.empty()) {
488 palette_group = game_data->palette_groups.dungeon_main;
490 std::vector<SDL_Color> colors(256);
491 size_t color_index = 0;
492 for (
size_t pal_idx = 0;
493 pal_idx < palette_group.
size() && color_index < 256; ++pal_idx) {
494 const auto& pal = palette_group[pal_idx];
495 for (
size_t i = 0; i < pal.size() && color_index < 256; ++i) {
496 ImVec4 rgb = pal[i].rgb();
497 colors[color_index++] = {
static_cast<Uint8
>(rgb.x),
498 static_cast<Uint8
>(rgb.y),
499 static_cast<Uint8
>(rgb.z), 255};
502 colors[255] = {0, 0, 0, 0};
503 bitmap.SetPalette(colors);
504 if (bitmap.surface()) {
505 SDL_SetColorKey(bitmap.surface(), SDL_TRUE, 255);
506 SDL_SetSurfaceBlendMode(bitmap.surface(), SDL_BLENDMODE_BLEND);
518 if (bitmap.modified() && bitmap.surface() &&
519 bitmap.mutable_data().size() > 0) {
520 SDL_LockSurface(bitmap.surface());
521 size_t surface_size = bitmap.surface()->h * bitmap.surface()->pitch;
522 size_t data_size = bitmap.mutable_data().size();
523 if (surface_size >= data_size) {
524 memcpy(bitmap.surface()->pixels, bitmap.mutable_data().data(),
527 SDL_UnlockSurface(bitmap.surface());
551 ImGui::PushStyleColor(
552 ImGuiCol_Header, ImVec4(0.15f, 0.25f, 0.35f, 1.0f));
553 ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
554 ImVec4(0.20f, 0.30f, 0.40f, 1.0f));
556 bool header_open = ImGui::CollapsingHeader(
561 ImGuiTreeNodeFlags_DefaultOpen);
563 ImGui::PopStyleColor(2);
566 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 6));
569 if (ImGui::BeginTable(
"StaticEditorLayout", 2,
570 ImGuiTableFlags_BordersInnerV)) {
571 ImGui::TableSetupColumn(
"Info", ImGuiTableColumnFlags_WidthFixed, 200);
572 ImGui::TableSetupColumn(
"Preview", ImGuiTableColumnFlags_WidthStretch);
574 ImGui::TableNextRow();
577 ImGui::TableNextColumn();
580 ImGui::TextColored(theme.text_info,
ICON_MD_TAG " Object ID");
588 ImGui::TextColored(theme.text_info,
ICON_MD_BRUSH " Draw Routine");
600 ImGui::Text(
"Orientation: %s",
605 ImGui::TextColored(theme.status_warning,
ICON_MD_LAYERS " Both BG");
615 ImGui::SetClipboardText(
618 if (ImGui::IsItemHovered()) {
619 ImGui::SetTooltip(
"Copy object ID to clipboard");
622 if (ImGui::Button(
ICON_MD_CODE " Export ASM", ImVec2(-1, 0))) {
625 if (ImGui::IsItemHovered()) {
626 ImGui::SetTooltip(
"Export object draw routine as ASM (Phase 5)");
632 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f));
633 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
634 ImVec4(0.6f, 0.3f, 0.3f, 1.0f));
638 ImGui::PopStyleColor(2);
642 ImGui::TableNextColumn();
644 ImGui::TextColored(theme.text_secondary_gray,
"Preview:");
671 ImVec2(24, 56),
"No preview available",
672 ImGui::GetColorU32(theme.text_secondary_gray));
678 ImGui::TextColored(theme.text_secondary_gray,
ICON_MD_INFO
679 " Double-click objects in browser\n"
680 "to view their draw routine info.");
686 ImGui::PopStyleVar();
695 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
699 const ImGuiIO& io = ImGui::GetIO();
702 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
707 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
712 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
717 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
722 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
727 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
732 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
739 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
740 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
747 if (ImGui::IsKeyPressed(ImGuiKey_G) && !io.KeyCtrl) {
752 if (ImGui::IsKeyPressed(ImGuiKey_I) && !io.KeyCtrl) {
759 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow))
761 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow))
763 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow))
765 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow))
768 if (dx != 0 || dy != 0) {
774 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
779 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
803 std::vector<size_t> all_indices;
805 for (
size_t i = 0; i < objects.size(); ++i) {
806 all_indices.push_back(i);
825 if (selected.empty())
829 if (selected.size() > 5) {
844 if (selected.empty())
848 std::vector<size_t> sorted_indices(selected.begin(), selected.end());
849 std::sort(sorted_indices.rbegin(), sorted_indices.rend());
851 for (
size_t idx : sorted_indices) {
855 interaction.ClearSelection();
865 if (selected.empty())
868 std::vector<size_t> new_indices;
870 for (
size_t idx : selected) {
872 if (new_idx.has_value()) {
873 new_indices.push_back(*new_idx);
877 interaction.SetSelectedObjects(new_indices);
887 if (selected.empty())
899 if (!new_indices.empty()) {
911 if (selected.empty())
914 for (
size_t idx : selected) {
927 size_t total_objects = objects.size();
928 if (total_objects == 0)
931 size_t current_idx = selected.empty() ? 0 : selected.front();
932 size_t next_idx = (current_idx + direction + total_objects) % total_objects;
934 interaction.SetSelectedObjects({next_idx});
942 if (index >= objects.size())
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 SetSelectedObjects(const std::vector< size_t > &indices)
std::vector< size_t > GetSelectedObjectIndices() const
void SetSelectionChangeCallback(std::function< void()> callback)
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
void SelectObject(int obj_id)
std::array< zelda3::Room, 0x128 > * get_rooms()
void SetStaticEditorObjectId(int obj_id)
void DrawObjectAssetBrowser()
void SetObjectSelectedCallback(std::function< void(const zelda3::RoomObject &)> callback)
DungeonCanvasViewer * canvas_viewer_
void DrawStaticObjectEditor()
zelda3::RoomObject preview_object_
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 OnSelectionChanged()
void DrawObjectSelector()
size_t cached_selection_count_
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_
bool selection_callbacks_setup_
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.
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()
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)
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_DOOR_FRONT
#define ICON_MD_CHECK_CIRCLE
#define ICON_MD_CONTENT_COPY
#define ICON_MD_ADD_CIRCLE
const AgentUITheme & GetTheme()
void EndCanvas(Canvas &canvas)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
void HelpMarker(const char *desc)
bool RenderPreviewPanel(const CanvasRuntime &rt, gfx::Bitmap &bmp, const PreviewPanelOpts &opts)
@ 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 std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Represents a group of palettes.
std::optional< float > grid_step