yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_viewer.cc
Go to the documentation of this file.
2
3#include "absl/strings/str_format.h"
4#include "app/gfx/arena.h"
6#include "app/gui/input.h"
7#include "app/rom.h"
10#include "imgui/imgui.h"
11#include "util/log.h"
12
13namespace yaze::editor {
14
15// DrawDungeonTabView() removed - DungeonEditorV2 uses EditorCard system for flexible docking
16
17void DungeonCanvasViewer::Draw(int room_id) {
18 DrawDungeonCanvas(room_id);
19}
20
22 // Validate room_id and ROM
23 if (room_id < 0 || room_id >= 0x128) {
24 ImGui::Text("Invalid room ID: %d", room_id);
25 return;
26 }
27
28 if (!rom_ || !rom_->is_loaded()) {
29 ImGui::Text("ROM not loaded");
30 return;
31 }
32
33 ImGui::BeginGroup();
34
35 // CRITICAL: Canvas coordinate system for dungeons
36 // The canvas system uses a two-stage scaling model:
37 // 1. Canvas size: UNSCALED content dimensions (512x512 for dungeon rooms)
38 // 2. Viewport size: canvas_size * global_scale (handles zoom)
39 // 3. Grid lines: grid_step * global_scale (auto-scales with zoom)
40 // 4. Bitmaps: drawn with scale = global_scale (matches viewport)
41 constexpr int kRoomPixelWidth = 512; // 64 tiles * 8 pixels (UNSCALED)
42 constexpr int kRoomPixelHeight = 512;
43 constexpr int kDungeonTileSize = 8; // Dungeon tiles are 8x8 pixels
44
45 // Configure canvas for dungeon display
46 canvas_.SetCanvasSize(ImVec2(kRoomPixelWidth, kRoomPixelHeight));
47 canvas_.SetGridSize(gui::CanvasGridSize::k8x8); // Match dungeon tile size
48
49 // DEBUG: Log canvas configuration
50 static int debug_frame_count = 0;
51 if (debug_frame_count++ % 60 == 0) { // Log once per second (assuming 60fps)
52 LOG_DEBUG("[DungeonCanvas]", "Canvas config: size=(%.0f,%.0f) scale=%.2f grid=%.0f",
54 LOG_DEBUG("[DungeonCanvas]", "Canvas viewport: p0=(%.0f,%.0f) p1=(%.0f,%.0f)",
58 }
59
60 if (rooms_) {
61 auto& room = (*rooms_)[room_id];
62
63 // Store previous values to detect changes
64 static int prev_blockset = -1;
65 static int prev_palette = -1;
66 static int prev_layout = -1;
67 static int prev_spriteset = -1;
68
69 // Room properties in organized table
70 if (ImGui::BeginTable("##RoomProperties", 4, ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_Borders)) {
71 ImGui::TableSetupColumn("Graphics");
72 ImGui::TableSetupColumn("Layout");
73 ImGui::TableSetupColumn("Floors");
74 ImGui::TableSetupColumn("Message");
75 ImGui::TableHeadersRow();
76
77 ImGui::TableNextRow();
78
79 // Column 1: Graphics (Blockset, Spriteset, Palette)
80 ImGui::TableNextColumn();
81 gui::InputHexByte("Gfx", &room.blockset, 50.f);
82 gui::InputHexByte("Sprite", &room.spriteset, 50.f);
83 gui::InputHexByte("Palette", &room.palette, 50.f);
84
85 // Column 2: Layout
86 ImGui::TableNextColumn();
87 gui::InputHexByte("Layout", &room.layout, 50.f);
88
89 // Column 3: Floors
90 ImGui::TableNextColumn();
91 uint8_t floor1_val = room.floor1();
92 uint8_t floor2_val = room.floor2();
93 if (gui::InputHexByte("Floor1", &floor1_val, 50.f) && ImGui::IsItemDeactivatedAfterEdit()) {
94 room.set_floor1(floor1_val);
95 if (room.rom() && room.rom()->is_loaded()) {
96 room.RenderRoomGraphics();
97 }
98 }
99 if (gui::InputHexByte("Floor2", &floor2_val, 50.f) && ImGui::IsItemDeactivatedAfterEdit()) {
100 room.set_floor2(floor2_val);
101 if (room.rom() && room.rom()->is_loaded()) {
102 room.RenderRoomGraphics();
103 }
104 }
105
106 // Column 4: Message
107 ImGui::TableNextColumn();
108 gui::InputHexWord("MsgID", &room.message_id_, 70.f);
109
110 ImGui::EndTable();
111 }
112
113 // Advanced room properties (Effect, Tags, Layer Merge)
114 ImGui::Separator();
115 if (ImGui::BeginTable("##AdvancedProperties", 3, ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_Borders)) {
116 ImGui::TableSetupColumn("Effect");
117 ImGui::TableSetupColumn("Tag 1");
118 ImGui::TableSetupColumn("Tag 2");
119 ImGui::TableHeadersRow();
120
121 ImGui::TableNextRow();
122
123 // Effect dropdown
124 ImGui::TableNextColumn();
125 const char* effect_names[] = {"Nothing", "One", "Moving Floor", "Moving Water", "Four", "Red Flashes", "Torch Show Floor", "Ganon Room"};
126 int effect_val = static_cast<int>(room.effect());
127 if (ImGui::Combo("##Effect", &effect_val, effect_names, 8)) {
128 room.SetEffect(static_cast<zelda3::EffectKey>(effect_val));
129 }
130
131 // Tag 1 dropdown (abbreviated for space)
132 ImGui::TableNextColumn();
133 const char* tag_names[] = {"Nothing", "NW Kill", "NE Kill", "SW Kill", "SE Kill", "W Kill", "E Kill", "N Kill", "S Kill",
134 "Clear Quad", "Clear Room", "NW Push", "NE Push", "SW Push", "SE Push", "W Push", "E Push",
135 "N Push", "S Push", "Push Block", "Pull Lever", "Clear Level", "Switch Hold", "Switch Toggle"};
136 int tag1_val = static_cast<int>(room.tag1());
137 if (ImGui::Combo("##Tag1", &tag1_val, tag_names, 24)) {
138 room.SetTag1(static_cast<zelda3::TagKey>(tag1_val));
139 }
140
141 // Tag 2 dropdown
142 ImGui::TableNextColumn();
143 int tag2_val = static_cast<int>(room.tag2());
144 if (ImGui::Combo("##Tag2", &tag2_val, tag_names, 24)) {
145 room.SetTag2(static_cast<zelda3::TagKey>(tag2_val));
146 }
147
148 ImGui::EndTable();
149 }
150
151 // Layer visibility and merge controls
152 ImGui::Separator();
153 if (ImGui::BeginTable("##LayerControls", 4, ImGuiTableFlags_SizingStretchSame)) {
154 ImGui::TableNextRow();
155
156 ImGui::TableNextColumn();
157 auto& layer_settings = GetRoomLayerSettings(room_id);
158 ImGui::Checkbox("BG1", &layer_settings.bg1_visible);
159
160 ImGui::TableNextColumn();
161 ImGui::Checkbox("BG2", &layer_settings.bg2_visible);
162
163 ImGui::TableNextColumn();
164 // BG2 layer type
165 const char* bg2_types[] = {"Norm", "Trans", "Add", "Dark", "Off"};
166 ImGui::SetNextItemWidth(-FLT_MIN);
167 ImGui::Combo("##BG2Type", &layer_settings.bg2_layer_type, bg2_types, 5);
168
169 ImGui::TableNextColumn();
170 // Layer merge type
171 const char* merge_types[] = {"Off", "Parallax", "Dark", "On top", "Translucent", "Addition", "Normal", "Transparent", "Dark room"};
172 int merge_val = room.layer_merging().ID;
173 if (ImGui::Combo("##Merge", &merge_val, merge_types, 9)) {
174 room.SetLayerMerging(zelda3::kLayerMergeTypeList[merge_val]);
175 }
176
177 ImGui::EndTable();
178 }
179
180 // Check if critical properties changed and trigger reload
181 if (prev_blockset != room.blockset || prev_palette != room.palette ||
182 prev_layout != room.layout || prev_spriteset != room.spriteset) {
183
184 // Only reload if ROM is properly loaded
185 if (room.rom() && room.rom()->is_loaded()) {
186 // Force reload of room graphics
187 // Room buffers are now self-contained - no need for separate palette operations
188 room.LoadRoomGraphics(room.blockset);
189 room.RenderRoomGraphics(); // Applies palettes internally
190 }
191
192 prev_blockset = room.blockset;
193 prev_palette = room.palette;
194 prev_layout = room.layout;
195 prev_spriteset = room.spriteset;
196 }
197 }
198
199 ImGui::EndGroup();
200
201 // CRITICAL: Draw canvas with explicit size to ensure viewport matches content
202 // Pass the unscaled room size directly to DrawBackground
203 canvas_.DrawBackground(ImVec2(kRoomPixelWidth, kRoomPixelHeight));
204
205 // DEBUG: Log canvas state after DrawBackground
206 if (debug_frame_count % 60 == 1) {
207 LOG_DEBUG("[DungeonCanvas]", "After DrawBackground: canvas_sz=(%.0f,%.0f) canvas_p0=(%.0f,%.0f) canvas_p1=(%.0f,%.0f)",
211 }
212
213 // Add dungeon-specific context menu items
215
216 if (rooms_ && rom_->is_loaded()) {
217 auto& room = (*rooms_)[room_id];
218 auto& layer_settings = GetRoomLayerSettings(room_id);
219
220 // Add object placement option
222 ICON_MD_ADD " Place Object",
223 []() {
224 // TODO: Show object palette/selector
225 },
226 "Ctrl+P"
227 });
228
229 // Add object deletion for selected objects
231 ICON_MD_DELETE " Delete Selected",
232 [this]() {
234 },
235 "Del"
236 });
237
238 // Add room property quick toggles
240 ICON_MD_LAYERS " Toggle BG1",
241 [this, room_id]() {
242 auto& settings = GetRoomLayerSettings(room_id);
243 settings.bg1_visible = !settings.bg1_visible;
244 },
245 "1"
246 });
247
249 ICON_MD_LAYERS " Toggle BG2",
250 [this, room_id]() {
251 auto& settings = GetRoomLayerSettings(room_id);
252 settings.bg2_visible = !settings.bg2_visible;
253 },
254 "2"
255 });
256
257 // Add re-render option
259 ICON_MD_REFRESH " Re-render Room",
260 [&room]() {
261 room.RenderRoomGraphics();
262 },
263 "Ctrl+R"
264 });
265
266 // === DEBUG MENU ===
268 debug_menu.label = ICON_MD_BUG_REPORT " Debug";
269
270 // Show room info
271 debug_menu.subitems.push_back({
272 ICON_MD_INFO " Show Room Info",
273 [this, room_id]() {
275 }
276 });
277
278 // Show texture info
279 debug_menu.subitems.push_back({
280 ICON_MD_IMAGE " Show Texture Debug",
281 [this]() {
283 }
284 });
285
286 // Show object bounds with sub-menu for categories
287 gui::Canvas::ContextMenuItem object_bounds_menu;
288 object_bounds_menu.label = ICON_MD_CROP_SQUARE " Show Object Bounds";
289 object_bounds_menu.callback = [this]() {
291 };
292
293 // Sub-menu for filtering by type
294 object_bounds_menu.subitems.push_back({
295 "Type 1 (0x00-0xFF)",
296 [this]() {
298 }
299 });
300 object_bounds_menu.subitems.push_back({
301 "Type 2 (0x100-0x1FF)",
302 [this]() {
304 }
305 });
306 object_bounds_menu.subitems.push_back({
307 "Type 3 (0xF00-0xFFF)",
308 [this]() {
310 }
311 });
312
313 // Separator
315 sep.label = "---";
316 sep.enabled_condition = []() { return false; };
317 object_bounds_menu.subitems.push_back(sep);
318
319 // Sub-menu for filtering by layer
320 object_bounds_menu.subitems.push_back({
321 "Layer 0 (BG1)",
322 [this]() {
324 }
325 });
326 object_bounds_menu.subitems.push_back({
327 "Layer 1 (BG2)",
328 [this]() {
330 }
331 });
332 object_bounds_menu.subitems.push_back({
333 "Layer 2 (BG3)",
334 [this]() {
336 }
337 });
338
339 debug_menu.subitems.push_back(object_bounds_menu);
340
341 // Show layer info
342 debug_menu.subitems.push_back({
343 ICON_MD_LAYERS " Show Layer Info",
344 [this]() {
346 }
347 });
348
349 // Force reload room
350 debug_menu.subitems.push_back({
351 ICON_MD_REFRESH " Force Reload",
352 [&room, room_id]() {
353 room.LoadObjects();
354 room.LoadRoomGraphics(room.blockset);
355 room.RenderRoomGraphics();
356 }
357 });
358
359 // Log room state
360 debug_menu.subitems.push_back({
361 ICON_MD_PRINT " Log Room State",
362 [&room, room_id]() {
363 LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id);
364 LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d",
365 room.blockset, room.palette, room.layout);
366 LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu",
367 room.GetTileObjects().size(), room.GetSprites().size());
368 LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d",
369 room.bg1_buffer().bitmap().width(), room.bg1_buffer().bitmap().height(),
370 room.bg2_buffer().bitmap().width(), room.bg2_buffer().bitmap().height());
371 }
372 });
373
374 canvas_.AddContextMenuItem(debug_menu);
375 }
376
378
379 // Draw persistent debug overlays
381 auto& room = (*rooms_)[room_id];
382 ImGui::SetNextWindowPos(ImVec2(canvas_.zero_point().x + 10, canvas_.zero_point().y + 10), ImGuiCond_FirstUseEver);
383 ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_FirstUseEver);
384 if (ImGui::Begin("Room Debug Info", &show_room_debug_info_, ImGuiWindowFlags_NoCollapse)) {
385 ImGui::Text("Room: 0x%03X (%d)", room_id, room_id);
386 ImGui::Separator();
387 ImGui::Text("Graphics");
388 ImGui::Text(" Blockset: 0x%02X", room.blockset);
389 ImGui::Text(" Palette: 0x%02X", room.palette);
390 ImGui::Text(" Layout: 0x%02X", room.layout);
391 ImGui::Text(" Spriteset: 0x%02X", room.spriteset);
392 ImGui::Separator();
393 ImGui::Text("Content");
394 ImGui::Text(" Objects: %zu", room.GetTileObjects().size());
395 ImGui::Text(" Sprites: %zu", room.GetSprites().size());
396 ImGui::Separator();
397 ImGui::Text("Buffers");
398 auto& bg1 = room.bg1_buffer().bitmap();
399 auto& bg2 = room.bg2_buffer().bitmap();
400 ImGui::Text(" BG1: %dx%d %s", bg1.width(), bg1.height(),
401 bg1.texture() ? "(has texture)" : "(NO TEXTURE)");
402 ImGui::Text(" BG2: %dx%d %s", bg2.width(), bg2.height(),
403 bg2.texture() ? "(has texture)" : "(NO TEXTURE)");
404 ImGui::Separator();
405 ImGui::Text("Layers");
406 auto& layer_settings = GetRoomLayerSettings(room_id);
407 ImGui::Checkbox("BG1 Visible", &layer_settings.bg1_visible);
408 ImGui::Checkbox("BG2 Visible", &layer_settings.bg2_visible);
409 ImGui::SliderInt("BG2 Type", &layer_settings.bg2_layer_type, 0, 4);
410
411 ImGui::Separator();
412 ImGui::Text("Layout Override");
413 static bool enable_override = false;
414 ImGui::Checkbox("Enable Override", &enable_override);
415 if (enable_override) {
416 ImGui::SliderInt("Layout ID", &layout_override_, 0, 7);
417 } else {
418 layout_override_ = -1; // Disable override
419 }
420
422 ImGui::Separator();
423 ImGui::Text("Object Outline Filters");
424 ImGui::Text("By Type:");
425 ImGui::Checkbox("Type 1", &object_outline_toggles_.show_type1_objects);
426 ImGui::Checkbox("Type 2", &object_outline_toggles_.show_type2_objects);
427 ImGui::Checkbox("Type 3", &object_outline_toggles_.show_type3_objects);
428 ImGui::Text("By Layer:");
429 ImGui::Checkbox("Layer 0", &object_outline_toggles_.show_layer0_objects);
430 ImGui::Checkbox("Layer 1", &object_outline_toggles_.show_layer1_objects);
431 ImGui::Checkbox("Layer 2", &object_outline_toggles_.show_layer2_objects);
432 }
433 }
434 ImGui::End();
435 }
436
438 ImGui::SetNextWindowPos(ImVec2(canvas_.zero_point().x + 320, canvas_.zero_point().y + 10), ImGuiCond_FirstUseEver);
439 ImGui::SetNextWindowSize(ImVec2(250, 0), ImGuiCond_FirstUseEver);
440 if (ImGui::Begin("Texture Debug", &show_texture_debug_, ImGuiWindowFlags_NoCollapse)) {
441 auto& room = (*rooms_)[room_id];
442 auto& bg1 = room.bg1_buffer().bitmap();
443 auto& bg2 = room.bg2_buffer().bitmap();
444
445 ImGui::Text("BG1 Bitmap");
446 ImGui::Text(" Size: %dx%d", bg1.width(), bg1.height());
447 ImGui::Text(" Active: %s", bg1.is_active() ? "YES" : "NO");
448 ImGui::Text(" Texture: 0x%p", bg1.texture());
449 ImGui::Text(" Modified: %s", bg1.modified() ? "YES" : "NO");
450
451 if (bg1.texture()) {
452 ImGui::Text(" Preview:");
453 ImGui::Image((ImTextureID)(intptr_t)bg1.texture(), ImVec2(128, 128));
454 }
455
456 ImGui::Separator();
457 ImGui::Text("BG2 Bitmap");
458 ImGui::Text(" Size: %dx%d", bg2.width(), bg2.height());
459 ImGui::Text(" Active: %s", bg2.is_active() ? "YES" : "NO");
460 ImGui::Text(" Texture: 0x%p", bg2.texture());
461 ImGui::Text(" Modified: %s", bg2.modified() ? "YES" : "NO");
462
463 if (bg2.texture()) {
464 ImGui::Text(" Preview:");
465 ImGui::Image((ImTextureID)(intptr_t)bg2.texture(), ImVec2(128, 128));
466 }
467 }
468 ImGui::End();
469 }
470
471 if (show_layer_info_) {
472 ImGui::SetNextWindowPos(ImVec2(canvas_.zero_point().x + 580, canvas_.zero_point().y + 10), ImGuiCond_FirstUseEver);
473 ImGui::SetNextWindowSize(ImVec2(200, 0), ImGuiCond_FirstUseEver);
474 if (ImGui::Begin("Layer Info", &show_layer_info_, ImGuiWindowFlags_NoCollapse)) {
475 ImGui::Text("Canvas Scale: %.2f", canvas_.global_scale());
476 ImGui::Text("Canvas Size: %.0fx%.0f", canvas_.width(), canvas_.height());
477 auto& layer_settings = GetRoomLayerSettings(room_id);
478 ImGui::Separator();
479 ImGui::Text("Layer Visibility:");
480 ImGui::Text(" BG1: %s", layer_settings.bg1_visible ? "VISIBLE" : "hidden");
481 ImGui::Text(" BG2: %s", layer_settings.bg2_visible ? "VISIBLE" : "hidden");
482 ImGui::Text("BG2 Type: %d", layer_settings.bg2_layer_type);
483 const char* bg2_type_names[] = {"Normal", "Translucent", "Addition", "Dark", "Off"};
484 ImGui::Text(" (%s)", bg2_type_names[std::min(layer_settings.bg2_layer_type, 4)]);
485 }
486 ImGui::End();
487 }
488
489 if (rooms_ && rom_->is_loaded()) {
490 auto& room = (*rooms_)[room_id];
491
492 // Update object interaction context
494
495 // Check if THIS ROOM's buffers need rendering (not global arena!)
496 auto& bg1_bitmap = room.bg1_buffer().bitmap();
497 bool needs_render = !bg1_bitmap.is_active() || bg1_bitmap.width() == 0;
498
499 // Render immediately if needed (but only once per room change)
500 static int last_rendered_room = -1;
501 static bool has_rendered = false;
502 if (needs_render && (last_rendered_room != room_id || !has_rendered)) {
503 printf("[DungeonCanvasViewer] Loading and rendering graphics for room %d\n", room_id);
504 (void)LoadAndRenderRoomGraphics(room_id);
505 last_rendered_room = room_id;
506 has_rendered = true;
507 }
508
509 // Load room objects if not already loaded
510 if (room.GetTileObjects().empty()) {
511 room.LoadObjects();
512 }
513
514 // CRITICAL: Process texture queue BEFORE drawing to ensure textures are ready
515 // This must happen before DrawRoomBackgroundLayers() attempts to draw bitmaps
516 if (rom_ && rom_->is_loaded()) {
518 }
519
520 // Draw the room's background layers to canvas
521 // This already includes objects rendered by ObjectDrawer in Room::RenderObjectsToBackground()
523
524 // Render sprites as simple 16x16 squares with labels
525 // (Sprites are not part of the background buffers)
526 RenderSprites(room);
527
528 // Handle object interaction if enabled
533 object_interaction_.DrawSelectionHighlights(); // Draw selection highlights on top
534 object_interaction_.ShowContextMenu(); // Show dungeon-aware context menu
535 }
536 }
537
538 // Draw optional overlays on top of background bitmap
539 if (rooms_ && rom_->is_loaded()) {
540 auto& room = (*rooms_)[room_id];
541
542 // Draw the room layout first as the base layer
543
544 // VISUALIZATION: Draw object position rectangles (for debugging)
545 // This shows where objects are placed regardless of whether graphics render
548 }
549 }
550
553
554 // Draw layer information overlay
555 if (rooms_ && rom_->is_loaded()) {
556 auto& room = (*rooms_)[room_id];
557 std::string layer_info = absl::StrFormat(
558 "Room %03X - Objects: %zu, Sprites: %zu\n"
559 "Layers are game concept: Objects exist on different levels\n"
560 "connected by stair objects for player navigation",
561 room_id, room.GetTileObjects().size(), room.GetSprites().size());
562
563 canvas_.DrawText(layer_info, 10, canvas_.height() - 60);
564 }
565}
566
567
569 int canvas_x, int canvas_y) {
570 // Display object information as text overlay
571 std::string info_text = absl::StrFormat("ID:%d X:%d Y:%d S:%d", object.id_,
572 object.x_, object.y_, object.size_);
573
574 // Draw text at the object position
575 canvas_.DrawText(info_text, canvas_x, canvas_y - 12);
576}
577
579 // Render sprites as simple 8x8 squares with sprite name/ID
580 for (const auto& sprite : room.GetSprites()) {
581 auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(sprite.x(), sprite.y());
582
583 if (IsWithinCanvasBounds(canvas_x, canvas_y, 8)) {
584 // Draw 8x8 square for sprite
585 ImVec4 sprite_color;
586
587 // Color-code sprites based on layer
588 if (sprite.layer() == 0) {
589 sprite_color = ImVec4(0.2f, 0.8f, 0.2f, 0.8f); // Green for layer 0
590 } else {
591 sprite_color = ImVec4(0.2f, 0.2f, 0.8f, 0.8f); // Blue for layer 1
592 }
593
594 canvas_.DrawRect(canvas_x, canvas_y, 8, 8, sprite_color);
595
596 // Draw sprite border
597 canvas_.DrawRect(canvas_x, canvas_y, 8, 8, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
598
599 // Draw sprite ID and name
600 std::string sprite_text;
601 if (sprite.id() >= 0) { // sprite.id() is uint8_t so always < 256
602 // Extract just the sprite name part (remove ID prefix)
603 std::string full_name = zelda3::kSpriteDefaultNames[sprite.id()];
604 auto space_pos = full_name.find(' ');
605 if (space_pos != std::string::npos && space_pos < full_name.length() - 1) {
606 std::string sprite_name = full_name.substr(space_pos + 1);
607 // Truncate long names
608 if (sprite_name.length() > 8) {
609 sprite_name = sprite_name.substr(0, 8) + "...";
610 }
611 sprite_text = absl::StrFormat("%02X\n%s", sprite.id(), sprite_name.c_str());
612 } else {
613 sprite_text = absl::StrFormat("%02X", sprite.id());
614 }
615 } else {
616 sprite_text = absl::StrFormat("%02X", sprite.id());
617 }
618
619 canvas_.DrawText(sprite_text, canvas_x + 18, canvas_y);
620 }
621 }
622}
623
624// Coordinate conversion helper functions
625std::pair<int, int> DungeonCanvasViewer::RoomToCanvasCoordinates(int room_x,
626 int room_y) const {
627 // Convert room coordinates (tile units) to UNSCALED canvas pixel coordinates
628 // Dungeon tiles are 8x8 pixels (not 16x16!)
629 // IMPORTANT: Return UNSCALED coordinates - Canvas drawing functions apply scale internally
630 // Do NOT multiply by scale here or we get double-scaling!
631
632 // Simple conversion: tile units → pixel units (no scale, no offset)
633 return {room_x * 8, room_y * 8};
634}
635
636std::pair<int, int> DungeonCanvasViewer::CanvasToRoomCoordinates(int canvas_x,
637 int canvas_y) const {
638 // Convert canvas screen coordinates (pixels) to room coordinates (tile units)
639 // Input: Screen-space coordinates (affected by zoom/scale)
640 // Output: Logical tile coordinates (0-63 for each axis)
641
642 // IMPORTANT: Mouse coordinates are in screen space, must undo scale first
643 float scale = canvas_.global_scale();
644 if (scale <= 0.0f) scale = 1.0f; // Prevent division by zero
645
646 // Step 1: Convert screen space → logical pixel space
647 int logical_x = static_cast<int>(canvas_x / scale);
648 int logical_y = static_cast<int>(canvas_y / scale);
649
650 // Step 2: Convert logical pixels → tile units (8 pixels per tile)
651 return {logical_x / 8, logical_y / 8};
652}
653
654bool DungeonCanvasViewer::IsWithinCanvasBounds(int canvas_x, int canvas_y,
655 int margin) const {
656 // Check if coordinates are within canvas bounds with optional margin
657 auto canvas_width = canvas_.width();
658 auto canvas_height = canvas_.height();
659 return (canvas_x >= -margin && canvas_y >= -margin &&
660 canvas_x <= canvas_width + margin &&
661 canvas_y <= canvas_height + margin);
662}
663
664void DungeonCanvasViewer::CalculateWallDimensions(const zelda3::RoomObject& object, int& width, int& height) {
665 // Default base size
666 width = 8;
667 height = 8;
668
669 // For walls, use the size field to determine length and orientation
670 if (object.id_ >= 0x10 && object.id_ <= 0x1F) {
671 // Wall objects: size determines length and orientation
672 uint8_t size_x = object.size_ & 0x0F;
673 uint8_t size_y = (object.size_ >> 4) & 0x0F;
674
675 // Walls can be horizontal or vertical based on size parameters
676 if (size_x > size_y) {
677 // Horizontal wall
678 width = 8 + size_x * 8; // Each unit adds 8 pixels
679 height = 8;
680 } else if (size_y > size_x) {
681 // Vertical wall
682 width = 8;
683 height = 8 + size_y * 8;
684 } else {
685 // Square wall or corner
686 width = 8 + size_x * 4;
687 height = 8 + size_y * 4;
688 }
689 } else {
690 // For other objects, use standard size calculation
691 width = 8 + (object.size_ & 0x0F) * 4;
692 height = 8 + ((object.size_ >> 4) & 0x0F) * 4;
693 }
694
695 // Clamp to reasonable limits
696 width = std::min(width, 256);
697 height = std::min(height, 256);
698}
699
700// Room layout visualization
701
702// Object visualization methods
704 // Draw colored rectangles showing object positions
705 // This helps visualize object placement even if graphics don't render correctly
706
707 const auto& objects = room.GetTileObjects();
708
709 for (const auto& obj : objects) {
710 // Filter by object type (default to true if unknown type)
711 bool show_this_type = true; // Default to showing
712 if (obj.id_ < 0x100) {
714 } else if (obj.id_ >= 0x100 && obj.id_ < 0x200) {
716 } else if (obj.id_ >= 0xF00) {
718 }
719 // else: unknown type, use default (true)
720
721 // Filter by layer (default to true if unknown layer)
722 bool show_this_layer = true; // Default to showing
723 if (obj.GetLayerValue() == 0) {
725 } else if (obj.GetLayerValue() == 1) {
727 } else if (obj.GetLayerValue() == 2) {
729 }
730 // else: unknown layer, use default (true)
731
732 // Skip if filtered out
733 if (!show_this_type || !show_this_layer) {
734 continue;
735 }
736
737 // Convert object position (tile coordinates) to canvas pixel coordinates (UNSCALED)
738 auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(obj.x(), obj.y());
739
740 // Calculate object dimensions based on type and size (UNSCALED logical pixels)
741 int width = 8; // Default 8x8 pixels
742 int height = 8;
743
744 // Use ZScream pattern: size field determines dimensions
745 // Lower nibble = horizontal size, upper nibble = vertical size
746 int size_h = (obj.size() & 0x0F);
747 int size_v = (obj.size() >> 4) & 0x0F;
748
749 // Objects are typically (size+1) tiles wide/tall (8 pixels per tile)
750 width = (size_h + 1) * 8;
751 height = (size_v + 1) * 8;
752
753 // IMPORTANT: Do NOT apply canvas scale here - DrawRect handles it
754 // Clamp to reasonable sizes (in logical space)
755 width = std::min(width, 512);
756 height = std::min(height, 512);
757
758 // Color-code by layer
759 ImVec4 outline_color;
760 if (obj.GetLayerValue() == 0) {
761 outline_color = ImVec4(1.0f, 0.0f, 0.0f, 0.5f); // Red for layer 0
762 } else if (obj.GetLayerValue() == 1) {
763 outline_color = ImVec4(0.0f, 1.0f, 0.0f, 0.5f); // Green for layer 1
764 } else {
765 outline_color = ImVec4(0.0f, 0.0f, 1.0f, 0.5f); // Blue for layer 2
766 }
767
768 // Draw outline rectangle
769 canvas_.DrawRect(canvas_x, canvas_y, width, height, outline_color);
770
771 // Draw object ID label (smaller, less obtrusive)
772 std::string label = absl::StrFormat("%02X", obj.id_);
773 canvas_.DrawText(label, canvas_x + 1, canvas_y + 1);
774 }
775}
776
777// Room graphics management methods
779 LOG_DEBUG("[LoadAndRender]", "START room_id=%d", room_id);
780
781 if (room_id < 0 || room_id >= 128) {
782 LOG_DEBUG("[LoadAndRender]", "ERROR: Invalid room ID");
783 return absl::InvalidArgumentError("Invalid room ID");
784 }
785
786 if (!rom_ || !rom_->is_loaded()) {
787 LOG_DEBUG("[LoadAndRender]", "ERROR: ROM not loaded");
788 return absl::FailedPreconditionError("ROM not loaded");
789 }
790
791 if (!rooms_) {
792 LOG_DEBUG("[LoadAndRender]", "ERROR: Room data not available");
793 return absl::FailedPreconditionError("Room data not available");
794 }
795
796 auto& room = (*rooms_)[room_id];
797 LOG_DEBUG("[LoadAndRender]", "Got room reference");
798
799 // Load room graphics with proper blockset
800 LOG_DEBUG("[LoadAndRender]", "Loading graphics for blockset %d", room.blockset);
801 room.LoadRoomGraphics(room.blockset);
802 LOG_DEBUG("[LoadAndRender]", "Graphics loaded");
803
804 // Load the room's palette with bounds checking
805 if (room.palette < rom_->paletteset_ids.size() &&
806 !rom_->paletteset_ids[room.palette].empty()) {
807 auto dungeon_palette_ptr = rom_->paletteset_ids[room.palette][0];
808 auto palette_id = rom_->ReadWord(0xDEC4B + dungeon_palette_ptr);
809 if (palette_id.ok()) {
810 current_palette_group_id_ = palette_id.value() / 180;
811 if (current_palette_group_id_ < rom_->palette_group().dungeon_main.size()) {
812 auto full_palette = rom_->palette_group().dungeon_main[current_palette_group_id_];
813 // TODO: Fix palette assignment to buffer.
816 LOG_DEBUG("[LoadAndRender]", "Palette loaded: group_id=%zu", current_palette_group_id_);
817 }
818 }
819 }
820
821 // Render the room graphics (self-contained - handles all palette application)
822 LOG_DEBUG("[LoadAndRender]", "Calling room.RenderRoomGraphics()...");
823 room.RenderRoomGraphics();
824 LOG_DEBUG("[LoadAndRender]", "RenderRoomGraphics() complete - room buffers self-contained");
825
826 LOG_DEBUG("[LoadAndRender]", "SUCCESS");
827 return absl::OkStatus();
828}
829
831 if (room_id < 0 || room_id >= zelda3::NumberOfRooms || !rooms_) return;
832
833 auto& room = (*rooms_)[room_id];
834 auto& layer_settings = GetRoomLayerSettings(room_id);
835
836 // Use THIS room's own buffers, not global arena!
837 auto& bg1_bitmap = room.bg1_buffer().bitmap();
838 auto& bg2_bitmap = room.bg2_buffer().bitmap();
839
840 // Draw BG1 layer if visible and active
841 if (layer_settings.bg1_visible && bg1_bitmap.is_active() && bg1_bitmap.width() > 0 && bg1_bitmap.height() > 0) {
842 if (!bg1_bitmap.texture()) {
843 // Queue texture creation for background layer 1 via Arena's deferred system
844 // BATCHING FIX: Don't process immediately - let the main loop handle batching
847
848 // Queue will be processed at the end of the frame in DrawDungeonCanvas()
849 // This allows multiple rooms to batch their texture operations together
850 }
851
852 // Only draw if texture was successfully created
853 if (bg1_bitmap.texture()) {
854 // Use canvas global scale so bitmap scales with zoom
855 float scale = canvas_.global_scale();
856 LOG_DEBUG("DungeonCanvasViewer", "Drawing BG1 bitmap to canvas with texture %p, scale=%.2f", bg1_bitmap.texture(), scale);
857 canvas_.DrawBitmap(bg1_bitmap, 0, 0, scale, 255);
858 } else {
859 LOG_DEBUG("DungeonCanvasViewer", "ERROR: BG1 bitmap has no texture!");
860 }
861 }
862
863 // Draw BG2 layer if visible and active
864 if (layer_settings.bg2_visible && bg2_bitmap.is_active() && bg2_bitmap.width() > 0 && bg2_bitmap.height() > 0) {
865 if (!bg2_bitmap.texture()) {
866 // Queue texture creation for background layer 2 via Arena's deferred system
867 // BATCHING FIX: Don't process immediately - let the main loop handle batching
870
871 // Queue will be processed at the end of the frame in DrawDungeonCanvas()
872 // This allows multiple rooms to batch their texture operations together
873 }
874
875 // Only draw if texture was successfully created
876 if (bg2_bitmap.texture()) {
877 // Use the selected BG2 layer type alpha value
878 const int bg2_alpha_values[] = {255, 191, 127, 64, 0};
879 int alpha_value = bg2_alpha_values[std::min(layer_settings.bg2_layer_type, 4)];
880 // Use canvas global scale so bitmap scales with zoom
881 float scale = canvas_.global_scale();
882 LOG_DEBUG("DungeonCanvasViewer", "Drawing BG2 bitmap to canvas with texture %p, alpha=%d, scale=%.2f", bg2_bitmap.texture(), alpha_value, scale);
883 canvas_.DrawBitmap(bg2_bitmap, 0, 0, scale, alpha_value);
884 } else {
885 LOG_DEBUG("DungeonCanvasViewer", "ERROR: BG2 bitmap has no texture!");
886 }
887 }
888
889 // DEBUG: Check if background buffers have content
890 if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0) {
891 LOG_DEBUG("DungeonCanvasViewer", "BG1 bitmap: %dx%d, active=%d, visible=%d, texture=%p",
892 bg1_bitmap.width(), bg1_bitmap.height(), bg1_bitmap.is_active(), layer_settings.bg1_visible, bg1_bitmap.texture());
893
894 // Check bitmap data content
895 auto& bg1_data = bg1_bitmap.mutable_data();
896 int non_zero_pixels = 0;
897 for (size_t i = 0; i < bg1_data.size(); i += 100) { // Sample every 100th pixel
898 if (bg1_data[i] != 0) non_zero_pixels++;
899 }
900 LOG_DEBUG("DungeonCanvasViewer", "BG1 bitmap data: %zu pixels, ~%d non-zero samples",
901 bg1_data.size(), non_zero_pixels);
902 }
903 if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0) {
904 LOG_DEBUG("DungeonCanvasViewer", "BG2 bitmap: %dx%d, active=%d, visible=%d, layer_type=%d, texture=%p",
905 bg2_bitmap.width(), bg2_bitmap.height(), bg2_bitmap.is_active(), layer_settings.bg2_visible, layer_settings.bg2_layer_type, bg2_bitmap.texture());
906
907 // Check bitmap data content
908 auto& bg2_data = bg2_bitmap.mutable_data();
909 int non_zero_pixels = 0;
910 for (size_t i = 0; i < bg2_data.size(); i += 100) { // Sample every 100th pixel
911 if (bg2_data[i] != 0) non_zero_pixels++;
912 }
913 LOG_DEBUG("DungeonCanvasViewer", "BG2 bitmap data: %zu pixels, ~%d non-zero samples",
914 bg2_data.size(), non_zero_pixels);
915 }
916
917 // DEBUG: Show canvas and bitmap info
918 LOG_DEBUG("DungeonCanvasViewer", "Canvas pos: (%.1f, %.1f), Canvas size: (%.1f, %.1f)",
920 LOG_DEBUG("DungeonCanvasViewer", "BG1 bitmap size: %dx%d, BG2 bitmap size: %dx%d",
921 bg1_bitmap.width(), bg1_bitmap.height(), bg2_bitmap.width(), bg2_bitmap.height());
922}
923
924} // namespace yaze::editor
auto palette_group() const
Definition rom.h:213
std::array< std::array< uint8_t, 4 >, kNumPalettesets > paletteset_ids
Definition rom.h:228
absl::StatusOr< uint16_t > ReadWord(int offset)
Definition rom.cc:665
bool is_loaded() const
Definition rom.h:197
std::pair< int, int > CanvasToRoomCoordinates(int canvas_x, int canvas_y) const
std::pair< int, int > RoomToCanvasCoordinates(int room_x, int room_y) const
RoomLayerSettings & GetRoomLayerSettings(int room_id)
std::array< zelda3::Room, 0x128 > * rooms_
absl::Status LoadAndRenderRoomGraphics(int room_id)
void DrawObjectPositionOutlines(const zelda3::Room &room)
DungeonObjectInteraction object_interaction_
void RenderSprites(const zelda3::Room &room)
bool IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin=32) const
void CalculateWallDimensions(const zelda3::RoomObject &object, int &width, int &height)
void DisplayObjectInfo(const zelda3::RoomObject &object, int canvas_x, int canvas_y)
void SetCurrentRoom(std::array< zelda3::Room, 0x128 > *rooms, int room_id)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:32
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:36
static Arena & Get()
Definition arena.cc:15
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1062
auto height() const
Definition canvas.h:349
auto custom_step() const
Definition canvas.h:347
auto global_scale() const
Definition canvas.h:345
void DrawContextMenu()
Definition canvas.cc:441
auto width() const
Definition canvas.h:348
void ClearContextMenuItems()
Definition canvas.cc:616
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1329
void SetCanvasSize(ImVec2 canvas_size)
Definition canvas.h:325
auto canvas_size() const
Definition canvas.h:314
void SetGridSize(CanvasGridSize grid_size)
Definition canvas.h:77
auto zero_point() const
Definition canvas.h:310
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:381
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1386
void DrawText(const std::string &text, int x, int y)
Definition canvas.cc:1334
void AddContextMenuItem(const ContextMenuItem &item)
Definition canvas.cc:612
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:205
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:220
#define ICON_MD_INFO
Definition icons.h:991
#define ICON_MD_REFRESH
Definition icons.h:1570
#define ICON_MD_BUG_REPORT
Definition icons.h:325
#define ICON_MD_LAYERS
Definition icons.h:1066
#define ICON_MD_ADD
Definition icons.h:84
#define ICON_MD_CROP_SQUARE
Definition icons.h:498
#define ICON_MD_IMAGE
Definition icons.h:980
#define ICON_MD_DELETE
Definition icons.h:528
#define ICON_MD_PRINT
Definition icons.h:1513
#define LOG_DEBUG(category, format,...)
Definition log.h:104
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:61
Editors are the view controllers for the application.
absl::StatusOr< PaletteGroup > CreatePaletteGroupFromLargePalette(SnesPalette &palette, int num_colors)
Take a SNESPalette, divide it into palettes of 8 colors.
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:175
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:189
constexpr int NumberOfRooms
Definition room.h:60
std::function< void()> callback
Definition canvas.h:155
std::vector< ContextMenuItem > subitems
Definition canvas.h:157
std::function< bool()> enabled_condition
Definition canvas.h:156