yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_workbench_content.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <cmath>
6#include <cstdint>
7#include <cstdio>
8#include <cstring>
9#include <utility>
10#include <vector>
11
18#include "app/gui/core/icons.h"
19#include "app/gui/core/input.h"
24#include "imgui/imgui.h"
25#include "rom/rom.h"
31
32namespace yaze::editor {
33
34namespace {
35
36// Object type category names based on ID range
37const char* GetObjectCategory(int object_id) {
38 if (object_id < 0x100)
39 return "Standard";
40 if (object_id < 0x200)
41 return "Extended";
42 if (object_id >= 0xF80)
43 return "Special";
44 return "Unknown";
45}
46
47// Derive a short dungeon-group label from a room's blockset value.
48// Blockset 0-12 map to vanilla ALTTP dungeons; higher values are custom.
49const char* GetBlocksetGroupName(uint8_t blockset) {
50 static const char* kGroupNames[] = {
51 "HC/Sewers", // 0
52 "Eastern", // 1
53 "Desert", // 2
54 "Hera", // 3
55 "A-Tower", // 4
56 "PoD", // 5
57 "Swamp", // 6
58 "Skull", // 7
59 "Thieves", // 8
60 "Ice", // 9
61 "Misery", // 10
62 "Turtle", // 11
63 "GT", // 12
64 };
65 constexpr size_t kCount = sizeof(kGroupNames) / sizeof(kGroupNames[0]);
66 return blockset < kCount ? kGroupNames[blockset] : "Custom";
67}
68
69const char* GetObjectStreamName(int layer_value) {
70 switch (layer_value) {
71 case 0:
72 return "Primary (main pass)";
73 case 1:
74 return "BG2 overlay";
75 case 2:
76 return "BG1 overlay";
77 default:
78 return "Unknown";
79 }
80}
81
82// Pot item names for the inspector
83const char* GetPotItemName(uint8_t item) {
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",
92 };
93 constexpr size_t kCount = sizeof(kNames) / sizeof(kNames[0]);
94 return item < kCount ? kNames[item] : "Unknown";
95}
96
97float ClampWorkbenchPaneWidth(float desired_width, float min_width,
98 float max_width) {
99 return std::clamp(desired_width, min_width, std::max(min_width, max_width));
100}
101
102float CalcWorkbenchIconButtonWidth(const char* icon, float button_height) {
103 if (!icon || !*icon) {
104 return button_height;
105 }
106
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);
112}
113
115 bool show_left = false;
116 bool show_right = false;
117 bool compact_left = false;
118 bool compact_right = false;
119};
120
122 float total_width, float min_canvas_width, float min_sidebar_width,
123 float splitter_width, bool want_left, bool want_right) {
125 result.show_left = want_left;
126 result.show_right = want_right;
127
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)
132 : min_sidebar_width)
133 : 0.0f;
134 required += right ? (compact_right ? std::max(200.0f, min_sidebar_width + 32.0f)
135 : min_sidebar_width)
136 : 0.0f;
137 if (left) {
138 required += splitter_width;
139 }
140 if (right) {
141 required += splitter_width;
142 }
143 return required;
144 };
145
146 if (result.show_left &&
147 total_width < required_width(result.show_left, result.show_right, false,
148 false)) {
149 result.compact_left = true;
150 }
151 if (result.show_right &&
152 total_width < required_width(result.show_left, result.show_right,
153 result.compact_left, false)) {
154 result.compact_right = true;
155 }
156 if (result.show_right &&
157 total_width < required_width(result.show_left, result.show_right,
158 result.compact_left, result.compact_right)) {
159 result.show_right = false;
160 result.compact_right = false;
161 }
162 if (result.show_left &&
163 total_width < required_width(result.show_left, result.show_right,
164 result.compact_left, result.compact_right)) {
165 result.show_left = false;
166 result.compact_left = false;
167 }
168
169 return result;
170}
171
172void DrawVerticalSplitter(const char* id, float height, float* pane_width,
173 float min_width, float max_width,
174 bool resize_from_left_edge) {
175 if (!pane_width) {
176 return;
177 }
178
179 const float splitter_width = gui::UIConfig::kSplitterWidth;
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);
186 }
187 if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
188 *pane_width = ClampWorkbenchPaneWidth(*pane_width, min_width, max_width);
189 }
190 if (active) {
191 const float delta = ImGui::GetIO().MouseDelta.x;
192 const float proposed =
193 resize_from_left_edge ? (*pane_width - delta) : (*pane_width + delta);
194 *pane_width = ClampWorkbenchPaneWidth(proposed, min_width, max_width);
195 ImGui::SetTooltip("Width: %.0f px", *pane_width);
196 }
197
198 ImVec4 splitter_color = gui::GetOutlineVec4();
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);
204}
205
206void DrawWorkbenchInspectorSectionHeader(const char* label) {
207 ImGui::SeparatorText(label);
208}
209
210bool BeginWorkbenchInspectorSection(const char* label, bool default_open) {
211 gui::StyleVarGuard frame_padding_guard(
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);
217}
218
219bool DrawWorkbenchActionButton(const char* label, const ImVec2& size) {
220 gui::StyleVarGuard align_guard(ImGuiStyleVar_ButtonTextAlign,
221 ImVec2(0.08f, 0.5f));
222 return ImGui::Button(label, size);
223}
224
225bool DrawWorkbenchSegment(const char* label, bool selected, float width,
226 float height) {
227 return ImGui::Selectable(label, selected, 0, ImVec2(width, height));
228}
229
230} // namespace
231
233 DungeonRoomSelector* room_selector, int* current_room_id,
234 std::function<void(int)> on_room_selected,
235 std::function<void(int, RoomSelectionIntent)> on_room_selected_with_intent,
236 std::function<void(int)> on_save_room,
237 std::function<DungeonCanvasViewer*()> get_viewer,
238 std::function<DungeonCanvasViewer*()> get_compare_viewer,
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)),
254 rom_(rom) {}
255
257 return "dungeon.workbench";
258}
260 return "Dungeon Workbench";
261}
263 return ICON_MD_WORKSPACES;
264}
266 return "Dungeon";
267}
269 return 10;
270}
271
273 rom_ = rom;
274 room_dungeon_cache_.clear();
276}
277
278void DungeonWorkbenchContent::DrawSidebarPane(float width, float height,
279 float button_size, bool compact) {
280 const bool sidebar_open =
281 ImGui::BeginChild("##DungeonWorkbenchSidebar", ImVec2(width, height),
282 true);
283 if (sidebar_open) {
284 DrawSidebarHeader(button_size, compact);
286 }
287 ImGui::EndChild();
288}
289
291 bool compact) {
292 gui::StyleVarGuard frame_padding_guard(
293 ImGuiStyleVar_FramePadding,
294 ImVec2(ImGui::GetStyle().FramePadding.x,
295 std::max(5.0f, ImGui::GetStyle().FramePadding.y + 1.0f)));
296 gui::StyleVarGuard item_spacing_guard(
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 =
301 std::max(button_size, gui::LayoutHelpers::GetTouchSafeWidgetHeight());
302
303 const bool can_open_overview = static_cast<bool>(show_panel_);
304 const float collapse_w =
305 CalcWorkbenchIconButtonWidth(ICON_MD_CHEVRON_LEFT, button_size);
306 const float menu_w =
307 can_open_overview
308 ? CalcWorkbenchIconButtonWidth(ICON_MD_MORE_HORIZ, button_size)
309 : 0.0f;
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);
315
316 ImGui::AlignTextToFramePadding();
317 ImGui::TextDisabled("%s", compact ? ICON_MD_VIEW_SIDEBAR " Navigate"
318 : ICON_MD_VIEW_SIDEBAR " Navigation");
319 ImGui::SameLine();
320 ImGui::SetCursorPosX(std::max(
321 ImGui::GetCursorPosX(),
322 ImGui::GetWindowContentRegionMax().x - action_cluster_w));
323
324 if (can_open_overview) {
325 if (ImGui::Button(ICON_MD_MORE_HORIZ "##SidebarQuickActions",
326 ImVec2(menu_w, button_size))) {
327 ImGui::OpenPopup("##WorkbenchSidebarQuickActions");
328 }
329 if (ImGui::IsItemHovered()) {
330 ImGui::SetTooltip("Open room review tools");
331 }
332 if (ImGui::BeginPopup("##WorkbenchSidebarQuickActions")) {
333 if (ImGui::MenuItem(ICON_MD_GRID_VIEW " Room Matrix")) {
334 show_panel_("dungeon.room_matrix");
335 }
336 if (ImGui::MenuItem(ICON_MD_MAP " Dungeon Map")) {
337 show_panel_("dungeon.dungeon_map");
338 }
339 ImGui::EndPopup();
340 }
341 ImGui::SameLine();
342 }
343 if (ImGui::Button(ICON_MD_CHEVRON_LEFT "##CollapseRooms",
344 ImVec2(collapse_w, button_size))) {
346 }
347 if (ImGui::IsItemHovered()) {
348 ImGui::SetTooltip("Collapse navigation pane");
349 }
350
351 ImGui::Dummy(ImVec2(0.0f, 3.0f));
352 DrawSidebarModeTabs(stack_mode_switch, segment_height);
353 ImGui::Separator();
354}
355
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);
362 if (DrawWorkbenchSegment(ICON_MD_LIST " Rooms",
363 sidebar_mode_ == SidebarMode::Rooms, mode_width,
364 segment_height)) {
366 }
367 if (!stacked) {
368 ImGui::SameLine();
369 }
370 if (DrawWorkbenchSegment(ICON_MD_DOOR_FRONT " Entrances",
372 mode_width, segment_height)) {
374 }
375}
376
378 if (!room_selector_) {
379 ImGui::TextDisabled("Room navigation unavailable");
380 return;
381 }
382
383 ImGui::PushID("WorkbenchSidebarMode");
384 switch (sidebar_mode_) {
387 break;
390 break;
391 }
392 ImGui::PopID();
393}
394
396 (void)p_open;
397 const auto& theme = AgentUI::GetTheme();
398
399 if (!rom_ || !rom_->is_loaded()) {
400 ImGui::TextDisabled(ICON_MD_INFO " Load a ROM to edit dungeon rooms.");
401 return;
402 }
404 ImGui::TextColored(theme.text_error_red, "Dungeon Workbench not wired");
405 return;
406 }
407
408 DungeonCanvasViewer* primary_viewer = get_viewer_ ? get_viewer_() : nullptr;
409 DungeonCanvasViewer* compare_viewer =
411 const float splitter_w = gui::UIConfig::kSplitterWidth;
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;
421
422 if (current_room_id_) {
424 params.layout = &layout_state_;
425 params.left_sidebar_visible = show_left;
430 params.primary_viewer = primary_viewer;
431 params.compare_viewer = compare_viewer;
437 const bool request_panel_workflow = DungeonWorkbenchToolbar::Draw(params);
438 if (request_panel_workflow && set_workflow_mode_) {
439 // Defer panel visibility mutation until toolbar child/table scopes closed.
440 set_workflow_mode_(false);
441 return;
442 }
443 ImGui::Spacing();
444 }
445
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;
454
455 float left_w = show_left
456 ? (responsive.compact_left ? compact_left_w
458 : 0.0f;
459 float right_w = show_right
460 ? (responsive.compact_right ? compact_right_w
462 : 0.0f;
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);
469 if (show_left) {
470 left_w =
471 ClampWorkbenchPaneWidth(left_w, active_left_min_w,
472 std::max(active_left_min_w, max_left_w));
473 if (!responsive.compact_left) {
474 layout_state_.left_width = left_w;
475 }
476 } else {
477 left_w = 0.0f;
478 }
479 if (show_right) {
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) {
483 layout_state_.right_width = right_w;
484 }
485 } else {
486 right_w = 0.0f;
487 }
488
489 float center_w = total_w - left_w - right_w;
490 if (show_left) {
491 center_w -= splitter_w;
492 }
493 if (show_right) {
494 center_w -= splitter_w;
495 }
496 if (center_w < min_canvas_w) {
497 float deficit = min_canvas_w - center_w;
498 if (show_right) {
499 const float shrink =
500 std::min(deficit, right_w - active_right_min_w);
501 right_w -= shrink;
502 if (!responsive.compact_right) {
503 layout_state_.right_width = right_w;
504 }
505 deficit -= shrink;
506 }
507 if (deficit > 0.0f && show_left) {
508 const float shrink =
509 std::min(deficit, left_w - active_left_min_w);
510 left_w -= shrink;
511 if (!responsive.compact_left) {
512 layout_state_.left_width = left_w;
513 }
514 deficit -= shrink;
515 }
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));
519 }
520
521 if (show_left) {
522 DrawSidebarPane(left_w, total_h, btn, responsive.compact_left);
523 }
524 if (show_left) {
525 ImGui::SameLine(0.0f, 0.0f);
526 DrawVerticalSplitter("##DungeonWorkbenchLeftSplitter", total_h,
527 &layout_state_.left_width, min_sidebar_w,
528 total_w - right_w - min_canvas_w -
529 (show_right ? splitter_w : 0.0f),
530 false);
531 }
532
533 if (show_left) {
534 ImGui::SameLine(0.0f, 0.0f);
535 }
536 DrawCanvasPane(center_w, total_h, primary_viewer, show_left);
537
538 if (show_right) {
539 ImGui::SameLine(0.0f, 0.0f);
540 DrawVerticalSplitter("##DungeonWorkbenchRightSplitter", total_h,
541 &layout_state_.right_width, min_sidebar_w,
542 total_w - left_w - min_canvas_w -
543 (show_left ? splitter_w : 0.0f),
544 true);
545 ImGui::SameLine(0.0f, 0.0f);
546 }
547 if (show_right) {
548 DrawInspectorPane(right_w, total_h, btn, responsive.compact_right,
549 primary_viewer);
550 }
551}
552
554 float width, float height, DungeonCanvasViewer* primary_viewer,
555 bool left_sidebar_visible) {
556 const bool canvas_open =
557 ImGui::BeginChild("##DungeonWorkbenchCanvas", ImVec2(width, height),
558 false);
559 if (canvas_open) {
560 if (primary_viewer) {
561 const bool show_recent_tabs =
562 split_view_enabled_ || !left_sidebar_visible;
563 if (show_recent_tabs) {
565 }
567 DrawSplitView(*primary_viewer);
568 } else {
569 primary_viewer->DrawDungeonCanvas(*current_room_id_);
570 }
571
572 const char* tool_mode = get_tool_mode_ ? get_tool_mode_() : "Select";
573 auto status = DungeonStatusBar::BuildState(*primary_viewer, tool_mode,
574 false);
575 status.workflow_mode = "Workbench";
576 status.workflow_primary = true;
577 if (can_undo_)
578 status.can_undo = can_undo_();
579 if (can_redo_)
580 status.can_redo = can_redo_();
581 if (undo_desc_) {
582 static std::string s_undo_desc;
583 s_undo_desc = undo_desc_();
584 status.undo_desc =
585 s_undo_desc.empty() ? nullptr : s_undo_desc.c_str();
586 }
587 if (redo_desc_) {
588 static std::string s_redo_desc;
589 s_redo_desc = redo_desc_();
590 status.redo_desc =
591 s_redo_desc.empty() ? nullptr : s_redo_desc.c_str();
592 }
593 if (undo_depth_)
594 status.undo_depth = undo_depth_();
595 status.on_undo = on_undo_;
596 status.on_redo = on_redo_;
598 } else {
599 ImGui::TextDisabled("No active viewer");
600 }
601 }
602 ImGui::EndChild();
603}
604
605void DungeonWorkbenchContent::DrawInspectorPane(float width, float height,
606 float button_size,
607 bool compact,
608 DungeonCanvasViewer* viewer) {
609 const bool inspector_open =
610 ImGui::BeginChild("##DungeonWorkbenchInspector", ImVec2(width, height),
611 true);
612 if (inspector_open) {
613 DrawInspectorHeader(button_size, compact);
614 if (viewer) {
615 DrawInspector(*viewer);
616 } else {
617 ImGui::TextDisabled("No active viewer");
618 }
619 }
620 ImGui::EndChild();
621}
622
624 bool compact) {
625 gui::StyleVarGuard frame_padding_guard(
626 ImGuiStyleVar_FramePadding,
627 ImVec2(ImGui::GetStyle().FramePadding.x,
628 std::max(5.0f, ImGui::GetStyle().FramePadding.y + 1.0f)));
629 gui::StyleVarGuard item_spacing_guard(
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 =
634 std::max(button_size, gui::LayoutHelpers::GetTouchSafeWidgetHeight());
635 const float collapse_w =
636 CalcWorkbenchIconButtonWidth(ICON_MD_CHEVRON_RIGHT, button_size);
637
638 ImGui::AlignTextToFramePadding();
639 ImGui::TextDisabled("%s", compact ? ICON_MD_TUNE " Inspect"
640 : ICON_MD_TUNE " Inspector");
641 ImGui::SameLine();
642 ImGui::SetCursorPosX(std::max(
643 ImGui::GetCursorPosX(),
644 ImGui::GetWindowContentRegionMax().x - collapse_w));
645 if (ImGui::Button(ICON_MD_CHEVRON_RIGHT "##CollapseInspector",
646 ImVec2(collapse_w, button_size))) {
648 }
649 if (ImGui::IsItemHovered()) {
650 ImGui::SetTooltip("Collapse inspector");
651 }
652
653 ImGui::Dummy(ImVec2(0.0f, 3.0f));
654 DrawInspectorPrimarySelector(segment_height);
655 ImGui::Separator();
656}
657
660 return;
661 }
662
663 const auto& recent = get_recent_rooms_();
664 if (recent.empty()) {
665 return;
666 }
667 // Copy IDs up-front so we can safely mutate the underlying MRU list (close
668 // tabs) without invalidating iterators mid-loop.
669 std::vector<int> recent_ids(recent.begin(), recent.end());
670 std::vector<int> to_forget;
671
672 constexpr ImGuiTabBarFlags kFlags = ImGuiTabBarFlags_AutoSelectNewTabs |
673 ImGuiTabBarFlags_FittingPolicyScroll |
674 ImGuiTabBarFlags_TabListPopupButton;
675
676 // Adaptive frame padding: larger tabs on touch/iPad for easier tapping
677 const ImVec2 frame_pad = ImGui::GetStyle().FramePadding;
678 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
679 const float extra_y = is_touch ? 6.0f : 1.0f;
680 const float extra_x = is_touch ? 4.0f : 0.0f;
681 gui::StyleVarGuard pad_guard(
682 ImGuiStyleVar_FramePadding,
683 ImVec2(frame_pad.x + extra_x, frame_pad.y + extra_y));
684
685 if (gui::BeginThemedTabBar("##DungeonRecentRooms", kFlags)) {
686 for (int room_id : recent_ids) {
687 bool open = true;
688 const ImGuiTabItemFlags tab_flags =
689 (room_id == *current_room_id_) ? ImGuiTabItemFlags_SetSelected : 0;
690 const auto room_name = zelda3::GetRoomLabel(room_id);
691 char tab_label[64];
692 if (room_name.empty() || room_name == "Unknown") {
693 snprintf(tab_label, sizeof(tab_label), "%03X##recent_%03X", room_id,
694 room_id);
695 } else {
696 snprintf(tab_label, sizeof(tab_label), "%03X %.12s##recent_%03X",
697 room_id, room_name.c_str(), room_id);
698 }
699 const bool selected = ImGui::BeginTabItem(tab_label, &open, tab_flags);
700
701 if (!open && forget_recent_room_) {
702 to_forget.push_back(room_id);
703 }
704
705 if (ImGui::IsItemHovered()) {
706 const auto label = zelda3::GetRoomLabel(room_id);
707 ImGui::SetTooltip("[%03X] %s", room_id, label.c_str());
708 }
709
710 if (ImGui::IsItemActivated() && room_id != *current_room_id_) {
711 on_room_selected_(room_id);
712 }
713
714 if (ImGui::BeginPopupContextItem()) {
715 if (ImGui::MenuItem(ICON_MD_COMPARE_ARROWS " Compare")) {
716 split_view_enabled_ = true;
717 compare_room_id_ = room_id;
718 }
720 ImGui::MenuItem(ICON_MD_OPEN_IN_NEW " Open as Panel")) {
723 }
724 if (forget_recent_room_ && ImGui::MenuItem(ICON_MD_CLOSE " Close")) {
725 to_forget.push_back(room_id);
726 }
727 ImGui::EndPopup();
728 }
729
730 if (selected) {
731 ImGui::EndTabItem();
732 }
733 }
734
736 }
737
738 if (!to_forget.empty() && forget_recent_room_) {
739 for (int rid : to_forget) {
741 }
742 }
743}
744
746 DungeonCanvasViewer& primary_viewer) {
749 split_view_enabled_ = false;
750 }
751 return;
752 }
753
754 // Choose a sensible default compare room (most-recent non-current).
756 if (get_recent_rooms_) {
757 for (int rid : get_recent_rooms_()) {
758 if (rid != *current_room_id_) {
759 compare_room_id_ = rid;
760 break;
761 }
762 }
763 }
764 }
765
766 if (compare_room_id_ < 0) {
767 // Nothing to compare yet.
768 split_view_enabled_ = false;
769 primary_viewer.DrawDungeonCanvas(*current_room_id_);
770 return;
771 }
772
773 constexpr ImGuiTableFlags kSplitFlags =
774 ImGuiTableFlags_Resizable | ImGuiTableFlags_NoPadOuterX |
775 ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_BordersInnerV;
776
777 if (!ImGui::BeginTable("##DungeonWorkbenchSplit", 2, kSplitFlags)) {
778 primary_viewer.DrawDungeonCanvas(*current_room_id_);
779 return;
780 }
781
782 ImGui::TableSetupColumn("Active", ImGuiTableColumnFlags_WidthStretch);
783 ImGui::TableSetupColumn("Compare", ImGuiTableColumnFlags_WidthStretch);
784 ImGui::TableNextRow();
785
786 // Active pane (minimum height so canvas never collapses)
787 ImGui::TableNextColumn();
788 ImGui::AlignTextToFramePadding();
789 ImGui::TextDisabled(ICON_MD_CROP_FREE " Active [%03X] %s",
791 zelda3::GetRoomLabel(*current_room_id_).c_str());
792 ImGui::Separator();
793 const bool split_active_open = gui::LayoutHelpers::BeginContentChild(
794 "##SplitActive", ImVec2(0.0f, gui::UIConfig::kContentMinHeightCanvas));
795 if (split_active_open) {
796 primary_viewer.DrawDungeonCanvas(*current_room_id_);
797 }
799
800 // Compare pane
801 ImGui::TableNextColumn();
802 ImGui::AlignTextToFramePadding();
803 ImGui::TextDisabled(ICON_MD_COMPARE_ARROWS " Compare [%03X] %s",
806 ImGui::Separator();
807 const bool split_compare_open = gui::LayoutHelpers::BeginContentChild(
808 "##SplitCompare", ImVec2(0.0f, gui::UIConfig::kContentMinHeightCanvas));
809 if (split_compare_open) {
810 if (auto* compare_viewer =
813 compare_viewer->canvas().ApplyScaleSnapshot(
814 primary_viewer.canvas().GetConfig());
815 }
816 compare_viewer->DrawDungeonCanvas(compare_room_id_);
817 } else {
818 ImGui::TextDisabled("No compare viewer");
819 }
820 }
822
823 ImGui::EndTable();
824}
825
827 room_dungeon_cache_.clear();
828 room_dungeon_cache_built_ = true; // Always set, even if ROM missing.
829 if (!rom_ || !rom_->is_loaded())
830 return;
831
832 // Short dungeon names for display in the inspector badge.
833 // Indices 0-13 = vanilla ALTTP dungeons; higher indices = custom/Oracle.
834 static const char* const kShortNames[] = {
835 "Sewers", "HC", "Eastern", "Desert", "A-Tower", "Swamp", "PoD",
836 "Misery", "Skull", "Ice", "Hera", "Thieves", "Turtle", "GT",
837 };
838 constexpr int kVanillaCount =
839 static_cast<int>(sizeof(kShortNames) / sizeof(kShortNames[0]));
840
841 auto AddRoom = [&](int room_id, int dungeon_id) {
842 if (room_id < 0)
843 return;
844 if (room_dungeon_cache_.contains(room_id))
845 return; // Entrance wins over spawn.
846 if (dungeon_id >= 0 && dungeon_id < kVanillaCount) {
847 room_dungeon_cache_[room_id] = kShortNames[dungeon_id];
848 } else {
849 char buf[16];
850 snprintf(buf, sizeof(buf), "Dungeon %02X", dungeon_id);
851 room_dungeon_cache_[room_id] = buf;
852 }
853 };
854
855 // Standard entrances (0x00–0x83) — authoritative dungeon assignment.
856 for (int i = 0; i < 0x84; ++i) {
857 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), false);
858 int did = ent.dungeon_id_;
859 if (did >= 0 && did < kVanillaCount) {
860 room_dungeon_cache_[ent.room_] = kShortNames[did];
861 } else {
862 char buf[16];
863 snprintf(buf, sizeof(buf), "Dungeon %02X", did);
864 room_dungeon_cache_[ent.room_] = buf;
865 }
866 }
867
868 // Spawn points (0x00–0x13) — fill in rooms not covered by entrances.
869 for (int i = 0; i < 0x14; ++i) {
870 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), true);
871 AddRoom(static_cast<int>(ent.room_), static_cast<int>(ent.dungeon_id_));
872 }
873}
874
876 gui::StyleVarGuard item_spacing_guard(
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)));
880 DrawInspectorShelf(viewer);
881}
882
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;
889 if (DrawWorkbenchSegment(ICON_MD_CASTLE " Room",
891 button_width, segment_height)) {
893 }
894 if (!stack) {
895 ImGui::SameLine();
896 }
897 if (DrawWorkbenchSegment(ICON_MD_SELECT_ALL " Selection",
899 button_width, segment_height)) {
901 }
902}
903
905 DungeonCanvasViewer& viewer) {
906 const int room_id =
907 (viewer.current_room_id() >= 0) ? viewer.current_room_id()
909 const auto& interaction = viewer.object_interaction();
910 const size_t selected_objects = interaction.GetSelectionCount();
911 const bool has_entity = interaction.HasEntitySelection();
912
913 ImGui::TextDisabled(ICON_MD_SUMMARIZE " Summary");
914 if (room_id >= 0) {
915 ImGui::Text("[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
916 } else {
917 ImGui::TextDisabled("No room selected");
918 }
919
920 ImGui::Spacing();
921 if (selected_objects > 0 || has_entity) {
922 ImGui::TextDisabled(ICON_MD_SELECT_ALL " Focus");
923 if (has_entity) {
924 ImGui::BulletText("Entity selected");
925 }
926 if (selected_objects > 0) {
927 ImGui::BulletText("%zu object%s selected", selected_objects,
928 selected_objects == 1 ? "" : "s");
929 }
930 if (DrawWorkbenchActionButton(ICON_MD_OPEN_IN_FULL " Open Selection",
931 ImVec2(-1, 0))) {
933 }
934 } else {
935 ImGui::TextDisabled("Nothing selected");
936 }
937
938 if (room_id >= 0) {
939 ImGui::Spacing();
940 if (on_save_room_ &&
941 DrawWorkbenchActionButton(ICON_MD_SAVE " Save Room", ImVec2(-1, 0))) {
942 on_save_room_(room_id);
943 }
944 if (DrawWorkbenchActionButton(ICON_MD_CASTLE " Room Details",
945 ImVec2(-1, 0))) {
947 }
948 }
949
950 ImGui::Spacing();
951 if (BeginWorkbenchInspectorSection(ICON_MD_VISIBILITY " View", true)) {
953 }
954
955 if (show_panel_ && BeginWorkbenchInspectorSection(ICON_MD_BUILD " Tools", false)) {
957 }
958}
959
961 if (ImGui::GetContentRegionAvail().x < 240.0f) {
963 return;
964 }
965
966 ImGui::Spacing();
969 } else {
971 }
972
973 ImGui::Spacing();
974 if (BeginWorkbenchInspectorSection(ICON_MD_VISIBILITY " View Options", true)) {
976 }
977 if (BeginWorkbenchInspectorSection(ICON_MD_BUILD " Quick Tools", false)) {
979 }
980}
981
983 DungeonCanvasViewer& viewer) {
984 const auto& theme = AgentUI::GetTheme();
985
986 int room_id = viewer.current_room_id();
987 if (room_id < 0 && current_room_id_) {
988 room_id = *current_room_id_;
989 }
990
991 const std::string room_label =
992 (room_id >= 0) ? zelda3::GetRoomLabel(room_id) : std::string("None");
993
994 // Room badge: hex ID + copy button (only for valid room IDs).
995 DrawWorkbenchInspectorSectionHeader(ICON_MD_CASTLE " Room Summary");
996 if (room_id >= 0) {
997 ImGui::Text("Room: 0x%03X (%d)", room_id, room_id);
998 ImGui::SameLine();
999 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY "##CopyRoomId")) {
1000 char buf[16];
1001 snprintf(buf, sizeof(buf), "0x%03X", room_id);
1002 ImGui::SetClipboardText(buf);
1003 }
1004 if (ImGui::IsItemHovered()) {
1005 ImGui::SetTooltip("Copy room ID (0x%03X) to clipboard", room_id);
1006 }
1007 } else {
1008 ImGui::TextUnformatted("Room: None");
1009 }
1010
1011 // Dungeon group context: prefer ROM entrance-based lookup (accurate for
1012 // custom Oracle dungeons); fall back to blockset-derived name.
1015 }
1016 if (room_id >= 0) {
1017 const char* group_name = nullptr;
1018 {
1019 auto cache_it = room_dungeon_cache_.find(room_id);
1020 if (cache_it != room_dungeon_cache_.end() && !cache_it->second.empty()) {
1021 group_name = cache_it->second.c_str();
1022 }
1023 }
1024 if (!group_name) {
1025 auto* rooms = viewer.rooms();
1026 if (rooms && room_id < static_cast<int>(rooms->size())) {
1027 group_name = GetBlocksetGroupName((*rooms)[room_id].blockset());
1028 }
1029 }
1030 if (group_name) {
1031 ImGui::TextDisabled(ICON_MD_CASTLE " %s – %s", group_name,
1032 room_label.c_str());
1033 } else {
1034 ImGui::TextDisabled("%s", room_label.c_str());
1035 }
1036 } else {
1037 ImGui::TextDisabled("%s", room_label.c_str());
1038 }
1039
1040 // Quick actions.
1041 DrawWorkbenchInspectorSectionHeader(ICON_MD_SAVE " Room Actions");
1042 if (on_save_room_ && room_id >= 0) {
1043 if (DrawWorkbenchActionButton(ICON_MD_SAVE " Save Room", ImVec2(-1, 0))) {
1044 on_save_room_(room_id);
1045 }
1046 }
1047
1048 if (show_panel_) {
1049 ImGui::TextDisabled(ICON_MD_OPEN_IN_NEW " Open Panels");
1050 constexpr ImGuiTableFlags kPanelFlags =
1051 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1052 if (ImGui::BeginTable("##WorkbenchRoomPanels", 2, kPanelFlags)) {
1053 ImGui::TableNextRow();
1054 ImGui::TableNextColumn();
1055 if (DrawWorkbenchActionButton(ICON_MD_IMAGE " Graphics",
1056 ImVec2(-1, 0))) {
1057 show_panel_("dungeon.room_graphics");
1058 }
1059 ImGui::TableNextColumn();
1060 if (DrawWorkbenchActionButton(ICON_MD_SETTINGS " Settings",
1061 ImVec2(-1, 0))) {
1062 show_panel_("dungeon.settings");
1063 }
1064 ImGui::EndTable();
1065 }
1066 }
1067
1068 // Core room properties (moved from canvas header).
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];
1073
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();
1078
1079 constexpr float kHexW = 92.0f;
1080
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);
1087
1088 gui::LayoutHelpers::PropertyRow("Blockset", [&]() {
1089 if (auto res = gui::InputHexByteEx("##Blockset", &blockset_val, 81,
1090 kHexW, true);
1091 res.ShouldApply()) {
1092 room.SetBlockset(blockset_val);
1093 if (room.rom() && room.rom()->is_loaded()) {
1094 room.RenderRoomGraphics();
1095 }
1096 }
1097 if (ImGui::IsItemHovered()) {
1098 ImGui::SetTooltip("Blockset (0-51)");
1099 }
1100 });
1101 gui::LayoutHelpers::PropertyRow("Palette", [&]() {
1102 if (auto res =
1103 gui::InputHexByteEx("##Palette", &palette_val, 71, kHexW, true);
1104 res.ShouldApply()) {
1105 room.SetPalette(palette_val);
1106 if (room.rom() && room.rom()->is_loaded()) {
1107 room.RenderRoomGraphics();
1108 }
1109 // Re-run editor sync so palette group + dependent panels update.
1110 if (on_room_selected_) {
1111 on_room_selected_(room_id);
1112 }
1113 }
1114 if (ImGui::IsItemHovered()) {
1115 ImGui::SetTooltip("Palette (0-47)");
1116 }
1117 });
1118 gui::LayoutHelpers::PropertyRow("Layout", [&]() {
1119 if (auto res =
1120 gui::InputHexByteEx("##Layout", &layout_val, 7, kHexW, true);
1121 res.ShouldApply()) {
1122 room.SetLayoutId(layout_val);
1123 room.MarkLayoutDirty();
1124 if (room.rom() && room.rom()->is_loaded()) {
1125 room.RenderRoomGraphics();
1126 }
1127 }
1128 if (ImGui::IsItemHovered()) {
1129 ImGui::SetTooltip("Layout (0-7)");
1130 }
1131 });
1132 gui::LayoutHelpers::PropertyRow("Spriteset", [&]() {
1133 if (auto res = gui::InputHexByteEx("##Spriteset", &spriteset_val, 143,
1134 kHexW, true);
1135 res.ShouldApply()) {
1136 room.SetSpriteset(spriteset_val);
1137 if (room.rom() && room.rom()->is_loaded()) {
1138 room.RenderRoomGraphics();
1139 }
1140 }
1141 if (ImGui::IsItemHovered()) {
1142 ImGui::SetTooltip("Spriteset (0-8F)");
1143 }
1144 });
1145
1146 ImGui::EndTable();
1147 }
1148 } else {
1149 ImGui::TextDisabled("Room properties unavailable");
1150 }
1151
1152 DrawWorkbenchInspectorSectionHeader(ICON_MD_BUILD " Editing Status");
1153 auto& interaction = viewer.object_interaction();
1154 const bool placing = interaction.mode_manager().IsPlacementActive();
1155 if (placing) {
1156 ImGui::TextColored(theme.text_info, "Placement active");
1157 ImGui::SameLine();
1158 if (ImGui::SmallButton(ICON_MD_CLOSE " Cancel")) {
1159 interaction.mode_manager().CancelCurrentMode();
1160 }
1161 }
1162}
1163
1165 DungeonCanvasViewer& viewer) {
1166 auto& interaction = viewer.object_interaction();
1167 const auto& theme = AgentUI::GetTheme();
1168
1169 const int room_id = viewer.current_room_id();
1170 const size_t obj_count = interaction.GetSelectionCount();
1171 const bool has_entity = interaction.HasEntitySelection();
1172
1173 if (!has_entity && obj_count == 0) {
1174 DrawWorkbenchInspectorSectionHeader(ICON_MD_INFO " Selection");
1175 ImGui::TextDisabled("Click an object or entity to inspect");
1176 if (show_panel_) {
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();
1184 if (DrawWorkbenchActionButton(ICON_MD_CATEGORY " Selector",
1185 ImVec2(-1, 0))) {
1186 show_panel_("dungeon.object_selector");
1187 }
1188 ImGui::TableNextColumn();
1189 if (DrawWorkbenchActionButton(ICON_MD_PERSON " Sprites",
1190 ImVec2(-1, 0))) {
1191 show_panel_("dungeon.sprite_editor");
1192 }
1193 ImGui::EndTable();
1194 }
1195 }
1196 return;
1197 }
1198
1199 // ── Tile Object Selection ──
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) {
1205 ImGui::SameLine();
1206 ImGui::TextDisabled("Focused selection");
1207 }
1208
1209 constexpr ImGuiTableFlags kActionFlags =
1210 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1211 if (ImGui::BeginTable("##SelectionObjectActions", 3, kActionFlags)) {
1212 ImGui::TableNextRow();
1213 ImGui::TableNextColumn();
1214 if (DrawWorkbenchActionButton(ICON_MD_DELETE " Delete",
1215 ImVec2(-1, 0))) {
1216 viewer.DeleteSelectedObjects();
1217 }
1218 ImGui::TableNextColumn();
1219 if (DrawWorkbenchActionButton(ICON_MD_CLEAR " Clear", ImVec2(-1, 0))) {
1220 interaction.ClearSelection();
1221 }
1222 ImGui::TableNextColumn();
1223 if (show_panel_) {
1224 if (DrawWorkbenchActionButton(ICON_MD_TUNE " Open Editor",
1225 ImVec2(-1, 0))) {
1226 show_panel_("dungeon.object_editor");
1227 }
1228 } else {
1229 ImGui::BeginDisabled();
1230 DrawWorkbenchActionButton(ICON_MD_TUNE " Open Editor",
1231 ImVec2(-1, 0));
1232 ImGui::EndDisabled();
1233 }
1234 ImGui::EndTable();
1235 }
1236
1237 const auto indices = interaction.GetSelectedObjectIndices();
1238
1239 // Multi-object summary
1240 if (indices.size() > 1 && room_id >= 0 && viewer.rooms()) {
1241 auto& room = (*viewer.rooms())[room_id];
1242 auto& objects = room.GetTileObjects();
1243 DrawWorkbenchInspectorSectionHeader(ICON_MD_SUMMARIZE " Multi-selection");
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];
1248 std::string name = zelda3::GetObjectName(obj.id_);
1249 ImGui::BulletText("0x%03X %s", obj.id_, name.c_str());
1250 }
1251 }
1252 if (indices.size() > 8) {
1253 ImGui::TextDisabled(" ... and %zu more", indices.size() - 8);
1254 }
1255 }
1256
1257 // Single-object detailed inspector
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];
1264 const std::string obj_name = zelda3::GetObjectName(obj.id_);
1265 const int subtype = zelda3::GetObjectSubtype(obj.id_);
1266
1267 // Name + category header
1268 DrawWorkbenchInspectorSectionHeader(ICON_MD_CATEGORY " Focused Object");
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);
1272
1273 // Property table
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,
1279 56.0f);
1280 ImGui::TableSetupColumn("Val", ImGuiTableColumnFlags_WidthStretch);
1281
1282 // ID
1284 uint16_t obj_id = static_cast<uint16_t>(obj.id_ & 0x0FFF);
1285 if (auto res =
1286 gui::InputHexWordEx("##SelObjId", &obj_id, 80.0f, true);
1287 res.ShouldApply()) {
1288 obj_id &= 0x0FFF;
1289 interaction.SetObjectId(idx, static_cast<int16_t>(obj_id));
1290 }
1291 });
1292
1293 // Position
1294 gui::LayoutHelpers::PropertyRow("Pos", [&]() {
1295 int pos_x = obj.x_;
1296 int pos_y = obj.y_;
1297 ImGui::SetNextItemWidth(60);
1298 bool x_changed =
1299 ImGui::DragInt("##SelObjX", &pos_x, 0.1f, 0, 63, "X:%d");
1300 ImGui::SameLine();
1301 ImGui::SetNextItemWidth(60);
1302 bool y_changed =
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);
1309 }
1310 });
1311
1312 // Size
1313 gui::LayoutHelpers::PropertyRow("Size", [&]() {
1314 uint8_t size = obj.size_ & 0x0F;
1315 if (auto res = gui::InputHexByteEx("##SelObjSize", &size, 0x0F,
1316 60.0f, true);
1317 res.ShouldApply()) {
1318 interaction.SetObjectSize(idx, size);
1319 }
1320 });
1321
1322 // Stream / layer routing
1323 gui::LayoutHelpers::PropertyRow("Stream", [&]() {
1324 int layer = static_cast<int>(obj.GetLayerValue());
1325 const char* layer_names[] = {"Primary (main pass)", "BG2 overlay",
1326 "BG1 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(
1332 idx, static_cast<zelda3::RoomObject::LayerType>(layer));
1333 }
1334 });
1335 gui::LayoutHelpers::PropertyRow("Route", [&]() {
1336 ImGui::TextDisabled("%s", GetObjectStreamName(obj.GetLayerValue()));
1337 });
1338
1339 // Pixel coords (read-only info)
1340 gui::LayoutHelpers::PropertyRow("Pixel", [&]() {
1341 ImGui::TextDisabled("(%d, %d)", obj.x_ * 8, obj.y_ * 8);
1342 });
1343
1344 ImGui::EndTable();
1345 }
1346 }
1347 }
1348 }
1349
1350 // ── Entity Selection (Doors, Sprites, Items) ──
1351 if (has_entity && room_id >= 0 && viewer.rooms()) {
1352 const auto sel = interaction.GetSelectedEntity();
1353 auto& room = (*viewer.rooms())[room_id];
1354 DrawWorkbenchInspectorSectionHeader(ICON_MD_SELECT_ALL " Entity Selection");
1355
1356 const char* entity_panel_id = nullptr;
1357 const char* entity_action_label = nullptr;
1358
1359 switch (sel.type) {
1360 case EntityType::Door: {
1361 entity_panel_id = "dungeon.entrance_properties";
1362 entity_action_label = ICON_MD_TUNE " Entrance Panel";
1363 const auto& doors = room.GetDoors();
1364 if (sel.index < doors.size()) {
1365 const auto& door = doors[sel.index];
1366 std::string type_name(zelda3::GetDoorTypeName(door.type));
1367 std::string dir_name(zelda3::GetDoorDirectionName(door.direction));
1368
1369 ImGui::TextColored(theme.text_primary, ICON_MD_DOOR_FRONT " %s",
1370 type_name.c_str());
1371 ImGui::TextDisabled("Direction: %s Position: 0x%02X",
1372 dir_name.c_str(), door.position);
1373
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,
1377 pixel_x, pixel_y);
1378 }
1379 break;
1380 }
1381 case EntityType::Sprite: {
1382 entity_panel_id = "dungeon.sprite_editor";
1383 entity_action_label = ICON_MD_PERSON " Sprite Panel";
1384 const auto& sprites = room.GetSprites();
1385 if (sel.index < sprites.size()) {
1386 const auto& sprite = sprites[sel.index];
1387 std::string sprite_name = zelda3::GetSpriteLabel(sprite.id());
1388
1389 ImGui::TextColored(theme.text_primary, ICON_MD_PERSON " %s",
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);
1395
1396 // Overlord check
1397 if (sprite.subtype() == 0x07 && sprite.id() >= 0x01 &&
1398 sprite.id() <= 0x1A) {
1399 std::string overlord_name = zelda3::GetOverlordLabel(sprite.id());
1400 ImGui::TextColored(theme.text_warning_yellow,
1401 ICON_MD_STAR " Overlord: %s",
1402 overlord_name.c_str());
1403 }
1404 }
1405 break;
1406 }
1407 case EntityType::Item: {
1408 entity_panel_id = "dungeon.item_editor";
1409 entity_action_label = ICON_MD_INVENTORY " Item Panel";
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);
1414
1415 ImGui::TextColored(theme.text_primary, ICON_MD_INVENTORY_2 " %s",
1416 item_name);
1417 ImGui::TextDisabled("Item ID: 0x%02X Raw Pos: 0x%04X", pot_item.item,
1418 pot_item.position);
1419 ImGui::TextDisabled("Pixel: (%d, %d) Tile: (%d, %d)",
1420 pot_item.GetPixelX(), pot_item.GetPixelY(),
1421 pot_item.GetTileX(), pot_item.GetTileY());
1422 }
1423 break;
1424 }
1425 default:
1426 break;
1427 }
1428
1429 if (ImGui::BeginTable(
1430 "##EntityActions", 2,
1431 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
1432 ImGui::TableNextRow();
1433 ImGui::TableNextColumn();
1434 if (DrawWorkbenchActionButton(ICON_MD_DELETE " Delete Entity",
1435 ImVec2(-1, 0))) {
1436 interaction.entity_coordinator().DeleteSelectedEntity();
1437 interaction.ClearEntitySelection();
1438 }
1439 ImGui::TableNextColumn();
1440 if (show_panel_ && entity_panel_id && entity_action_label) {
1441 if (DrawWorkbenchActionButton(entity_action_label, ImVec2(-1, 0))) {
1442 show_panel_(entity_panel_id);
1443 }
1444 } else {
1445 ImGui::BeginDisabled();
1446 DrawWorkbenchActionButton(ICON_MD_OPEN_IN_NEW " Open Panel",
1447 ImVec2(-1, 0));
1448 ImGui::EndDisabled();
1449 }
1450 ImGui::EndTable();
1451 }
1452 }
1453}
1454
1456 DungeonCanvasViewer& viewer) {
1457 DrawWorkbenchInspectorSectionHeader(ICON_MD_VISIBILITY " Overlay Toggles");
1458 bool val = viewer.show_grid();
1459 if (ImGui::Checkbox("Grid (8x8)", &val))
1460 viewer.set_show_grid(val);
1461
1462 val = viewer.show_object_bounds();
1463 if (ImGui::Checkbox("Object Bounds", &val)) {
1464 viewer.set_show_object_bounds(val);
1465 }
1466
1467 val = viewer.show_coordinate_overlay();
1468 if (ImGui::Checkbox("Hover Coordinates", &val)) {
1469 viewer.set_show_coordinate_overlay(val);
1470 }
1471
1472 val = viewer.show_camera_quadrant_overlay();
1473 if (ImGui::Checkbox("Camera Quadrants", &val)) {
1475 }
1476
1477 val = viewer.show_track_collision_overlay();
1478 if (ImGui::Checkbox("Track Collision", &val)) {
1480 }
1481
1482 val = viewer.show_custom_collision_overlay();
1483 if (ImGui::Checkbox("Custom Collision", &val)) {
1485 }
1486
1487 val = viewer.show_water_fill_overlay();
1488 if (ImGui::Checkbox("Water Fill (Oracle)", &val)) {
1489 viewer.set_show_water_fill_overlay(val);
1490 }
1491
1492 val = viewer.show_minecart_sprite_overlay();
1493 if (ImGui::Checkbox("Minecart Pathing", &val)) {
1495 }
1496
1497 val = viewer.show_track_gap_overlay();
1498 if (ImGui::Checkbox("Track Gaps", &val)) {
1499 viewer.set_show_track_gap_overlay(val);
1500 }
1501
1502 val = viewer.show_track_route_overlay();
1503 if (ImGui::Checkbox("Track Routes", &val)) {
1504 viewer.set_show_track_route_overlay(val);
1505 }
1506
1507 val = viewer.show_custom_objects_overlay();
1508 if (ImGui::Checkbox("Custom Objects (Oracle)", &val)) {
1510 }
1511 if (ImGui::IsItemHovered()) {
1512 ImGui::SetTooltip(
1513 "Highlight custom-draw objects (IDs 0x31/0x32)\n"
1514 "with a cyan overlay showing position and subtype.");
1515 }
1516}
1517
1519 DungeonCanvasViewer& /*viewer*/) {
1520 if (!show_panel_) {
1521 ImGui::TextDisabled("No panel launcher available");
1522 return;
1523 }
1524
1525 DrawWorkbenchInspectorSectionHeader(ICON_MD_EDIT_NOTE " Edit");
1526 constexpr ImGuiTableFlags kFlags =
1527 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1528 if (!ImGui::BeginTable("##WorkbenchToolsGrid", 2, kFlags)) {
1529 return;
1530 }
1531
1532 ImGui::TableNextRow();
1533 ImGui::TableNextColumn();
1534 if (DrawWorkbenchActionButton(ICON_MD_CATEGORY " Selector", ImVec2(-1, 0))) {
1535 show_panel_("dungeon.object_selector");
1536 }
1537 ImGui::TableNextColumn();
1538 if (DrawWorkbenchActionButton(ICON_MD_TUNE " Object Editor", ImVec2(-1, 0))) {
1539 show_panel_("dungeon.object_editor");
1540 }
1541
1542 ImGui::TableNextRow();
1543 ImGui::TableNextColumn();
1544 if (DrawWorkbenchActionButton(ICON_MD_PERSON " Sprites", ImVec2(-1, 0))) {
1545 show_panel_("dungeon.sprite_editor");
1546 }
1547 ImGui::TableNextColumn();
1548 if (DrawWorkbenchActionButton(ICON_MD_INVENTORY " Items", ImVec2(-1, 0))) {
1549 show_panel_("dungeon.item_editor");
1550 }
1551
1552 ImGui::TableNextRow();
1553 ImGui::TableNextColumn();
1554 if (DrawWorkbenchActionButton(ICON_MD_SETTINGS " Settings",
1555 ImVec2(-1, 0))) {
1556 show_panel_("dungeon.settings");
1557 }
1558 ImGui::TableNextColumn();
1559 ImGui::Dummy(ImVec2(0.0f, 0.0f));
1560
1561 ImGui::EndTable();
1562
1563 DrawWorkbenchInspectorSectionHeader(ICON_MD_TRAVEL_EXPLORE " Review");
1564 if (ImGui::BeginTable("##WorkbenchReviewGrid", 2, kFlags)) {
1565 ImGui::TableNextRow();
1566 ImGui::TableNextColumn();
1567 if (DrawWorkbenchActionButton(ICON_MD_GRID_VIEW " Matrix",
1568 ImVec2(-1, 0))) {
1569 show_panel_("dungeon.room_matrix");
1570 }
1571 ImGui::TableNextColumn();
1572 if (DrawWorkbenchActionButton(ICON_MD_MAP " Dungeon Map",
1573 ImVec2(-1, 0))) {
1574 show_panel_("dungeon.dungeon_map");
1575 }
1576
1577 ImGui::TableNextRow();
1578 ImGui::TableNextColumn();
1579 if (DrawWorkbenchActionButton(ICON_MD_DOOR_FRONT " Entrances",
1580 ImVec2(-1, 0))) {
1581 show_panel_("dungeon.entrance_list");
1582 }
1583 ImGui::TableNextColumn();
1584 if (DrawWorkbenchActionButton(ICON_MD_PALETTE " Palette",
1585 ImVec2(-1, 0))) {
1586 show_panel_("dungeon.palette_editor");
1587 }
1588 ImGui::EndTable();
1589 }
1590
1591 DrawWorkbenchInspectorSectionHeader(ICON_MD_KEYBOARD " Reference");
1592 if (DrawWorkbenchActionButton(ICON_MD_KEYBOARD " Keyboard Shortcuts",
1593 ImVec2(-1, 0))) {
1594 show_shortcut_legend_ = true;
1595 }
1597}
1598
1599} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
bool is_loaded() const
Definition rom.h:132
DungeonObjectInteraction & object_interaction()
Handles room and entrance selection UI.
void DrawRoomBrowser(RoomSelectionIntent single_click_intent=RoomSelectionIntent::kFocusInWorkbench)
static void Draw(const DungeonStatusBarState &state)
static DungeonStatusBarState BuildState(const DungeonCanvasViewer &viewer, const char *tool_mode, bool room_dirty)
void DrawInspectorShelfSelection(DungeonCanvasViewer &viewer)
int GetPriority() const override
Get display priority for menu ordering.
std::function< DungeonCanvasViewer *()> get_compare_viewer_
void DrawInspectorHeader(float button_size, bool compact)
void DrawInspectorCompactSummary(DungeonCanvasViewer &viewer)
void DrawSidebarModeTabs(bool stacked, float segment_height)
void DrawInspectorPrimarySelector(float segment_height)
void DrawSidebarPane(float width, float height, float button_size, bool compact)
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::string GetEditorCategory() const override
Editor category this panel belongs to.
void DrawInspectorShelfTools(DungeonCanvasViewer &viewer)
void DrawCanvasPane(float width, float height, DungeonCanvasViewer *primary_viewer, bool left_sidebar_visible)
void DrawSidebarHeader(float button_size, bool compact)
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< void(int, RoomSelectionIntent)> on_room_selected_with_intent_
std::string GetId() const override
Unique identifier for this panel.
void DrawSplitView(DungeonCanvasViewer &primary_viewer)
std::string GetIcon() const override
Material Design icon for this panel.
std::unordered_map< int, std::string > room_dungeon_cache_
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.
std::function< const std::deque< int > &()> get_recent_rooms_
void DrawInspector(DungeonCanvasViewer &viewer)
std::function< DungeonCanvasViewer *()> get_viewer_
static bool Draw(const DungeonWorkbenchToolbarParams &params)
bool IsPlacementActive() const
Check if any placement mode is active.
CanvasConfig & GetConfig()
Definition canvas.h:324
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)
RAII guard for ImGui style vars.
Definition style_guard.h:68
Dungeon Room Entrance or Spawn Point.
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_SUMMARIZE
Definition icons.h:1885
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_COMPARE_ARROWS
Definition icons.h:448
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_OPEN_IN_FULL
Definition icons.h:1353
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_TRAVEL_EXPLORE
Definition icons.h:2012
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_INVENTORY
Definition icons.h:1011
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_CHEVRON_LEFT
Definition icons.h:405
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_EDIT_NOTE
Definition icons.h:650
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_CATEGORY
Definition icons.h:382
#define ICON_MD_VIEW_SIDEBAR
Definition icons.h:2095
#define ICON_MD_CHEVRON_RIGHT
Definition icons.h:406
#define ICON_MD_WORKSPACES
Definition icons.h:2186
#define ICON_MD_CROP_FREE
Definition icons.h:495
const AgentUITheme & GetTheme()
ResponsiveWorkbenchLayout ResolveResponsiveWorkbenchLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, bool want_left, bool want_right)
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 DrawWorkbenchSegment(const char *label, bool selected, float width, float height)
float ClampWorkbenchPaneWidth(float desired_width, float min_width, float max_width)
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.
void EndThemedTabBar()
InputHexResult InputHexByteEx(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:407
ImVec4 GetOutlineVec4()
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:439
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.
Definition door_types.h:161
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:106
std::function< const std::deque< int > &()> get_recent_rooms
bool ShouldApply() const
Definition input.h:48
static constexpr float kContentMinHeightCanvas
Definition ui_config.h:56
static constexpr float kSplitterWidth
Definition ui_config.h:76
static constexpr float kContentMinWidthSidebar
Definition ui_config.h:58