yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_editor_panel.cc
Go to the documentation of this file.
1// Related header
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <memory>
7#include <vector>
8
9// Third-party library headers
10#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
14
15// Project headers
19#include "app/gui/core/icons.h"
23#include "rom/rom.h"
32
33namespace yaze {
34namespace editor {
35
37 gfx::IRenderer* renderer, Rom* rom, DungeonCanvasViewer* canvas_viewer,
38 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor)
39 : renderer_(renderer),
40 rom_(rom),
41 canvas_viewer_(canvas_viewer),
42 object_selector_(rom),
43 object_editor_(object_editor) {
44 emulator_preview_.Initialize(renderer, rom);
45
46 // Initialize object parser for static editor info lookup
47 if (rom) {
48 object_parser_ = std::make_unique<zelda3::ObjectParser>(rom);
49 }
50
51 // Wire up object selector callback
53 [this](const zelda3::RoomObject& obj) {
54 preview_object_ = obj;
56 if (canvas_viewer_) {
59 }
60
61 // Sync with backend editor if available
62 if (object_editor_) {
64 object_editor_->SetCurrentObjectType(obj.id_);
65 }
66 });
67
68 // Wire up double-click callback for static object editor
70 [this](int obj_id) { OpenStaticObjectEditor(obj_id); });
71
72 // Wire up selection change callback for property panel sync
74}
75
85
87 if (!canvas_viewer_)
88 return;
89
90 auto& interaction = canvas_viewer_->object_interaction();
92
93 // Sync with backend editor if available
94 if (object_editor_) {
95 auto indices = interaction.GetSelectedObjectIndices();
96 // Clear backend selection first
97 (void)object_editor_->ClearSelection();
98
99 // Add each selected index to backend
100 for (size_t idx : indices) {
101 (void)object_editor_->AddToSelection(idx);
102 }
103 }
104}
105
106void ObjectEditorPanel::Draw(bool* p_open) {
107 const auto& theme = AgentUI::GetTheme();
108 const int max_objects = static_cast<int>(zelda3::kMaxTileObjects);
109 const int max_sprites = static_cast<int>(zelda3::kMaxTotalSprites);
110 const int max_doors = static_cast<int>(zelda3::kMaxDoors);
111
112 // Check if placement was blocked by ROM limits
113 if (canvas_viewer_) {
114 auto& coordinator =
116
117 auto& tile_handler = coordinator.tile_handler();
118 if (tile_handler.was_placement_blocked()) {
119 const auto reason = tile_handler.placement_block_reason();
120 tile_handler.clear_placement_blocked();
121 switch (reason) {
123 SetPlacementError(absl::StrFormat(
124 "Object limit reached (%d max) - placement blocked",
125 max_objects));
126 break;
128 SetPlacementError("Invalid room target - placement blocked");
129 break;
131 default:
132 SetPlacementError("Object placement blocked");
133 break;
134 }
135 }
136
137 auto& sprite_handler = coordinator.sprite_handler();
138 if (sprite_handler.was_placement_blocked()) {
139 const auto reason = sprite_handler.placement_block_reason();
140 sprite_handler.clear_placement_blocked();
141 switch (reason) {
143 SetPlacementError(absl::StrFormat(
144 "Sprite limit reached (%d max) - placement blocked",
145 max_sprites));
146 break;
148 SetPlacementError("Invalid room target - sprite placement blocked");
149 break;
151 default:
152 SetPlacementError("Sprite placement blocked");
153 break;
154 }
155 }
156
157 auto& door_handler = coordinator.door_handler();
158 if (door_handler.was_placement_blocked()) {
159 const auto reason = door_handler.placement_block_reason();
160 door_handler.clear_placement_blocked();
161 switch (reason) {
163 SetPlacementError(absl::StrFormat(
164 "Door limit reached (%d max) - placement blocked", max_doors));
165 break;
167 SetPlacementError("Invalid door position - must be near a wall");
168 break;
170 SetPlacementError("Invalid room target - door placement blocked");
171 break;
173 default:
174 SetPlacementError("Door placement blocked");
175 break;
176 }
177 }
178 }
179
180 // Room validation bar (object/sprite/door/chest counts)
182
183 // Door Section (Collapsible)
184 if (ImGui::CollapsingHeader(ICON_MD_DOOR_FRONT " Doors",
185 ImGuiTreeNodeFlags_DefaultOpen)) {
187 }
188
189 ImGui::Separator();
190
191 // Object Browser - takes up available space
192 float available_height = ImGui::GetContentRegionAvail().y;
193 // Reserve space for status indicator at bottom
194 float reserved_height = 60.0f;
195 // Reduce browser height when static editor is open to give it more space
197 reserved_height += 200.0f;
198 }
199 float browser_height = std::max(150.0f, available_height - reserved_height);
200
201 ImGui::BeginChild("ObjectBrowserRegion", ImVec2(0, browser_height), true);
203 ImGui::EndChild();
204
205 ImGui::Separator();
206
207 // Static Object Editor (if open)
210 ImGui::Separator();
211 }
212
213 // Status indicator: show current interaction state
214 {
215 bool is_placing = has_preview_object_ && canvas_viewer_ &&
217 if (!is_placing && has_preview_object_) {
218 has_preview_object_ = false;
219 }
220 if (is_placing) {
221 ImGui::TextColored(theme.status_warning,
222 ICON_MD_ADD_CIRCLE " Placing: Object 0x%02X",
224 ImGui::SameLine();
225 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel")) {
227 }
228 } else {
229 ImGui::TextColored(
230 theme.text_secondary_gray, ICON_MD_MOUSE
231 " Selection Mode - Click to select, drag to multi-select");
232 ImGui::SameLine();
233 if (ImGui::SmallButton(ICON_MD_HELP_OUTLINE " ?")) {
234 show_shortcut_help_ = true;
235 }
236 }
237 }
238
239 // Placement error feedback (timed)
240 if (!last_placement_error_.empty()) {
241 double elapsed = ImGui::GetTime() - placement_error_time_;
242 if (elapsed < kPlacementErrorDuration) {
243 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
244 last_placement_error_.c_str());
245 } else {
246 last_placement_error_.clear();
247 }
248 }
249
250 // Current object info
252
253 ImGui::Separator();
254
255 // Emulator Preview (Collapsible)
256 bool preview_open = ImGui::CollapsingHeader(ICON_MD_MONITOR " Preview");
257 show_emulator_preview_ = preview_open;
258
259 if (preview_open) {
260 ImGui::PushID("PreviewSection");
262 ImGui::PopID();
263 }
264
265 // Keyboard shortcut help popup
267
268 // Handle keyboard shortcuts
270}
271
275
277 // In agent mode, we might force tabs open or change layout
278 (void)enabled;
279}
280
281void ObjectEditorPanel::SetPlacementError(const std::string& message) {
282 // Avoid refreshing the timer for repeated identical errors; keeps the
283 // message stable during rapid blocked clicks.
284 if (message == last_placement_error_ && placement_error_time_ >= 0.0) {
285 double elapsed = ImGui::GetTime() - placement_error_time_;
286 if (elapsed < kPlacementErrorDuration) {
287 return;
288 }
289 }
290 last_placement_error_ = message;
291 placement_error_time_ = ImGui::GetTime();
292}
293
295 // Delegate to the DungeonObjectSelector component
297}
298
300 const auto& theme = AgentUI::GetTheme();
301
302 // Common door types for the grid
303 static constexpr std::array<zelda3::DoorType, 20> kDoorTypes = {{
324 }};
325
326 // Placement mode indicator
328 ImGui::TextColored(
329 theme.status_warning,
330 ICON_MD_PLACE " Placing: %s - Click wall to place",
331 std::string(zelda3::GetDoorTypeName(selected_door_type_)).c_str());
332 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel")) {
333 door_placement_mode_ = false;
334 if (canvas_viewer_) {
337 }
338 }
339 ImGui::Separator();
340 }
341
342 // Door type selector grid with preview thumbnails
343 ImGui::Text(ICON_MD_CATEGORY " Select Door Type:");
344
345 constexpr float kPreviewSize = 32.0f;
346 constexpr int kItemsPerRow = 5;
347 float panel_width = ImGui::GetContentRegionAvail().x;
348 int items_per_row =
349 std::max(1, static_cast<int>(panel_width / (kPreviewSize + 8)));
350
351 ImGui::BeginChild("##DoorTypeGrid", ImVec2(0, 80), true,
352 ImGuiWindowFlags_HorizontalScrollbar);
353
354 int col = 0;
355 for (size_t i = 0; i < kDoorTypes.size(); ++i) {
356 auto door_type = kDoorTypes[i];
357 bool is_selected = (selected_door_type_ == door_type);
358
359 ImGui::PushID(static_cast<int>(i));
360
361 // Color-coded button for door type
362 ImVec4 button_color;
363 // Color-code by door category
364 int type_val = static_cast<int>(door_type);
365 if (type_val <= 0x12) { // Standard doors
366 button_color = ImVec4(0.3f, 0.5f, 0.7f, 1.0f); // Blue
367 } else if (type_val <= 0x1E) { // Shutter/special
368 button_color = ImVec4(0.7f, 0.5f, 0.3f, 1.0f); // Orange
369 } else { // Markers
370 button_color = ImVec4(0.5f, 0.7f, 0.3f, 1.0f); // Green
371 }
372
373 if (is_selected) {
374 button_color.x += 0.2f;
375 button_color.y += 0.2f;
376 button_color.z += 0.2f;
377 }
378
379 {
380 gui::StyleColorGuard btn_colors({
381 {ImGuiCol_Button, button_color},
382 {ImGuiCol_ButtonHovered,
383 ImVec4(button_color.x + 0.1f, button_color.y + 0.1f,
384 button_color.z + 0.1f, 1.0f)},
385 {ImGuiCol_ButtonActive,
386 ImVec4(button_color.x + 0.2f, button_color.y + 0.2f,
387 button_color.z + 0.2f, 1.0f)},
388 });
389
390 // Draw button with door type abbreviation
391 std::string label = absl::StrFormat("%02X", type_val);
392 if (ImGui::Button(label.c_str(), ImVec2(kPreviewSize, kPreviewSize))) {
393 selected_door_type_ = door_type;
395 if (canvas_viewer_) {
397 true, selected_door_type_);
398 }
399 }
400 }
401
402 // Tooltip with full name
403 if (ImGui::IsItemHovered()) {
404 ImGui::SetTooltip("%s (0x%02X)\nClick to select for placement",
405 std::string(zelda3::GetDoorTypeName(door_type)).c_str(),
406 type_val);
407 }
408
409 // Selection highlight
410 if (is_selected) {
411 ImVec2 min = ImGui::GetItemRectMin();
412 ImVec2 max = ImGui::GetItemRectMax();
413 ImGui::GetWindowDrawList()->AddRect(min, max, IM_COL32(255, 255, 0, 255),
414 0.0f, 0, 2.0f);
415 }
416
417 ImGui::PopID();
418
419 col++;
420 if (col < items_per_row && i < kDoorTypes.size() - 1) {
421 ImGui::SameLine();
422 } else {
423 col = 0;
424 }
425 }
426
427 ImGui::EndChild();
428
429 // Show current room's doors
430 auto* rooms = object_selector_.get_rooms();
431 if (rooms && current_room_id_ >= 0 && current_room_id_ < 296) {
432 const auto& room = (*rooms)[current_room_id_];
433 const auto& doors = room.GetDoors();
434
435 if (!doors.empty()) {
436 ImGui::Text(ICON_MD_LIST " Room Doors (%zu):", doors.size());
437
438 ImGui::BeginChild("##DoorList", ImVec2(0, 80), true);
439 for (size_t i = 0; i < doors.size(); ++i) {
440 const auto& door = doors[i];
441 auto [tile_x, tile_y] = door.GetTileCoords();
442
443 ImGui::PushID(static_cast<int>(i));
444
445 std::string type_name(zelda3::GetDoorTypeName(door.type));
446 std::string dir_name(zelda3::GetDoorDirectionName(door.direction));
447
448 ImGui::Text("[%zu] %s (%s)", i, type_name.c_str(), dir_name.c_str());
449 ImGui::SameLine();
450 ImGui::TextColored(theme.text_secondary_gray, "@ (%d,%d)", tile_x,
451 tile_y);
452
453 ImGui::SameLine();
454 if (ImGui::SmallButton(ICON_MD_DELETE "##Del")) {
455 auto& mutable_room = (*rooms)[current_room_id_];
456 mutable_room.RemoveDoor(i);
457 }
458
459 ImGui::PopID();
460 }
461 ImGui::EndChild();
462 } else {
463 ImGui::TextColored(theme.text_secondary_gray,
464 ICON_MD_INFO " No doors in this room");
465 }
466 }
467}
468
470 const auto& theme = AgentUI::GetTheme();
471
472 ImGui::TextColored(theme.text_secondary_gray,
473 ICON_MD_INFO " Real-time object rendering preview");
475 "Uses SNES emulation to render objects accurately.\n"
476 "May impact performance.");
477
478 ImGui::Separator();
479
480 ImGui::BeginChild("##EmulatorPreviewRegion", ImVec2(0, 260), true);
482 ImGui::EndChild();
483}
484
486 const auto& theme = AgentUI::GetTheme();
487
488 // Show selection state at top - with extra safety checks
490 auto& interaction = canvas_viewer_->object_interaction();
491 auto selected = interaction.GetSelectedObjectIndices();
492
493 if (!selected.empty()) {
494 ImGui::TextColored(theme.status_success,
495 ICON_MD_CHECK_CIRCLE " Selected:");
496 ImGui::SameLine();
497
498 if (selected.size() == 1) {
499 if (object_editor_) {
500 const auto& objects = object_editor_->GetObjects();
501 if (selected[0] < objects.size()) {
502 const auto& obj = objects[selected[0]];
503 const auto semantics = zelda3::GetObjectLayerSemantics(obj);
504 ImGui::Text("Object #%zu (ID: 0x%02X)", selected[0], obj.id_);
505 ImGui::TextColored(
506 theme.text_secondary_gray,
507 " Position: (%d, %d) Size: 0x%02X Layer: %s Draws: %s",
508 obj.x_, obj.y_, obj.size_,
509 obj.layer_ == zelda3::RoomObject::BG1 ? "BG1"
510 : obj.layer_ == zelda3::RoomObject::BG2 ? "BG2"
511 : "BG3",
512 zelda3::EffectiveBgLayerLabel(semantics.effective_bg_layer));
513 }
514 } else {
515 ImGui::Text("1 object");
516 }
517 } else {
518 ImGui::Text("%zu objects", selected.size());
519 ImGui::SameLine();
520 ImGui::TextColored(theme.text_secondary_gray,
521 "(Shift+click to add, Ctrl+click to toggle)");
522 }
523 ImGui::Separator();
524 }
525 }
526
527 ImGui::BeginGroup();
528
529 ImGui::TextColored(theme.text_info, ICON_MD_INFO " Current:");
530
532 ImGui::SameLine();
533 const auto semantics = zelda3::GetObjectLayerSemantics(preview_object_);
534 ImGui::Text("ID: 0x%02X", preview_object_.id_);
535 ImGui::SameLine();
536 ImGui::Text("Layer: %s Draws: %s",
539 : "BG3",
540 zelda3::EffectiveBgLayerLabel(semantics.effective_bg_layer));
541 }
542
543 ImGui::EndGroup();
544
545 ImGui::Separator();
546
547 // Delegate property editing to the backend
548 if (object_editor_) {
549 object_editor_->DrawPropertyUI();
550 }
551}
552
553// =============================================================================
554// Static Object Editor (opened via double-click)
555// =============================================================================
556
558 static_editor_open_ = true;
559 static_editor_object_id_ = object_id;
561
562 // Sync with object selector for visual indicator
564
565 // Fetch draw routine info for this object
566 if (object_parser_) {
567 static_editor_draw_info_ = object_parser_->GetObjectDrawInfo(object_id);
568 }
569
570 // Render the object preview using ObjectDrawer
571 auto* rooms = object_selector_.get_rooms();
572 if (rom_ && rom_->is_loaded() && rooms && current_room_id_ >= 0 &&
573 current_room_id_ < static_cast<int>(rooms->size())) {
574 auto& room = (*rooms)[current_room_id_];
575
576 // Ensure room graphics are loaded
577 if (!room.IsLoaded()) {
578 room.LoadRoomGraphics(room.blockset());
579 }
580
581 // Clear preview buffer and initialize bitmap
584
585 // Create a preview object at top-left of canvas (tile 2,2 = pixel 16,16)
586 // to fit within the 128x128 preview area with some margin
587 zelda3::RoomObject preview_obj(object_id, 2, 2, 0x12, 0);
588 preview_obj.SetRom(rom_);
589 preview_obj.EnsureTilesLoaded();
590
591 if (preview_obj.tiles().empty()) {
592 return; // No tiles to draw
593 }
594
595 // Get room graphics data
596 const uint8_t* gfx_data = room.get_gfx_buffer().data();
597
598 // Apply palette to bitmap
599 auto& bitmap = static_preview_buffer_.bitmap();
600 gfx::PaletteGroup palette_group;
601 auto* game_data = object_selector_.game_data();
602 if (game_data && !game_data->palette_groups.dungeon_main.empty()) {
603 // Use the entire dungeon_main palette group
604 palette_group = game_data->palette_groups.dungeon_main;
605
606 std::vector<SDL_Color> colors(256);
607 size_t color_index = 0;
608 for (size_t pal_idx = 0;
609 pal_idx < palette_group.size() && color_index < 256; ++pal_idx) {
610 const auto& pal = palette_group[pal_idx];
611 for (size_t i = 0; i < pal.size() && color_index < 256; ++i) {
612 ImVec4 rgb = pal[i].rgb();
613 colors[color_index++] = {static_cast<Uint8>(rgb.x),
614 static_cast<Uint8>(rgb.y),
615 static_cast<Uint8>(rgb.z), 255};
616 }
617 }
618 colors[255] = {0, 0, 0, 0}; // Transparent
619 bitmap.SetPalette(colors);
620 if (bitmap.surface()) {
621 SDL_SetColorKey(bitmap.surface(), SDL_TRUE, 255);
622 SDL_SetSurfaceBlendMode(bitmap.surface(), SDL_BLENDMODE_BLEND);
623 }
624 }
625
626 // Create drawer with room's graphics data
627 zelda3::ObjectDrawer drawer(rom_, current_room_id_, gfx_data);
628 drawer.InitializeDrawRoutines();
629
630 auto status = drawer.DrawObject(preview_obj, static_preview_buffer_,
631 static_preview_buffer_, palette_group);
632 if (status.ok()) {
633 // Sync bitmap data to SDL surface
634 if (bitmap.modified() && bitmap.surface() &&
635 bitmap.mutable_data().size() > 0) {
636 SDL_LockSurface(bitmap.surface());
637 size_t surface_size = bitmap.surface()->h * bitmap.surface()->pitch;
638 size_t data_size = bitmap.mutable_data().size();
639 if (surface_size >= data_size) {
640 memcpy(bitmap.surface()->pixels, bitmap.mutable_data().data(),
641 data_size);
642 }
643 SDL_UnlockSurface(bitmap.surface());
644 }
645
646 // Create texture
650
651 static_preview_rendered_ = bitmap.texture() != nullptr;
652 }
653 }
654}
655
657 static_editor_open_ = false;
659
660 // Clear the visual indicator in object selector
662}
663
665 const auto& theme = AgentUI::GetTheme();
666
667 gui::StyleColorGuard header_colors({
668 {ImGuiCol_Header, ImVec4(0.15f, 0.25f, 0.35f, 1.0f)},
669 {ImGuiCol_HeaderHovered, ImVec4(0.20f, 0.30f, 0.40f, 1.0f)},
670 });
671
672 bool header_open = ImGui::CollapsingHeader(
673 absl::StrFormat(ICON_MD_CONSTRUCTION " Object 0x%02X - %s",
676 .c_str(),
677 ImGuiTreeNodeFlags_DefaultOpen);
678
679 if (header_open) {
680 gui::StyleVarGuard frame_pad_guard(ImGuiStyleVar_FramePadding,
681 ImVec2(8, 6));
682
683 // Two-column layout: Info | Preview
684 if (ImGui::BeginTable("StaticEditorLayout", 2,
685 ImGuiTableFlags_BordersInnerV)) {
686 ImGui::TableSetupColumn("Info", ImGuiTableColumnFlags_WidthFixed, 200);
687 ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch);
688
689 ImGui::TableNextRow();
690
691 // Left column: Object information
692 ImGui::TableNextColumn();
693 {
694 // Object ID with hex/decimal display
695 ImGui::TextColored(theme.text_info, ICON_MD_TAG " Object ID");
696 ImGui::SameLine();
697 ImGui::Text("0x%02X (%d)", static_editor_object_id_,
699
700 ImGui::Spacing();
701
702 // Draw routine info
703 ImGui::TextColored(theme.text_info, ICON_MD_BRUSH " Draw Routine");
704 ImGui::Indent();
705 ImGui::Text("ID: %d", static_editor_draw_info_.draw_routine_id);
706 ImGui::Text("Name: %s", static_editor_draw_info_.routine_name.c_str());
707 ImGui::Unindent();
708
709 ImGui::Spacing();
710
711 // Tile and size info
712 ImGui::TextColored(theme.text_info, ICON_MD_GRID_VIEW " Tile Info");
713 ImGui::Indent();
714 ImGui::Text("Tile Count: %d", static_editor_draw_info_.tile_count);
715 ImGui::Text("Orientation: %s",
718 : "Both");
720 ImGui::TextColored(theme.status_warning, ICON_MD_LAYERS " Both BG");
721 }
722 ImGui::Unindent();
723
724 ImGui::Spacing();
725 ImGui::Separator();
726 ImGui::Spacing();
727
728 // Action buttons (vertical layout)
729 if (ImGui::Button(ICON_MD_CONTENT_COPY " Copy ID", ImVec2(-1, 0))) {
730 ImGui::SetClipboardText(
731 absl::StrFormat("0x%02X", static_editor_object_id_).c_str());
732 }
733 if (ImGui::IsItemHovered()) {
734 ImGui::SetTooltip("Copy object ID to clipboard");
735 }
736
737 if (ImGui::Button(ICON_MD_CODE " Export ASM", ImVec2(-1, 0))) {
738 // TODO: Implement ASM export (Phase 5)
739 }
740 if (ImGui::IsItemHovered()) {
741 ImGui::SetTooltip("Export object draw routine as ASM (Phase 5)");
742 }
743
744 if (ImGui::Button(ICON_MD_GRID_ON " Edit Tiles", ImVec2(-1, 0))) {
747 static_cast<int16_t>(static_editor_object_id_));
748 }
749 }
750 if (ImGui::IsItemHovered()) {
751 ImGui::SetTooltip("Open tile editor to rearrange 8x8 tiles");
752 }
753
754 ImGui::Spacing();
755
756 // Close button at bottom
757 if (gui::DangerButton(ICON_MD_CLOSE " Close", ImVec2(-1, 0))) {
759 }
760 }
761
762 // Right column: Preview canvas
763 ImGui::TableNextColumn();
764 {
765 ImGui::TextColored(theme.text_secondary_gray, "Preview:");
766
767 gui::PreviewPanelOpts preview_opts;
768 preview_opts.canvas_size = ImVec2(128, 128);
769 preview_opts.render_popups = false;
770 preview_opts.grid_step = 0.0f;
771 preview_opts.ensure_texture = true;
772
773 gui::CanvasFrameOptions frame_opts;
774 frame_opts.canvas_size = preview_opts.canvas_size;
775 frame_opts.draw_context_menu = false;
776 frame_opts.draw_grid = preview_opts.grid_step > 0.0f;
777 if (preview_opts.grid_step > 0.0f) {
778 frame_opts.grid_step = preview_opts.grid_step;
779 }
780 frame_opts.draw_overlay = true;
781 frame_opts.render_popups = preview_opts.render_popups;
782
783 auto rt = gui::BeginCanvas(static_preview_canvas_, frame_opts);
784
786 auto& bitmap = static_preview_buffer_.bitmap();
787 gui::RenderPreviewPanel(rt, bitmap, preview_opts);
788 } else {
790 preview_opts);
792 ImVec2(24, 56), "No preview available",
793 ImGui::GetColorU32(theme.text_secondary_gray));
794 }
795 gui::EndCanvas(static_preview_canvas_, rt, frame_opts);
796
797 // Usage hint
798 ImGui::Spacing();
799 ImGui::TextColored(theme.text_secondary_gray, ICON_MD_INFO
800 " Double-click objects in browser\n"
801 "to view their draw routine info.");
802 }
803
804 ImGui::EndTable();
805 }
806 }
807}
808
809// =============================================================================
810// Room Validation Bar
811// =============================================================================
812
814 auto* rooms = object_selector_.get_rooms();
815 if (!rooms || current_room_id_ < 0 ||
817 return;
818 }
819
820 const auto& theme = AgentUI::GetTheme();
821 const auto& room = (*rooms)[current_room_id_];
822
823 // Gather counts
824 size_t object_count = room.GetTileObjects().size();
825 size_t sprite_count = room.GetSprites().size();
826 size_t door_count = room.GetDoors().size();
827
828 // Count chests (objects in 0xF9-0xFD range)
829 int chest_count = 0;
830 for (const auto& obj : room.GetTileObjects()) {
831 if (obj.id_ >= 0xF9 && obj.id_ <= 0xFD) {
832 chest_count++;
833 }
834 }
835
836 // Canonical limits shared across handlers/validator.
837 const int kMaxObjects = static_cast<int>(zelda3::kMaxTileObjects);
838 const int kMaxSprites = static_cast<int>(zelda3::kMaxTotalSprites);
839 const int kMaxDoors = static_cast<int>(zelda3::kMaxDoors);
840 const int kMaxChests = static_cast<int>(zelda3::kMaxChests);
841
842 // Helper to pick color based on usage ratio
843 auto usage_color = [&](size_t count, int max_val) -> ImVec4 {
844 float ratio = static_cast<float>(count) / static_cast<float>(max_val);
845 if (ratio >= 1.0f) {
846 return theme.status_error;
847 }
848 if (ratio >= 0.75f) {
849 return theme.status_warning;
850 }
851 return theme.text_secondary_gray;
852 };
853
854 // Compact inline counters
855 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
856
857 ImGui::TextColored(usage_color(object_count, kMaxObjects),
858 ICON_MD_WIDGETS " %zu/%d", object_count, kMaxObjects);
859 if (ImGui::IsItemHovered()) {
860 ImGui::SetTooltip("Objects: %zu of %d maximum", object_count, kMaxObjects);
861 }
862
863 ImGui::SameLine();
864 ImGui::TextColored(usage_color(sprite_count, kMaxSprites),
865 ICON_MD_PEST_CONTROL " %zu/%d", sprite_count, kMaxSprites);
866 if (ImGui::IsItemHovered()) {
867 ImGui::SetTooltip("Sprites: %zu of %d maximum", sprite_count, kMaxSprites);
868 }
869
870 ImGui::SameLine();
871 ImGui::TextColored(usage_color(door_count, kMaxDoors),
872 ICON_MD_DOOR_FRONT " %zu/%d", door_count, kMaxDoors);
873 if (ImGui::IsItemHovered()) {
874 ImGui::SetTooltip("Doors: %zu of %d maximum", door_count, kMaxDoors);
875 }
876
877 ImGui::SameLine();
878 ImGui::TextColored(usage_color(chest_count, kMaxChests),
879 ICON_MD_INVENTORY_2 " %d/%d", chest_count, kMaxChests);
880 if (ImGui::IsItemHovered()) {
881 ImGui::SetTooltip("Chests: %d of %d maximum", chest_count, kMaxChests);
882 }
883
884 ImGui::PopStyleVar();
885
886 // Run full validation and show warnings/errors inline
887 zelda3::DungeonValidator validator;
888 auto result = validator.ValidateRoom(room);
889 if (!result.errors.empty() || !result.warnings.empty()) {
890 for (const auto& err : result.errors) {
891 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s", err.c_str());
892 }
893 for (const auto& warn : result.warnings) {
894 ImGui::TextColored(theme.status_warning, ICON_MD_WARNING " %s",
895 warn.c_str());
896 }
897 }
898
899 ImGui::Separator();
900}
901
902// =============================================================================
903// Keyboard Shortcut Help
904// =============================================================================
905
907 if (!show_shortcut_help_) {
908 return;
909 }
910
911 ImGui::SetNextWindowSize(ImVec2(340, 0), ImGuiCond_Appearing);
912 if (ImGui::Begin("Keyboard Shortcuts##ObjEditor", &show_shortcut_help_,
913 ImGuiWindowFlags_NoCollapse)) {
914 const auto& theme = AgentUI::GetTheme();
915
916 auto shortcut_row = [&](const char* keys, const char* desc) {
917 ImGui::TextColored(theme.status_warning, "%-18s", keys);
918 ImGui::SameLine();
919 ImGui::TextUnformatted(desc);
920 };
921
922 ImGui::TextColored(theme.status_success, ICON_MD_KEYBOARD " Selection");
923 ImGui::Separator();
924 shortcut_row("Ctrl+A", "Select all objects");
925 shortcut_row("Ctrl+Shift+A", "Deselect all");
926 shortcut_row("Tab / Shift+Tab", "Cycle selection");
927 shortcut_row("Escape", "Cancel placement / deselect");
928
929 ImGui::Spacing();
930 ImGui::TextColored(theme.status_success, ICON_MD_EDIT " Editing");
931 ImGui::Separator();
932 shortcut_row("Delete", "Remove selected");
933 shortcut_row("Ctrl+D", "Duplicate selected");
934 shortcut_row("Ctrl+C", "Copy selected");
935 shortcut_row("Ctrl+V", "Paste");
936 shortcut_row("Ctrl+Z", "Undo");
937 shortcut_row("Ctrl+Shift+Z", "Redo");
938
939 ImGui::Spacing();
940 ImGui::TextColored(theme.status_success, ICON_MD_OPEN_WITH " Movement");
941 ImGui::Separator();
942 shortcut_row("Arrow Keys", "Nudge selected (1px)");
943
944 ImGui::Spacing();
945 ImGui::TextColored(theme.status_success, ICON_MD_VISIBILITY " Display");
946 ImGui::Separator();
947 shortcut_row("G", "Toggle grid");
948 shortcut_row("I", "Toggle object ID labels");
949 shortcut_row("?", "Show this help");
950 }
951 ImGui::End();
952}
953
954// =============================================================================
955// Keyboard Shortcuts
956// =============================================================================
957
959 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
960 return;
961 }
962
963 const ImGuiIO& io = ImGui::GetIO();
964
965 // Ctrl+A: Select all objects
966 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
968 }
969
970 // Ctrl+Shift+A: Deselect all
971 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
973 }
974
975 // Delete: Remove selected objects
976 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
978 }
979
980 // Ctrl+D: Duplicate selected objects
981 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
983 }
984
985 // Ctrl+C: Copy selected objects
986 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
988 }
989
990 // Ctrl+V: Paste objects
991 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
992 PasteObjects();
993 }
994
995 // Ctrl+Z: Undo
996 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
997 if (object_editor_) {
998 object_editor_->Undo();
999 }
1000 }
1001
1002 // Ctrl+Shift+Z or Ctrl+Y: Redo
1003 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
1004 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
1005 if (object_editor_) {
1006 object_editor_->Redo();
1007 }
1008 }
1009
1010 // G: Toggle grid
1011 if (ImGui::IsKeyPressed(ImGuiKey_G) && !io.KeyCtrl) {
1013 }
1014
1015 // I: Toggle object ID labels
1016 if (ImGui::IsKeyPressed(ImGuiKey_I) && !io.KeyCtrl) {
1018 }
1019
1020 // Arrow keys: Nudge selected objects
1021 if (!io.KeyCtrl) {
1022 int dx = 0, dy = 0;
1023 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow))
1024 dx = -1;
1025 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow))
1026 dx = 1;
1027 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow))
1028 dy = -1;
1029 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow))
1030 dy = 1;
1031
1032 if (dx != 0 || dy != 0) {
1033 NudgeSelectedObjects(dx, dy);
1034 }
1035 }
1036
1037 // Tab: Cycle through objects
1038 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
1039 CycleObjectSelection(io.KeyShift ? -1 : 1);
1040 }
1041
1042 // Escape: Cancel placement or deselect all
1043 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
1047 } else {
1049 }
1050 }
1051
1052 // ?: Toggle shortcut help (Shift+/ on US keyboards)
1053 if (ImGui::IsKeyPressed(ImGuiKey_Slash) && io.KeyShift) {
1055 }
1056}
1057
1065
1068 return;
1069
1070 auto& interaction = canvas_viewer_->object_interaction();
1071 const auto& objects = object_editor_->GetObjects();
1072 std::vector<size_t> all_indices;
1073
1074 for (size_t i = 0; i < objects.size(); ++i) {
1075 all_indices.push_back(i);
1076 }
1077
1078 interaction.SetSelectedObjects(all_indices);
1079}
1080
1086
1089 return;
1090
1091 auto& interaction = canvas_viewer_->object_interaction();
1092 const auto& selected = interaction.GetSelectedObjectIndices();
1093
1094 if (selected.empty())
1095 return;
1096
1097 // Show confirmation for bulk delete (more than 5 objects)
1098 if (selected.size() > 5) {
1100 return;
1101 }
1102
1103 PerformDelete();
1104}
1105
1108 return;
1109
1110 auto& interaction = canvas_viewer_->object_interaction();
1111 const auto& selected = interaction.GetSelectedObjectIndices();
1112
1113 if (selected.empty())
1114 return;
1115
1116 // Delete in reverse order to maintain indices
1117 std::vector<size_t> sorted_indices(selected.begin(), selected.end());
1118 std::sort(sorted_indices.rbegin(), sorted_indices.rend());
1119
1120 for (size_t idx : sorted_indices) {
1121 object_editor_->DeleteObject(idx);
1122 }
1123
1124 interaction.ClearSelection();
1125}
1126
1129 return;
1130
1131 auto& interaction = canvas_viewer_->object_interaction();
1132 const auto& selected = interaction.GetSelectedObjectIndices();
1133
1134 if (selected.empty())
1135 return;
1136
1137 std::vector<size_t> new_indices;
1138
1139 for (size_t idx : selected) {
1140 auto new_idx = object_editor_->DuplicateObject(idx, 1, 1);
1141 if (new_idx.has_value()) {
1142 new_indices.push_back(*new_idx);
1143 }
1144 }
1145
1146 interaction.SetSelectedObjects(new_indices);
1147}
1148
1151 return;
1152
1153 auto& interaction = canvas_viewer_->object_interaction();
1154 const auto& selected = interaction.GetSelectedObjectIndices();
1155
1156 if (selected.empty())
1157 return;
1158
1159 object_editor_->CopySelectedObjects(selected);
1160}
1161
1164 return;
1165
1166 auto new_indices = object_editor_->PasteObjects();
1167
1168 if (!new_indices.empty()) {
1170 }
1171}
1172
1175 return;
1176
1177 auto& interaction = canvas_viewer_->object_interaction();
1178 const auto& selected = interaction.GetSelectedObjectIndices();
1179
1180 if (selected.empty())
1181 return;
1182
1183 for (size_t idx : selected) {
1184 object_editor_->MoveObject(idx, dx, dy);
1185 }
1186}
1187
1190 return;
1191
1192 auto& interaction = canvas_viewer_->object_interaction();
1193 const auto& selected = interaction.GetSelectedObjectIndices();
1194 const auto& objects = object_editor_->GetObjects();
1195
1196 size_t total_objects = objects.size();
1197 if (total_objects == 0)
1198 return;
1199
1200 size_t current_idx = selected.empty() ? 0 : selected.front();
1201 size_t next_idx = (current_idx + direction + total_objects) % total_objects;
1202
1203 interaction.SetSelectedObjects({next_idx});
1204 ScrollToObject(next_idx);
1205}
1206
1209 return;
1210
1211 const auto& objects = object_editor_->GetObjects();
1212 if (index >= objects.size())
1213 return;
1214
1215 const auto& obj = objects[index];
1216 canvas_viewer_->ScrollToTile(obj.x(), obj.y());
1217}
1218
1219} // namespace editor
1220} // namespace yaze
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()
void SetPreviewObject(const zelda3::RoomObject &object)
void ScrollToTile(int tile_x, int tile_y)
void SetSelectedObjects(const std::vector< size_t > &indices)
std::vector< size_t > GetSelectedObjectIndices() const
void SetSelectionChangeCallback(std::function< void()> callback)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SetDoorPlacementMode(bool enabled, zelda3::DoorType type=zelda3::DoorType::NormalDoor)
void SetObjectDoubleClickCallback(std::function< void(int)> callback)
std::array< zelda3::Room, 0x128 > * get_rooms()
void SelectObject(int obj_id, int subtype=-1)
void SetObjectSelectedCallback(std::function< void(const zelda3::RoomObject &)> callback)
void SetPlacementError(const std::string &message)
ObjectEditorPanel(gfx::IRenderer *renderer, Rom *rom, DungeonCanvasViewer *canvas_viewer, std::shared_ptr< zelda3::DungeonObjectEditor > object_editor=nullptr)
static constexpr double kPlacementErrorDuration
std::unique_ptr< zelda3::ObjectParser > object_parser_
gui::DungeonObjectEmulatorPreview emulator_preview_
gfx::BackgroundBuffer static_preview_buffer_
void Draw(bool *p_open) override
Draw the panel content.
std::shared_ptr< zelda3::DungeonObjectEditor > object_editor_
zelda3::ObjectDrawInfo static_editor_draw_info_
PlacementBlockReason placement_block_reason() const
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
static Arena & Get()
Definition arena.cc:21
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
void AddTextAt(ImVec2 local_pos, const std::string &text, uint32_t color)
Definition canvas.cc:2507
void Initialize(gfx::IRenderer *renderer, Rom *rom, zelda3::GameData *game_data=nullptr, emu::render::EmulatorRenderService *render_service=nullptr)
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
ValidationResult ValidateRoom(const Room &room)
Draws dungeon objects to background buffers using game patterns.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr)
Draw a room object to background buffers.
const std::vector< gfx::TileInfo > & tiles() const
Definition room_object.h:91
void SetRom(Rom *rom)
Definition room_object.h:70
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_MONITOR
Definition icons.h:1233
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_PLACE
Definition icons.h:1477
#define ICON_MD_CONSTRUCTION
Definition icons.h:458
#define ICON_MD_BRUSH
Definition icons.h:325
#define ICON_MD_OPEN_WITH
Definition icons.h:1356
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_PEST_CONTROL
Definition icons.h:1429
#define ICON_MD_HELP_OUTLINE
Definition icons.h:935
#define ICON_MD_MOUSE
Definition icons.h:1251
#define ICON_MD_DELETE
Definition icons.h:530
#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_TAG
Definition icons.h:1940
#define ICON_MD_ADD_CIRCLE
Definition icons.h:95
const AgentUITheme & GetTheme()
void EndCanvas(Canvas &canvas)
Definition canvas.cc:1591
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1568
bool DangerButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a danger action button (error color).
void HelpMarker(const char *desc)
bool RenderPreviewPanel(const CanvasRuntime &rt, gfx::Bitmap &bmp, const PreviewPanelOpts &opts)
Definition canvas.cc:2238
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
@ FancyDungeonExit
Fancy dungeon exit.
@ SmallKeyDoor
Small key door.
@ SmallKeyStairsDown
Small key stairs (downwards)
@ SmallKeyStairsUp
Small key stairs (upwards)
@ DungeonSwapMarker
Dungeon swap marker.
@ NormalDoor
Normal door (upper layer)
@ BombableDoor
Bombable door.
@ LayerSwapMarker
Layer swap marker.
@ ExplodingWall
Exploding wall.
@ TopSidedShutter
Top-sided shutter door.
@ NormalDoorLower
Normal door (lower layer)
@ BottomSidedShutter
Bottom-sided shutter door.
@ CurtainDoor
Curtain door.
@ WaterfallDoor
Waterfall door.
@ BigKeyDoor
Big key door.
@ EyeWatchDoor
Eye watch door.
@ ExitMarker
Exit marker.
@ DoubleSidedShutter
Double sided shutter door.
constexpr size_t kMaxTileObjects
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:161
constexpr size_t kMaxDoors
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:106
constexpr int kNumberOfRooms
constexpr size_t kMaxChests
constexpr size_t kMaxTotalSprites
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
Represents a group of palettes.
std::optional< float > grid_step
Definition canvas.h:70
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89