24#include "imgui/imgui.h"
38 if (object_id < 0x100)
40 if (object_id < 0x200)
42 if (object_id >= 0xF80)
50 static const char* kGroupNames[] = {
65 constexpr size_t kCount =
sizeof(kGroupNames) /
sizeof(kGroupNames[0]);
66 return blockset < kCount ? kGroupNames[blockset] :
"Custom";
70 switch (layer_value) {
72 return "Primary (main pass)";
84 static const char* kNames[] = {
85 "Nothing",
"Green Rupee",
"Rock",
"Bee",
86 "Heart (4)",
"Bomb (4)",
"Heart",
"Blue Rupee",
87 "Key",
"Arrow (5)",
"Bomb (1)",
"Heart",
88 "Magic (Small)",
"Full Magic",
"Cucco",
"Green Soldier",
89 "Bush Stal",
"Blue Soldier",
"Landmine",
"Heart",
90 "Fairy",
"Heart",
"Nothing (22)",
"Hole",
91 "Warp",
"Staircase",
"Bombable",
"Switch",
93 constexpr size_t kCount =
sizeof(kNames) /
sizeof(kNames[0]);
94 return item < kCount ? kNames[item] :
"Unknown";
99 return std::clamp(desired_width, min_width, std::max(min_width, max_width));
103 if (!icon || !*icon) {
104 return button_height;
107 const ImGuiStyle& style = ImGui::GetStyle();
108 const float text_w = ImGui::CalcTextSize(icon).x;
109 const float padding = std::max(2.0f, style.FramePadding.x);
110 const float width = std::ceil(text_w + (style.FramePadding.x * 2.0f) + padding);
111 return std::max(button_height, width);
115 bool show_left =
false;
116 bool show_right =
false;
117 bool compact_left =
false;
118 bool compact_right =
false;
122 float total_width,
float min_canvas_width,
float min_sidebar_width,
123 float splitter_width,
bool want_left,
bool want_right) {
128 auto required_width = [&](
bool left,
bool right,
bool compact_left,
129 bool compact_right) {
130 float required = min_canvas_width;
131 required += left ? (compact_left ? std::max(136.0f, min_sidebar_width * 0.72f)
134 required += right ? (compact_right ? std::max(200.0f, min_sidebar_width + 32.0f)
138 required += splitter_width;
141 required += splitter_width;
173 float min_width,
float max_width,
174 bool resize_from_left_edge) {
180 const ImVec2 splitter_pos = ImGui::GetCursorScreenPos();
181 ImGui::InvisibleButton(
id, ImVec2(splitter_width, std::max(height, 1.0f)));
182 const bool hovered = ImGui::IsItemHovered();
183 const bool active = ImGui::IsItemActive();
184 if (hovered || active) {
185 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
187 if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
191 const float delta = ImGui::GetIO().MouseDelta.x;
192 const float proposed =
193 resize_from_left_edge ? (*pane_width - delta) : (*pane_width + delta);
195 ImGui::SetTooltip(
"Width: %.0f px", *pane_width);
199 splitter_color.w = active ? 0.95f : (hovered ? 0.72f : 0.35f);
200 ImGui::GetWindowDrawList()->AddLine(
201 ImVec2(splitter_pos.x + splitter_width * 0.5f, splitter_pos.y),
202 ImVec2(splitter_pos.x + splitter_width * 0.5f, splitter_pos.y + height),
203 ImGui::GetColorU32(splitter_color), active ? 2.0f : 1.0f);
207 ImGui::SeparatorText(label);
212 ImGuiStyleVar_FramePadding,
213 ImVec2(ImGui::GetStyle().FramePadding.x,
214 std::max(5.0f, ImGui::GetStyle().FramePadding.y + 1.0f)));
215 return ImGui::CollapsingHeader(
216 label, default_open ? ImGuiTreeNodeFlags_DefaultOpen : 0);
221 ImVec2(0.08f, 0.5f));
222 return ImGui::Button(label, size);
227 return ImGui::Selectable(label, selected, 0, ImVec2(width, height));
234 std::function<
void(
int)> on_room_selected,
236 std::function<
void(
int)> on_save_room,
239 std::function<
const std::deque<int>&()> get_recent_rooms,
240 std::function<
void(
int)> forget_recent_room,
241 std::function<
void(
const std::string&)> show_panel,
242 std::function<
void(
bool)> set_workflow_mode,
Rom* rom)
243 : room_selector_(room_selector),
244 current_room_id_(current_room_id),
245 on_room_selected_(std::move(on_room_selected)),
246 on_room_selected_with_intent_(std::move(on_room_selected_with_intent)),
247 on_save_room_(std::move(on_save_room)),
248 get_viewer_(std::move(get_viewer)),
249 get_compare_viewer_(std::move(get_compare_viewer)),
250 get_recent_rooms_(std::move(get_recent_rooms)),
251 forget_recent_room_(std::move(forget_recent_room)),
252 show_panel_(std::move(show_panel)),
253 set_workflow_mode_(std::move(set_workflow_mode)),
257 return "dungeon.workbench";
260 return "Dungeon Workbench";
279 float button_size,
bool compact) {
280 const bool sidebar_open =
281 ImGui::BeginChild(
"##DungeonWorkbenchSidebar", ImVec2(width, height),
293 ImGuiStyleVar_FramePadding,
294 ImVec2(ImGui::GetStyle().FramePadding.x,
295 std::max(5.0f, ImGui::GetStyle().FramePadding.y + 1.0f)));
297 ImGuiStyleVar_ItemSpacing,
298 ImVec2(std::max(4.0f, ImGui::GetStyle().ItemSpacing.x * 0.75f),
299 ImGui::GetStyle().ItemSpacing.y));
300 const float segment_height =
303 const bool can_open_overview =
static_cast<bool>(
show_panel_);
304 const float collapse_w =
310 const float spacing = ImGui::GetStyle().ItemSpacing.x;
311 const float header_width = ImGui::GetContentRegionAvail().x;
312 const bool stack_mode_switch = header_width < 240.0f;
313 const float action_cluster_w =
314 collapse_w + (can_open_overview ? (spacing + menu_w) : 0.0f);
316 ImGui::AlignTextToFramePadding();
320 ImGui::SetCursorPosX(std::max(
321 ImGui::GetCursorPosX(),
322 ImGui::GetWindowContentRegionMax().x - action_cluster_w));
324 if (can_open_overview) {
326 ImVec2(menu_w, button_size))) {
327 ImGui::OpenPopup(
"##WorkbenchSidebarQuickActions");
329 if (ImGui::IsItemHovered()) {
330 ImGui::SetTooltip(
"Open room review tools");
332 if (ImGui::BeginPopup(
"##WorkbenchSidebarQuickActions")) {
344 ImVec2(collapse_w, button_size))) {
347 if (ImGui::IsItemHovered()) {
348 ImGui::SetTooltip(
"Collapse navigation pane");
351 ImGui::Dummy(ImVec2(0.0f, 3.0f));
357 float segment_height) {
358 const float width = ImGui::GetContentRegionAvail().x;
359 const float spacing = ImGui::GetStyle().ItemSpacing.x;
360 const float mode_width =
361 stacked ? -1.0f : std::max(92.0f, (width - spacing) * 0.5f);
372 mode_width, segment_height)) {
379 ImGui::TextDisabled(
"Room navigation unavailable");
383 ImGui::PushID(
"WorkbenchSidebarMode");
400 ImGui::TextDisabled(
ICON_MD_INFO " Load a ROM to edit dungeon rooms.");
404 ImGui::TextColored(theme.text_error_red,
"Dungeon Workbench not wired");
412 const float total_w = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
413 const float min_sidebar_w =
415 const float min_canvas_w = std::max(360.0f, min_sidebar_w + 80.0f);
416 const ResponsiveWorkbenchLayout responsive = ResolveResponsiveWorkbenchLayout(
417 total_w, min_canvas_w, min_sidebar_w, splitter_w,
419 const bool show_left = responsive.show_left;
420 const bool show_right = responsive.show_right;
447 const float total_h = std::max(ImGui::GetContentRegionAvail().y, 1.0f);
448 const float compact_left_w = std::max(176.0f, min_sidebar_w * 0.8f);
449 const float compact_right_w = std::max(232.0f, min_sidebar_w + 36.0f);
450 const float active_left_min_w =
451 responsive.compact_left ? compact_left_w : min_sidebar_w;
452 const float active_right_min_w =
453 responsive.compact_right ? compact_right_w : min_sidebar_w;
455 float left_w = show_left
456 ? (responsive.compact_left ? compact_left_w
459 float right_w = show_right
460 ? (responsive.compact_right ? compact_right_w
463 const float max_left_w =
464 total_w - right_w - min_canvas_w - (show_left ? splitter_w : 0.0f) -
465 (show_right ? splitter_w : 0.0f);
466 const float max_right_w =
467 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0.0f) -
468 (show_right ? splitter_w : 0.0f);
471 ClampWorkbenchPaneWidth(left_w, active_left_min_w,
472 std::max(active_left_min_w, max_left_w));
473 if (!responsive.compact_left) {
480 right_w = ClampWorkbenchPaneWidth(right_w, active_right_min_w,
481 std::max(active_right_min_w, max_right_w));
482 if (!responsive.compact_right) {
489 float center_w = total_w - left_w - right_w;
491 center_w -= splitter_w;
494 center_w -= splitter_w;
496 if (center_w < min_canvas_w) {
497 float deficit = min_canvas_w - center_w;
500 std::min(deficit, right_w - active_right_min_w);
502 if (!responsive.compact_right) {
507 if (deficit > 0.0f && show_left) {
509 std::min(deficit, left_w - active_left_min_w);
511 if (!responsive.compact_left) {
516 center_w = std::max(1.0f, total_w - left_w - right_w -
517 (show_left ? splitter_w : 0.0f) -
518 (show_right ? splitter_w : 0.0f));
525 ImGui::SameLine(0.0f, 0.0f);
526 DrawVerticalSplitter(
"##DungeonWorkbenchLeftSplitter", total_h,
528 total_w - right_w - min_canvas_w -
529 (show_right ? splitter_w : 0.0f),
534 ImGui::SameLine(0.0f, 0.0f);
539 ImGui::SameLine(0.0f, 0.0f);
540 DrawVerticalSplitter(
"##DungeonWorkbenchRightSplitter", total_h,
542 total_w - left_w - min_canvas_w -
543 (show_left ? splitter_w : 0.0f),
545 ImGui::SameLine(0.0f, 0.0f);
555 bool left_sidebar_visible) {
556 const bool canvas_open =
557 ImGui::BeginChild(
"##DungeonWorkbenchCanvas", ImVec2(width, height),
560 if (primary_viewer) {
561 const bool show_recent_tabs =
563 if (show_recent_tabs) {
575 status.workflow_mode =
"Workbench";
576 status.workflow_primary =
true;
582 static std::string s_undo_desc;
585 s_undo_desc.empty() ? nullptr : s_undo_desc.c_str();
588 static std::string s_redo_desc;
591 s_redo_desc.empty() ? nullptr : s_redo_desc.c_str();
599 ImGui::TextDisabled(
"No active viewer");
609 const bool inspector_open =
610 ImGui::BeginChild(
"##DungeonWorkbenchInspector", ImVec2(width, height),
612 if (inspector_open) {
617 ImGui::TextDisabled(
"No active viewer");
626 ImGuiStyleVar_FramePadding,
627 ImVec2(ImGui::GetStyle().FramePadding.x,
628 std::max(5.0f, ImGui::GetStyle().FramePadding.y + 1.0f)));
630 ImGuiStyleVar_ItemSpacing,
631 ImVec2(std::max(4.0f, ImGui::GetStyle().ItemSpacing.x * 0.75f),
632 ImGui::GetStyle().ItemSpacing.y));
633 const float segment_height =
635 const float collapse_w =
638 ImGui::AlignTextToFramePadding();
639 ImGui::TextDisabled(
"%s", compact ?
ICON_MD_TUNE " Inspect"
642 ImGui::SetCursorPosX(std::max(
643 ImGui::GetCursorPosX(),
644 ImGui::GetWindowContentRegionMax().x - collapse_w));
646 ImVec2(collapse_w, button_size))) {
649 if (ImGui::IsItemHovered()) {
650 ImGui::SetTooltip(
"Collapse inspector");
653 ImGui::Dummy(ImVec2(0.0f, 3.0f));
664 if (recent.empty()) {
669 std::vector<int> recent_ids(recent.begin(), recent.end());
670 std::vector<int> to_forget;
672 constexpr ImGuiTabBarFlags kFlags = ImGuiTabBarFlags_AutoSelectNewTabs |
673 ImGuiTabBarFlags_FittingPolicyScroll |
674 ImGuiTabBarFlags_TabListPopupButton;
677 const ImVec2 frame_pad = ImGui::GetStyle().FramePadding;
679 const float extra_y = is_touch ? 6.0f : 1.0f;
680 const float extra_x = is_touch ? 4.0f : 0.0f;
682 ImGuiStyleVar_FramePadding,
683 ImVec2(frame_pad.x + extra_x, frame_pad.y + extra_y));
686 for (
int room_id : recent_ids) {
688 const ImGuiTabItemFlags tab_flags =
692 if (room_name.empty() || room_name ==
"Unknown") {
693 snprintf(tab_label,
sizeof(tab_label),
"%03X##recent_%03X", room_id,
696 snprintf(tab_label,
sizeof(tab_label),
"%03X %.12s##recent_%03X",
697 room_id, room_name.c_str(), room_id);
699 const bool selected = ImGui::BeginTabItem(tab_label, &open, tab_flags);
702 to_forget.push_back(room_id);
705 if (ImGui::IsItemHovered()) {
707 ImGui::SetTooltip(
"[%03X] %s", room_id, label.c_str());
714 if (ImGui::BeginPopupContextItem()) {
725 to_forget.push_back(room_id);
739 for (
int rid : to_forget) {
773 constexpr ImGuiTableFlags kSplitFlags =
774 ImGuiTableFlags_Resizable | ImGuiTableFlags_NoPadOuterX |
775 ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_BordersInnerV;
777 if (!ImGui::BeginTable(
"##DungeonWorkbenchSplit", 2, kSplitFlags)) {
782 ImGui::TableSetupColumn(
"Active", ImGuiTableColumnFlags_WidthStretch);
783 ImGui::TableSetupColumn(
"Compare", ImGuiTableColumnFlags_WidthStretch);
784 ImGui::TableNextRow();
787 ImGui::TableNextColumn();
788 ImGui::AlignTextToFramePadding();
795 if (split_active_open) {
801 ImGui::TableNextColumn();
802 ImGui::AlignTextToFramePadding();
809 if (split_compare_open) {
810 if (
auto* compare_viewer =
813 compare_viewer->canvas().ApplyScaleSnapshot(
818 ImGui::TextDisabled(
"No compare viewer");
834 static const char*
const kShortNames[] = {
835 "Sewers",
"HC",
"Eastern",
"Desert",
"A-Tower",
"Swamp",
"PoD",
836 "Misery",
"Skull",
"Ice",
"Hera",
"Thieves",
"Turtle",
"GT",
838 constexpr int kVanillaCount =
839 static_cast<int>(
sizeof(kShortNames) /
sizeof(kShortNames[0]));
841 auto AddRoom = [&](
int room_id,
int dungeon_id) {
846 if (dungeon_id >= 0 && dungeon_id < kVanillaCount) {
850 snprintf(buf,
sizeof(buf),
"Dungeon %02X", dungeon_id);
856 for (
int i = 0; i < 0x84; ++i) {
859 if (did >= 0 && did < kVanillaCount) {
863 snprintf(buf,
sizeof(buf),
"Dungeon %02X", did);
869 for (
int i = 0; i < 0x14; ++i) {
877 ImGuiStyleVar_ItemSpacing,
878 ImVec2(std::max(6.0f, ImGui::GetStyle().ItemSpacing.x * 0.9f),
879 std::max(6.0f, ImGui::GetStyle().ItemSpacing.y * 0.95f)));
884 float segment_height) {
885 const float width = ImGui::GetContentRegionAvail().x;
886 const bool stack = width < 260.0f;
887 const float button_width =
888 stack ? -1.0f : (width - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
891 button_width, segment_height)) {
899 button_width, segment_height)) {
911 const bool has_entity = interaction.HasEntitySelection();
917 ImGui::TextDisabled(
"No room selected");
921 if (selected_objects > 0 || has_entity) {
924 ImGui::BulletText(
"Entity selected");
926 if (selected_objects > 0) {
927 ImGui::BulletText(
"%zu object%s selected", selected_objects,
928 selected_objects == 1 ?
"" :
"s");
935 ImGui::TextDisabled(
"Nothing selected");
941 DrawWorkbenchActionButton(
ICON_MD_SAVE " Save Room", ImVec2(-1, 0))) {
961 if (ImGui::GetContentRegionAvail().x < 240.0f) {
977 if (BeginWorkbenchInspectorSection(
ICON_MD_BUILD " Quick Tools",
false)) {
991 const std::string room_label =
995 DrawWorkbenchInspectorSectionHeader(
ICON_MD_CASTLE " Room Summary");
997 ImGui::Text(
"Room: 0x%03X (%d)", room_id, room_id);
1001 snprintf(buf,
sizeof(buf),
"0x%03X", room_id);
1002 ImGui::SetClipboardText(buf);
1004 if (ImGui::IsItemHovered()) {
1005 ImGui::SetTooltip(
"Copy room ID (0x%03X) to clipboard", room_id);
1008 ImGui::TextUnformatted(
"Room: None");
1017 const char* group_name =
nullptr;
1021 group_name = cache_it->second.c_str();
1025 auto* rooms = viewer.
rooms();
1026 if (rooms && room_id <
static_cast<int>(rooms->size())) {
1027 group_name = GetBlocksetGroupName((*rooms)[room_id].blockset());
1032 room_label.c_str());
1034 ImGui::TextDisabled(
"%s", room_label.c_str());
1037 ImGui::TextDisabled(
"%s", room_label.c_str());
1041 DrawWorkbenchInspectorSectionHeader(
ICON_MD_SAVE " Room Actions");
1043 if (DrawWorkbenchActionButton(
ICON_MD_SAVE " Save Room", ImVec2(-1, 0))) {
1050 constexpr ImGuiTableFlags kPanelFlags =
1051 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1052 if (ImGui::BeginTable(
"##WorkbenchRoomPanels", 2, kPanelFlags)) {
1053 ImGui::TableNextRow();
1054 ImGui::TableNextColumn();
1059 ImGui::TableNextColumn();
1069 DrawWorkbenchInspectorSectionHeader(
ICON_MD_TUNE " Room Properties");
1070 if (
auto* rooms = viewer.
rooms();
1071 rooms && room_id >= 0 && room_id < static_cast<int>(rooms->size())) {
1072 auto& room = (*rooms)[room_id];
1074 uint8_t blockset_val = room.blockset();
1075 uint8_t palette_val = room.palette();
1076 uint8_t layout_val = room.layout_id();
1077 uint8_t spriteset_val = room.spriteset();
1079 constexpr float kHexW = 92.0f;
1081 constexpr ImGuiTableFlags kPropsFlags = ImGuiTableFlags_BordersInnerV |
1082 ImGuiTableFlags_RowBg |
1083 ImGuiTableFlags_NoPadOuterX;
1084 if (ImGui::BeginTable(
"##WorkbenchRoomProps", 2, kPropsFlags)) {
1085 ImGui::TableSetupColumn(
"Prop", ImGuiTableColumnFlags_WidthFixed, 90.0f);
1086 ImGui::TableSetupColumn(
"Val", ImGuiTableColumnFlags_WidthStretch);
1092 room.SetBlockset(blockset_val);
1093 if (room.rom() && room.rom()->is_loaded()) {
1094 room.RenderRoomGraphics();
1097 if (ImGui::IsItemHovered()) {
1098 ImGui::SetTooltip(
"Blockset (0-51)");
1105 room.SetPalette(palette_val);
1106 if (room.rom() && room.rom()->is_loaded()) {
1107 room.RenderRoomGraphics();
1114 if (ImGui::IsItemHovered()) {
1115 ImGui::SetTooltip(
"Palette (0-47)");
1122 room.SetLayoutId(layout_val);
1123 room.MarkLayoutDirty();
1124 if (room.rom() && room.rom()->is_loaded()) {
1125 room.RenderRoomGraphics();
1128 if (ImGui::IsItemHovered()) {
1129 ImGui::SetTooltip(
"Layout (0-7)");
1136 room.SetSpriteset(spriteset_val);
1137 if (room.rom() && room.rom()->is_loaded()) {
1138 room.RenderRoomGraphics();
1141 if (ImGui::IsItemHovered()) {
1142 ImGui::SetTooltip(
"Spriteset (0-8F)");
1149 ImGui::TextDisabled(
"Room properties unavailable");
1152 DrawWorkbenchInspectorSectionHeader(
ICON_MD_BUILD " Editing Status");
1156 ImGui::TextColored(theme.text_info,
"Placement active");
1159 interaction.mode_manager().CancelCurrentMode();
1170 const size_t obj_count = interaction.GetSelectionCount();
1171 const bool has_entity = interaction.HasEntitySelection();
1173 if (!has_entity && obj_count == 0) {
1174 DrawWorkbenchInspectorSectionHeader(
ICON_MD_INFO " Selection");
1175 ImGui::TextDisabled(
"Click an object or entity to inspect");
1177 DrawWorkbenchInspectorSectionHeader(
ICON_MD_BUILD " Jump Into Editing");
1178 constexpr ImGuiTableFlags kEmptyStateFlags =
1179 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1180 if (ImGui::BeginTable(
"##SelectionEmptyStateActions", 2,
1181 kEmptyStateFlags)) {
1182 ImGui::TableNextRow();
1183 ImGui::TableNextColumn();
1188 ImGui::TableNextColumn();
1200 if (obj_count > 0) {
1201 DrawWorkbenchInspectorSectionHeader(
ICON_MD_WIDGETS " Object Selection");
1202 ImGui::TextColored(theme.text_primary,
ICON_MD_WIDGETS " %zu object%s",
1203 obj_count, obj_count == 1 ?
"" :
"s");
1204 if (obj_count == 1) {
1206 ImGui::TextDisabled(
"Focused selection");
1209 constexpr ImGuiTableFlags kActionFlags =
1210 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1211 if (ImGui::BeginTable(
"##SelectionObjectActions", 3, kActionFlags)) {
1212 ImGui::TableNextRow();
1213 ImGui::TableNextColumn();
1218 ImGui::TableNextColumn();
1219 if (DrawWorkbenchActionButton(
ICON_MD_CLEAR " Clear", ImVec2(-1, 0))) {
1220 interaction.ClearSelection();
1222 ImGui::TableNextColumn();
1224 if (DrawWorkbenchActionButton(
ICON_MD_TUNE " Open Editor",
1229 ImGui::BeginDisabled();
1232 ImGui::EndDisabled();
1237 const auto indices = interaction.GetSelectedObjectIndices();
1240 if (indices.size() > 1 && room_id >= 0 && viewer.
rooms()) {
1241 auto& room = (*viewer.
rooms())[room_id];
1242 auto& objects = room.GetTileObjects();
1244 for (
size_t i = 0; i < indices.size() && i < 8; ++i) {
1245 size_t idx = indices[i];
1246 if (idx < objects.size()) {
1247 auto& obj = objects[idx];
1249 ImGui::BulletText(
"0x%03X %s", obj.id_, name.c_str());
1252 if (indices.size() > 8) {
1253 ImGui::TextDisabled(
" ... and %zu more", indices.size() - 8);
1258 if (indices.size() == 1 && room_id >= 0 && viewer.
rooms()) {
1259 auto& room = (*viewer.
rooms())[room_id];
1260 auto& objects = room.GetTileObjects();
1261 const size_t idx = indices.front();
1262 if (idx < objects.size()) {
1263 auto& obj = objects[idx];
1269 ImGui::TextColored(theme.text_primary,
"%s", obj_name.c_str());
1270 ImGui::TextDisabled(
"%s (Type %d) #%zu in list",
1271 GetObjectCategory(obj.id_), subtype, idx);
1274 constexpr ImGuiTableFlags kPropsFlags = ImGuiTableFlags_BordersInnerV |
1275 ImGuiTableFlags_RowBg |
1276 ImGuiTableFlags_NoPadOuterX;
1277 if (ImGui::BeginTable(
"##SelObjProps", 2, kPropsFlags)) {
1278 ImGui::TableSetupColumn(
"Prop", ImGuiTableColumnFlags_WidthFixed,
1280 ImGui::TableSetupColumn(
"Val", ImGuiTableColumnFlags_WidthStretch);
1284 uint16_t obj_id =
static_cast<uint16_t
>(obj.id_ & 0x0FFF);
1289 interaction.SetObjectId(idx,
static_cast<int16_t
>(obj_id));
1297 ImGui::SetNextItemWidth(60);
1299 ImGui::DragInt(
"##SelObjX", &pos_x, 0.1f, 0, 63,
"X:%d");
1301 ImGui::SetNextItemWidth(60);
1303 ImGui::DragInt(
"##SelObjY", &pos_y, 0.1f, 0, 63,
"Y:%d");
1304 if (x_changed || y_changed) {
1305 int delta_x = pos_x - obj.x_;
1306 int delta_y = pos_y - obj.y_;
1307 interaction.entity_coordinator().tile_handler().MoveObjects(
1308 room_id, {idx}, delta_x, delta_y);
1314 uint8_t size = obj.size_ & 0x0F;
1318 interaction.SetObjectSize(idx, size);
1324 int layer =
static_cast<int>(obj.GetLayerValue());
1325 const char* layer_names[] = {
"Primary (main pass)",
"BG2 overlay",
1327 ImGui::SetNextItemWidth(-1);
1328 if (ImGui::Combo(
"##SelObjLayer", &layer, layer_names,
1329 IM_ARRAYSIZE(layer_names))) {
1330 layer = std::clamp(layer, 0, 2);
1331 interaction.SetObjectLayer(
1336 ImGui::TextDisabled(
"%s", GetObjectStreamName(obj.GetLayerValue()));
1341 ImGui::TextDisabled(
"(%d, %d)", obj.x_ * 8, obj.y_ * 8);
1351 if (has_entity && room_id >= 0 && viewer.
rooms()) {
1352 const auto sel = interaction.GetSelectedEntity();
1353 auto& room = (*viewer.
rooms())[room_id];
1356 const char* entity_panel_id =
nullptr;
1357 const char* entity_action_label =
nullptr;
1361 entity_panel_id =
"dungeon.entrance_properties";
1363 const auto& doors = room.GetDoors();
1364 if (sel.index < doors.size()) {
1365 const auto& door = doors[sel.index];
1371 ImGui::TextDisabled(
"Direction: %s Position: 0x%02X",
1372 dir_name.c_str(), door.position);
1374 auto [tile_x, tile_y] = door.GetTileCoords();
1375 auto [pixel_x, pixel_y] = door.GetPixelCoords();
1376 ImGui::TextDisabled(
"Tile: (%d, %d) Pixel: (%d, %d)", tile_x, tile_y,
1382 entity_panel_id =
"dungeon.sprite_editor";
1384 const auto& sprites = room.GetSprites();
1385 if (sel.index < sprites.size()) {
1386 const auto& sprite = sprites[sel.index];
1390 sprite_name.c_str());
1391 ImGui::TextDisabled(
"ID: 0x%02X Subtype: %d Layer: %d", sprite.id(),
1392 sprite.subtype(), sprite.layer());
1393 ImGui::TextDisabled(
"Pos: (%d, %d) Pixel: (%d, %d)", sprite.x(),
1394 sprite.y(), sprite.x() * 16, sprite.y() * 16);
1397 if (sprite.subtype() == 0x07 && sprite.id() >= 0x01 &&
1398 sprite.id() <= 0x1A) {
1400 ImGui::TextColored(theme.text_warning_yellow,
1402 overlord_name.c_str());
1408 entity_panel_id =
"dungeon.item_editor";
1410 const auto& items = room.GetPotItems();
1411 if (sel.index < items.size()) {
1412 const auto& pot_item = items[sel.index];
1413 const char* item_name = GetPotItemName(pot_item.item);
1417 ImGui::TextDisabled(
"Item ID: 0x%02X Raw Pos: 0x%04X", pot_item.item,
1419 ImGui::TextDisabled(
"Pixel: (%d, %d) Tile: (%d, %d)",
1420 pot_item.GetPixelX(), pot_item.GetPixelY(),
1421 pot_item.GetTileX(), pot_item.GetTileY());
1429 if (ImGui::BeginTable(
1430 "##EntityActions", 2,
1431 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
1432 ImGui::TableNextRow();
1433 ImGui::TableNextColumn();
1436 interaction.entity_coordinator().DeleteSelectedEntity();
1437 interaction.ClearEntitySelection();
1439 ImGui::TableNextColumn();
1440 if (
show_panel_ && entity_panel_id && entity_action_label) {
1441 if (DrawWorkbenchActionButton(entity_action_label, ImVec2(-1, 0))) {
1445 ImGui::BeginDisabled();
1448 ImGui::EndDisabled();
1459 if (ImGui::Checkbox(
"Grid (8x8)", &val))
1463 if (ImGui::Checkbox(
"Object Bounds", &val)) {
1468 if (ImGui::Checkbox(
"Hover Coordinates", &val)) {
1473 if (ImGui::Checkbox(
"Camera Quadrants", &val)) {
1478 if (ImGui::Checkbox(
"Track Collision", &val)) {
1483 if (ImGui::Checkbox(
"Custom Collision", &val)) {
1488 if (ImGui::Checkbox(
"Water Fill (Oracle)", &val)) {
1493 if (ImGui::Checkbox(
"Minecart Pathing", &val)) {
1498 if (ImGui::Checkbox(
"Track Gaps", &val)) {
1503 if (ImGui::Checkbox(
"Track Routes", &val)) {
1508 if (ImGui::Checkbox(
"Custom Objects (Oracle)", &val)) {
1511 if (ImGui::IsItemHovered()) {
1513 "Highlight custom-draw objects (IDs 0x31/0x32)\n"
1514 "with a cyan overlay showing position and subtype.");
1521 ImGui::TextDisabled(
"No panel launcher available");
1526 constexpr ImGuiTableFlags kFlags =
1527 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1528 if (!ImGui::BeginTable(
"##WorkbenchToolsGrid", 2, kFlags)) {
1532 ImGui::TableNextRow();
1533 ImGui::TableNextColumn();
1534 if (DrawWorkbenchActionButton(
ICON_MD_CATEGORY " Selector", ImVec2(-1, 0))) {
1537 ImGui::TableNextColumn();
1538 if (DrawWorkbenchActionButton(
ICON_MD_TUNE " Object Editor", ImVec2(-1, 0))) {
1542 ImGui::TableNextRow();
1543 ImGui::TableNextColumn();
1544 if (DrawWorkbenchActionButton(
ICON_MD_PERSON " Sprites", ImVec2(-1, 0))) {
1547 ImGui::TableNextColumn();
1552 ImGui::TableNextRow();
1553 ImGui::TableNextColumn();
1558 ImGui::TableNextColumn();
1559 ImGui::Dummy(ImVec2(0.0f, 0.0f));
1564 if (ImGui::BeginTable(
"##WorkbenchReviewGrid", 2, kFlags)) {
1565 ImGui::TableNextRow();
1566 ImGui::TableNextColumn();
1571 ImGui::TableNextColumn();
1572 if (DrawWorkbenchActionButton(
ICON_MD_MAP " Dungeon Map",
1577 ImGui::TableNextRow();
1578 ImGui::TableNextColumn();
1583 ImGui::TableNextColumn();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
bool show_object_bounds() const
DungeonRoomStore * rooms() const
void set_show_water_fill_overlay(bool show)
void set_show_custom_collision_overlay(bool show)
int current_room_id() const
void set_show_track_route_overlay(bool show)
bool show_track_gap_overlay() const
void set_show_custom_objects_overlay(bool show)
bool show_custom_objects_overlay() const
void set_show_track_collision_overlay(bool show)
DungeonObjectInteraction & object_interaction()
void set_show_minecart_sprite_overlay(bool show)
bool show_coordinate_overlay() const
bool show_track_collision_overlay() const
bool show_minecart_sprite_overlay() const
bool show_water_fill_overlay() const
bool show_camera_quadrant_overlay() const
void set_show_object_bounds(bool show)
void set_show_coordinate_overlay(bool show)
bool show_track_route_overlay() const
void set_show_track_gap_overlay(bool show)
void DrawDungeonCanvas(int room_id)
void set_show_camera_quadrant_overlay(bool show)
void DeleteSelectedObjects()
bool show_custom_collision_overlay() const
void set_show_grid(bool show)
InteractionModeManager & mode_manager()
size_t GetSelectionCount() const
Handles room and entrance selection UI.
void DrawRoomBrowser(RoomSelectionIntent single_click_intent=RoomSelectionIntent::kFocusInWorkbench)
void DrawEntranceBrowser()
static void Draw(const DungeonStatusBarState &state)
static DungeonStatusBarState BuildState(const DungeonCanvasViewer &viewer, const char *tool_mode, bool room_dirty)
bool room_dungeon_cache_built_
void DrawInspectorShelfSelection(DungeonCanvasViewer &viewer)
std::function< void(int)> on_room_selected_
std::function< bool()> can_redo_
int GetPriority() const override
Get display priority for menu ordering.
std::function< DungeonCanvasViewer *()> get_compare_viewer_
SidebarMode sidebar_mode_
void DrawInspectorHeader(float button_size, bool compact)
void DrawInspectorCompactSummary(DungeonCanvasViewer &viewer)
void DrawSidebarModeTabs(bool stacked, float segment_height)
std::function< void()> on_redo_
void DrawInspectorPrimarySelector(float segment_height)
void DrawRecentRoomTabs()
void DrawSidebarPane(float width, float height, float button_size, bool compact)
void DrawSidebarContent()
void DrawInspectorShelf(DungeonCanvasViewer &viewer)
void DrawInspectorPane(float width, float height, float button_size, bool compact, DungeonCanvasViewer *viewer)
void DrawInspectorShelfRoom(DungeonCanvasViewer &viewer)
void DrawInspectorShelfView(DungeonCanvasViewer &viewer)
std::function< void(const std::string &) show_panel_)
std::function< int()> undo_depth_
std::string GetEditorCategory() const override
Editor category this panel belongs to.
std::function< std::string()> undo_desc_
void DrawInspectorShelfTools(DungeonCanvasViewer &viewer)
void DrawCanvasPane(float width, float height, DungeonCanvasViewer *primary_viewer, bool left_sidebar_visible)
std::function< bool()> can_undo_
DungeonRoomSelector * room_selector_
void DrawSidebarHeader(float button_size, bool compact)
std::function< void()> on_undo_
DungeonWorkbenchContent(DungeonRoomSelector *room_selector, int *current_room_id, std::function< void(int)> on_room_selected, std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent, std::function< void(int)> on_save_room, std::function< DungeonCanvasViewer *()> get_viewer, std::function< DungeonCanvasViewer *()> get_compare_viewer, std::function< const std::deque< int > &()> get_recent_rooms, std::function< void(int)> forget_recent_room, std::function< void(const std::string &)> show_panel, std::function< void(bool)> set_workflow_mode, Rom *rom=nullptr)
std::function< std::string()> redo_desc_
char compare_search_buf_[64]
std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent_
std::function< void(int)> on_save_room_
std::string GetId() const override
Unique identifier for this panel.
void DrawSplitView(DungeonCanvasViewer &primary_viewer)
std::function< void(bool)> set_workflow_mode_
std::string GetIcon() const override
Material Design icon for this panel.
std::unordered_map< int, std::string > room_dungeon_cache_
InspectorFocus inspector_focus_
std::function< const char *()> get_tool_mode_
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
void Draw(bool *p_open) override
Draw the panel content.
DungeonWorkbenchLayoutState layout_state_
bool show_shortcut_legend_
std::function< void(int)> forget_recent_room_
std::function< const std::deque< int > &()> get_recent_rooms_
void DrawInspector(DungeonCanvasViewer &viewer)
void BuildRoomDungeonCache()
std::function< DungeonCanvasViewer *()> get_viewer_
bool IsPlacementActive() const
Check if any placement mode is active.
static void Draw(bool *p_open)
CanvasConfig & GetConfig()
static float GetTouchSafeWidgetHeight()
static bool BeginContentChild(const char *id, const ImVec2 &min_size, bool border=false, ImGuiWindowFlags flags=0)
static void EndContentChild()
static void PropertyRow(const char *label, std::function< void()> widget_callback)
static bool IsTouchDevice()
RAII guard for ImGui style vars.
Dungeon Room Entrance or Spawn Point.
#define ICON_MD_GRID_VIEW
#define ICON_MD_SUMMARIZE
#define ICON_MD_COMPARE_ARROWS
#define ICON_MD_OPEN_IN_FULL
#define ICON_MD_VISIBILITY
#define ICON_MD_MORE_HORIZ
#define ICON_MD_TRAVEL_EXPLORE
#define ICON_MD_INVENTORY
#define ICON_MD_DOOR_FRONT
#define ICON_MD_CHEVRON_LEFT
#define ICON_MD_SELECT_ALL
#define ICON_MD_EDIT_NOTE
#define ICON_MD_OPEN_IN_NEW
#define ICON_MD_CONTENT_COPY
#define ICON_MD_INVENTORY_2
#define ICON_MD_VIEW_SIDEBAR
#define ICON_MD_CHEVRON_RIGHT
#define ICON_MD_WORKSPACES
#define ICON_MD_CROP_FREE
const AgentUITheme & GetTheme()
const char * GetObjectStreamName(int layer_value)
ResponsiveWorkbenchLayout ResolveResponsiveWorkbenchLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, bool want_left, bool want_right)
bool DrawWorkbenchActionButton(const char *label, const ImVec2 &size)
const char * GetPotItemName(uint8_t item)
const char * GetBlocksetGroupName(uint8_t blockset)
void DrawWorkbenchInspectorSectionHeader(const char *label)
float CalcWorkbenchIconButtonWidth(const char *icon, float button_height)
void DrawVerticalSplitter(const char *id, float height, float *pane_width, float min_width, float max_width, bool resize_from_left_edge)
bool BeginWorkbenchInspectorSection(const char *label, bool default_open)
bool DrawWorkbenchSegment(const char *label, bool selected, float width, float height)
float ClampWorkbenchPaneWidth(float desired_width, float min_width, float max_width)
const char * GetObjectCategory(int object_id)
Editors are the view controllers for the application.
RoomSelectionIntent
Intent for room selection in the dungeon editor.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
InputHexResult InputHexByteEx(const char *label, uint8_t *data, float input_width, bool no_step)
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
std::string GetSpriteLabel(int id)
Convenience function to get a sprite label.
std::string GetRoomLabel(int id)
Convenience function to get a room label.
int GetObjectSubtype(int object_id)
std::string GetOverlordLabel(int id)
Convenience function to get an overlord label.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
bool show_right_inspector
static constexpr float kContentMinHeightCanvas
static constexpr float kSplitterWidth
static constexpr float kContentMinWidthSidebar