yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_canvas_renderer.cc
Go to the documentation of this file.
1// Related header
3#include "util/i18n/tr.h"
4
5#ifndef IM_PI
6#define IM_PI 3.14159265358979323846f
7#endif
8
9// C++ standard library headers
10#include <memory>
11#include <string>
12
13// Third-party library headers
14#include "absl/status/status.h"
15#include "absl/strings/str_format.h"
16#include "imgui/imgui.h"
17
18// Project headers
29#include "app/gfx/core/bitmap.h"
34#include "app/gui/core/icons.h"
35#include "app/gui/core/style.h"
39#include "rom/rom.h"
40#include "util/log.h"
42
43namespace yaze::editor {
44
45namespace {
46
48 if (mode == nullptr) {
49 return;
50 }
51 constexpr const char* kLabels[] = {"Fit", "1x", "2x", "4x"};
52 int selected = static_cast<int>(*mode);
53 ImGui::AlignTextToFramePadding();
54 ImGui::TextDisabled("%s", tr("Scale"));
55 ImGui::SameLine();
56 ImGui::SetNextItemWidth(gui::ScaledSize(80.0f, 0.0f).x);
57 if (ImGui::Combo(id, &selected, kLabels, IM_ARRAYSIZE(kLabels))) {
58 *mode = static_cast<gui::AdaptiveSheetScaleMode>(selected);
59 }
60}
61
62} // namespace
63
65 : editor_(editor) {
66 if (editor_) {
69 [this](const ImVec2& position) {
70 return PrepareContextMenu(position);
71 });
72 }
73}
74
75// =============================================================================
76// Main Canvas Drawing
77// =============================================================================
78
80 if (!editor_) {
81 return;
82 }
83 const bool lyra_preview = editor_->overworld_.is_lyra_preview();
84 if (lyra_preview) {
85 ImGui::TextUnformatted(
86 "Lyra ROM rev1: 192 stored / 160 active / read-only");
87 ImGui::SameLine();
88 ImGui::TextDisabled("Sprite lists and area overlays unavailable");
89 int map_id = editor_->current_map_;
90 ImGui::SetNextItemWidth(210.0f);
91 if (ImGui::SliderInt("Inspect map", &map_id, 0x80, 0xBF, "0x%02X")) {
92 editor_->JumpToMap(map_id);
93 }
94 ImGui::SameLine();
95 ImGui::TextDisabled("Sea cells are stored, not active gameplay maps");
96 }
97
98 // Keep appearance edits beside the canvas, using the same undo/refresh path
99 // as the full Map Properties panel.
100 if (!lyra_preview && editor_->rom_ != nullptr && editor_->rom_->is_loaded() &&
103 editor_->toolbar_->Draw(
108 editor_->game_state_, [this]() {
109 if (editor_->sidebar_) {
110 editor_->sidebar_->DrawQuickProperties(
111 editor_->current_map_, editor_->current_map_lock_,
112 editor_->dependencies_.project, /*compact=*/true);
113 }
114 });
116 }
117
118 // ==========================================================================
119 // PHASE 3: Modern BeginCanvas/EndCanvas Pattern
120 // ==========================================================================
121 // Menu actions are captured by PrepareContextMenu only when it opens.
122 // Keep rendering an open menu even after entity hover changes. Paint modes
123 // keep right-click for sampling; Shift+right-click opens the menu there
124 // (PrepareContextMenu vetoes a plain right-click).
125 const bool show_context_menu = !lyra_preview;
126
127 // Configure canvas frame options
128 gui::CanvasFrameOptions frame_opts;
130 frame_opts.draw_grid = true;
131 frame_opts.draw_context_menu = show_context_menu;
132 frame_opts.draw_overlay = true;
133 frame_opts.render_popups = !lyra_preview;
134 frame_opts.use_child_window = false; // CRITICAL: Canvas has own pan logic
135
136 // Wrap in child window for scrollbars. The navigation manager owns all
137 // scrolling (wheel, drag pan, zoom anchoring), so ImGui's own wheel scroll
138 // is disabled on this child.
140 if (editor_->canvas_nav_) {
141 editor_->canvas_nav_->BeginCanvasViewport();
142 }
143 ImGui::BeginChild(ImGui::GetID(reinterpret_cast<void*>(intptr_t{7})),
144 ImGui::GetContentRegionAvail(), true,
145 ImGuiWindowFlags_AlwaysVerticalScrollbar |
146 ImGuiWindowFlags_AlwaysHorizontalScrollbar |
147 ImGuiWindowFlags_NoScrollWithMouse);
148
149 // Keep canvas scroll at 0 - ImGui's child window handles all scrolling
150 // The scrollbars scroll the child window which moves the entire canvas
151 editor_->ow_map_canvas_.set_scrolling(ImVec2(0, 0));
152
153 // Begin canvas frame - this handles DrawBackground + DrawContextMenu
154 auto canvas_rt = gui::BeginCanvas(editor_->ow_map_canvas_, frame_opts);
156
157 if (editor_->canvas_nav_) {
158 editor_->canvas_nav_->EndCanvasViewport(canvas_rt.hovered);
159 }
160
161 if (editor_->overworld_.is_loaded()) {
162 // Draw the 64 overworld map bitmaps
163 DrawOverworldMaps();
164 if (lyra_preview) {
165 editor_->status_ = editor_->CheckForCurrentMap();
166 gui::EndCanvas(editor_->ow_map_canvas_, canvas_rt, frame_opts);
167 ImGui::EndChild();
168 return;
169 }
170
171 // Draw all entities using the new CanvasRuntime-based methods
172 if (editor_->entity_renderer_ && editor_->show_entities_) {
173 editor_->entity_renderer_->DrawExits(canvas_rt, editor_->current_world_);
174 editor_->entity_renderer_->DrawEntrances(canvas_rt,
175 editor_->current_world_);
176 editor_->entity_renderer_->DrawItems(canvas_rt, editor_->current_world_);
177 editor_->entity_renderer_->DrawSprites(canvas_rt, editor_->current_world_,
178 editor_->game_state_);
179 FilterHoveredEntityByMode();
180 } else if (editor_->entity_renderer_) {
181 // Hidden entities can't be hovered, dragged or right-clicked.
182 editor_->entity_renderer_->ResetHoveredEntity();
183 }
184
185 // Area subscreen overlays are composited per map in DrawOverworldMaps()
186 // (toggle: show_overlay_preview_).
187
188 // Always refresh hover preview: when the canvas is not hovered this clears
189 // hovered_map_ so the status bar falls back to the selected map.
190 editor_->status_ = editor_->CheckForCurrentMap();
191 if (canvas_rt.hovered) {
192 editor_->HandleMapInteraction();
193 }
194
195 if (editor_->current_mode == EditingMode::DRAW_TILE ||
196 editor_->current_mode == EditingMode::FILL_TILE) {
197 editor_->CheckForOverworldEdits();
198 }
199
200 // --- BEGIN ENTITY DRAG/DROP LOGIC ---
201 if (editor_->current_mode == EditingMode::MOUSE &&
202 editor_->TryPlaceArmedItem()) {
203 // The click placed the item chosen from Insert Item.
204 } else if (editor_->current_mode == EditingMode::MOUSE &&
205 editor_->entity_renderer_) {
206 auto hovered_entity = editor_->entity_renderer_->hovered_entity();
207
208 // 1. Initiate drag
209 if (!editor_->is_dragging_entity_ && hovered_entity &&
210 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
211 editor_->dragged_entity_ = hovered_entity;
212 editor_->is_dragging_entity_ = true;
213 editor_->drag_item_snapshot_.reset();
214 if (hovered_entity->entity_type_ ==
216 editor_->SelectItemByIdentity(
217 *static_cast<zelda3::OverworldItem*>(hovered_entity));
218 // Items have snapshot undo; capture before the drag moves it.
219 editor_->drag_item_snapshot_ = editor_->CaptureItemUndoSnapshot();
220 }
221 if (editor_->dragged_entity_->entity_type_ ==
223 editor_->dragged_entity_free_movement_ = true;
224 }
225 }
226
227 // 2. Update drag
228 if (editor_->is_dragging_entity_ && editor_->dragged_entity_ &&
229 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
230 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
231 ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
232 float scale = canvas_rt.scale;
233 if (scale > 0.0f) {
234 editor_->dragged_entity_->x_ += mouse_delta.x / scale;
235 editor_->dragged_entity_->y_ += mouse_delta.y / scale;
236 }
237 }
238
239 // 3. End drag
240 if (editor_->is_dragging_entity_ &&
241 ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
242 if (editor_->dragged_entity_) {
243 float end_scale = canvas_rt.scale;
244 MoveEntityOnGrid(editor_->dragged_entity_, canvas_rt.canvas_p0,
245 canvas_rt.scrolling,
246 editor_->dragged_entity_free_movement_, end_scale);
247 // Pass overworld context for proper area size detection
248 editor_->dragged_entity_->UpdateMapProperties(
249 editor_->dragged_entity_->map_id_, &editor_->overworld_);
250 if (editor_->drag_item_snapshot_) {
251 // No-op if the item ended where it started.
252 editor_->PushItemUndoAction(
253 std::move(*editor_->drag_item_snapshot_),
254 "Move overworld item");
255 }
256 // TODO(overworld): entrances, exits and sprites have no undo action
257 // yet; their drags only mark the ROM dirty.
258 editor_->rom_->set_dirty(true);
259 }
260 editor_->drag_item_snapshot_.reset();
261 editor_->is_dragging_entity_ = false;
262 editor_->dragged_entity_ = nullptr;
263 editor_->dragged_entity_free_movement_ = false;
264 }
265 }
266 // --- END ENTITY DRAG/DROP LOGIC ---
267
268 // --- TILE DROP TARGET ---
269 // Accept tile drops from the blockset selector onto the map canvas.
270 gui::TileDragPayload tile_drop;
271 if (gui::AcceptTileDrop(&tile_drop)) {
272 editor_->RequestTile16Selection(tile_drop.tile_id);
273 if (editor_->current_mode != EditingMode::DRAW_TILE) {
274 editor_->current_mode = EditingMode::DRAW_TILE;
275 editor_->ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kTilePainting);
276 }
277 }
278 }
279
280 // End canvas frame - draws grid/overlay based on frame_opts
281 gui::EndCanvas(editor_->ow_map_canvas_, canvas_rt, frame_opts);
282 ImGui::EndChild();
283 editor_->DrawItemInsertPopup();
284}
285
286// =============================================================================
287// Internal Canvas Drawing Helpers
288// =============================================================================
289
290// Draws one map the way the game layers it: the area backdrop color (palette
291// color 0, which the map bitmap keeps transparent), the map, then the area's
292// subscreen overlay layer. The layer is built in the map's own palette;
293// background overlays (sky, pyramid, lava) only cover backdrop pixels and are
294// drawn opaque, front overlays (fog, canopy, rain, curtains) at half alpha.
295void OverworldCanvasRenderer::DrawMapWithAreaLayers(int map_index, int map_x,
296 int map_y, float scale) {
297 auto& canvas = editor_->ow_map_canvas_;
298 auto& bitmap = editor_->maps_bmp_[map_index];
299 const float size = kOverworldMapSize * scale;
300 const ImVec2 origin(canvas.zero_point().x + canvas.scrolling().x + map_x,
301 canvas.zero_point().y + canvas.scrolling().y + map_y);
302
303 if (!bitmap.palette().empty()) {
304 const ImVec4 rgb = bitmap.palette()[0].rgb();
305 ImGui::GetWindowDrawList()->AddRectFilled(
306 origin, ImVec2(origin.x + size, origin.y + size),
307 IM_COL32(static_cast<int>(rgb.x), static_cast<int>(rgb.y),
308 static_cast<int>(rgb.z), 255));
309 }
310
311 canvas.DrawBitmap(bitmap, map_x, map_y, scale);
312
313 if (!editor_->show_overlay_preview_) {
314 return;
315 }
316 auto* layer = EnsureOverlayLayer(map_index);
317 if (layer && layer->bitmap.is_active() && layer->bitmap.texture()) {
318 canvas.DrawBitmap(layer->bitmap, map_x, map_y, scale,
319 layer->background ? 255 : 128);
320 }
321}
322
323// Returns the cached overlay layer for a map. The layer is built once per
324// map (building the map if the build cache evicted it) and rebuilt when the
325// map's pixels change while it is built, so an evicted map keeps its last
326// layer instead of being rebuilt every frame.
328OverworldCanvasRenderer::EnsureOverlayLayer(int map_index) {
329 const auto* map = editor_->overworld_.overworld_map(map_index);
330 if (!map) {
331 return nullptr;
332 }
333 const bool has_overlay =
334 zelda3::SubscreenOverlayScreen(map->render_subscreen_overlay()) >= 0;
335 auto it = overlay_layers_.find(map_index);
336 const bool missing =
337 it == overlay_layers_.end() || it->second.has_overlay != has_overlay;
338 const bool stale = !missing && map->is_built() &&
339 it->second.bitmap_serial != map->bitmap_serial();
340 if (missing || stale) {
341 auto& entry = overlay_layers_[map_index];
342 entry.has_overlay = has_overlay;
343 entry.bitmap_serial = map->bitmap_serial();
344 if (has_overlay) {
345 auto layer = editor_->overworld_.BuildSubscreenOverlayLayer(map_index);
346 if (!layer.ok() || layer->overlay_screen < 0) {
347 entry.has_overlay = false;
348 return nullptr;
349 }
350 map = editor_->overworld_.overworld_map(map_index);
351 entry.bitmap_serial = map->bitmap_serial();
352 entry.background = layer->background;
353 if (!entry.bitmap.is_active()) {
354 entry.bitmap.Create(kOverworldMapSize, kOverworldMapSize, 8,
355 layer->pixels);
356 } else {
357 entry.bitmap.set_data(layer->pixels);
358 }
359 entry.bitmap.SetPalette(map->current_palette());
361 entry.bitmap.texture() ? gfx::Arena::TextureCommandType::UPDATE
363 &entry.bitmap);
364 }
365 it = overlay_layers_.find(map_index);
366 }
367 if (!it->second.has_overlay) {
368 return nullptr;
369 }
370 return &it->second;
371}
372
373void OverworldCanvasRenderer::DrawOverworldMaps() {
374 // Get the current zoom scale for positioning and sizing
375 float scale = editor_->ow_map_canvas_.global_scale();
376 if (scale <= 0.0f)
377 scale = 1.0f;
378
379 // Build only maps that intersect the visible canvas, and at most a few per
380 // frame: building all 64 maps of a world in the first frame stalled the UI.
381 // Off-screen maps are built when scrolled into view; visible ones that are
382 // over the budget show the loading placeholder for a frame or two.
383 constexpr int kMaxMapBuildsPerFrame = 4;
384 int builds_this_frame = 0;
385 const ImVec2 clip_min = ImGui::GetWindowDrawList()->GetClipRectMin();
386 const ImVec2 clip_max = ImGui::GetWindowDrawList()->GetClipRectMax();
387 const ImVec2 canvas_origin(editor_->ow_map_canvas_.zero_point().x +
388 editor_->ow_map_canvas_.scrolling().x,
389 editor_->ow_map_canvas_.zero_point().y +
390 editor_->ow_map_canvas_.scrolling().y);
391 const float map_extent = kOverworldMapSize * scale;
392
393 for (int i = 0; i < 0x40; i++) {
394 const int xx = i % 8;
395 const int yy = i / 8;
396 int world_index = i + (editor_->current_world_ * 0x40);
397
398 // Bounds checking to prevent crashes
399 if (world_index < 0 ||
400 world_index >= editor_->overworld_.inspected_map_count()) {
401 continue; // Skip invalid map index
402 }
403
404 // Apply scale to positions for proper zoom support
405 int map_x = static_cast<int>(xx * kOverworldMapSize * scale);
406 int map_y = static_cast<int>(yy * kOverworldMapSize * scale);
407
408 const float left = canvas_origin.x + map_x;
409 const float top = canvas_origin.y + map_y;
410 if (left >= clip_max.x || top >= clip_max.y ||
411 left + map_extent <= clip_min.x || top + map_extent <= clip_min.y) {
412 continue; // Not visible
413 }
414
415 // Ensure visible maps are materialized on demand before drawing. A map
416 // whose bitmap exists only waits for its queued texture (cheap).
417 auto& map_bitmap = editor_->maps_bmp_[world_index];
418 if (!map_bitmap.is_active()) {
419 if (builds_this_frame < kMaxMapBuildsPerFrame) {
420 editor_->EnsureMapTexture(world_index);
421 ++builds_this_frame;
422 }
423 } else if (!map_bitmap.texture()) {
424 editor_->EnsureMapTexture(world_index);
425 }
426
427 // Only draw if the map has a valid texture AND is active (has bitmap data)
428 // The current_map_ check was causing crashes when hovering over unbuilt maps
429 // because the bitmap would be drawn before EnsureMapBuilt() was called
430 bool can_draw = editor_->maps_bmp_[world_index].texture() &&
431 editor_->maps_bmp_[world_index].is_active();
432
433 if (can_draw) {
434 DrawMapWithAreaLayers(world_index, map_x, map_y, scale);
435 } else {
436 // Draw a placeholder for maps that haven't loaded yet
437 ImDrawList* draw_list = ImGui::GetWindowDrawList();
438 ImVec2 canvas_pos = editor_->ow_map_canvas_.zero_point();
439 ImVec2 scrolling = editor_->ow_map_canvas_.scrolling();
440 // Apply scrolling offset and use already-scaled map_x/map_y
441 ImVec2 placeholder_pos = ImVec2(canvas_pos.x + scrolling.x + map_x,
442 canvas_pos.y + scrolling.y + map_y);
443 // Scale the placeholder size to match zoomed maps
444 float scaled_size = kOverworldMapSize * scale;
445 ImVec2 placeholder_size = ImVec2(scaled_size, scaled_size);
446
447 // Modern loading indicator with theme colors
448 const auto& theme = AgentUI::GetTheme();
449 draw_list->AddRectFilled(
450 placeholder_pos,
451 ImVec2(placeholder_pos.x + placeholder_size.x,
452 placeholder_pos.y + placeholder_size.y),
453 ImGui::GetColorU32(
454 theme.editor_background)); // Theme-aware background
455
456 // Animated loading spinner - scale spinner radius with zoom
457 ImVec2 spinner_pos = ImVec2(placeholder_pos.x + placeholder_size.x / 2,
458 placeholder_pos.y + placeholder_size.y / 2);
459
460 const float spinner_radius = 8.0f * scale;
461 const float rotation = static_cast<float>(ImGui::GetTime()) * 3.0f;
462 const float start_angle = rotation;
463 const float end_angle = rotation + IM_PI * 1.5f;
464
465 draw_list->PathArcTo(spinner_pos, spinner_radius, start_angle, end_angle,
466 12);
467 draw_list->PathStroke(ImGui::GetColorU32(theme.status_active), 0,
468 2.5f * scale);
469 }
470 }
471}
472
473// =============================================================================
474// Panel Drawing Methods
475// =============================================================================
476
477absl::Status OverworldCanvasRenderer::DrawTile16Selector() {
478 if (!editor_->blockset_selector_) {
479 gui::TileSelectorWidget::Config selector_config;
480 const auto& theme = AgentUI::GetTheme();
481 selector_config.tile_size = 16;
482 selector_config.display_scale = kTile16SelectorScale;
483 selector_config.tiles_per_row = kTile16SelectorColumns;
485 selector_config.draw_offset = ImVec2(kTile16SelectorDrawOffsetX, 0.0f);
486 selector_config.highlight_color = theme.selection_primary;
487
488 selector_config.enable_drag = true;
489 selector_config.show_hover_tooltip = true;
490
491 editor_->blockset_selector_ = std::make_unique<gui::TileSelectorWidget>(
492 "OwBlocksetSelector", selector_config);
493 editor_->blockset_selector_->AttachCanvas(&editor_->blockset_canvas_);
494 // Only the selector's own items (Edit Tile16, Copy Tile ID) belong here;
495 // the canvas debug/scale items do not apply to a fixed tile grid.
496 editor_->blockset_canvas_.SetShowBuiltinContextMenu(false);
497 }
498
499 editor_->UpdateBlocksetSelectorState();
500 editor_->blockset_canvas_.ClearContextMenuItems();
501
502 auto open_tile16_editor = [this](int tile_id) {
503 editor_->RequestTile16Selection(tile_id);
504 if (editor_->dependencies_.window_manager) {
505 const size_t session_id =
506 editor_->dependencies_.window_manager->GetActiveSessionId();
507 editor_->dependencies_.window_manager->OpenWindowFloating(
508 session_id, OverworldPanelIds::kTile16Editor);
509 editor_->dependencies_.window_manager->MarkWindowRecentlyUsed(
510 OverworldPanelIds::kTile16Editor);
511 }
512 };
513
514 gui::CanvasMenuItem edit_tile_item;
515 edit_tile_item.label = "Edit Tile16";
516 edit_tile_item.icon = ICON_MD_GRID_VIEW;
517 edit_tile_item.shortcut = "Double-click";
518 edit_tile_item.enabled_condition = [this]() {
519 return editor_->blockset_selector_ &&
520 editor_->blockset_selector_->GetSelectedTileID() >= 0;
521 };
522 edit_tile_item.callback = [this, open_tile16_editor]() {
523 if (!editor_->blockset_selector_) {
524 return;
525 }
526 open_tile16_editor(editor_->blockset_selector_->GetSelectedTileID());
527 };
528 editor_->blockset_canvas_.AddContextMenuItem(edit_tile_item);
529 auto graphics_item = edit_tile_item;
530 graphics_item.label = "Edit Graphics...";
531 graphics_item.icon = ICON_MD_IMAGE;
532 graphics_item.callback = [this]() {
533 if (editor_->blockset_selector_) {
534 editor_->status_ = editor_->OpenGraphicsForTile16Quadrant(
535 editor_->blockset_selector_->GetSelectedTileID(), 0);
536 }
537 };
538 editor_->blockset_canvas_.AddContextMenuItem(std::move(graphics_item));
539 editor_->blockset_canvas_.AddContextMenuItem(
540 editor_->blockset_selector_->CopyTileIdMenuItem());
541
542 const float selector_available_width = ImGui::GetContentRegionAvail().x;
543 DrawSheetScaleControl("##Tile16Scale", &tile16_scale_mode_);
544 const int tile_rows =
547 const gui::AdaptiveSheetLayout selector_layout =
549 selector_available_width, 16 * kTile16SelectorColumns, 16 * tile_rows,
550 tile16_scale_mode_, 0.25f, 4.0f,
552 editor_->blockset_selector_->SetDisplayScale(selector_layout.display_scale);
553 ImGui::SameLine();
554 ImGui::TextDisabled("%.2fx", selector_layout.display_scale);
555
556 // Tile lookup and filtering share the scale row; only the grid scrolls.
557 // The child fills the panel width (the panel's preferred width is grid +
558 // scrollbar), has no padding or border, and never scrolls horizontally.
559 ImGui::SameLine();
560 if (editor_->blockset_selector_->DrawFilterBar()) {
561 editor_->RequestTile16Selection(
562 editor_->blockset_selector_->GetSelectedTileID());
563 }
564
565 // Padding is read at BeginChild; pop it right away so the grid's tooltip
566 // and context menu keep normal popup padding.
567 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
568 const bool grid_visible = ImGui::BeginChild(
569 "##Tile16Grid", ImVec2(0.0f, 0.0f), ImGuiChildFlags_None,
570 ImGuiWindowFlags_AlwaysVerticalScrollbar);
571 ImGui::PopStyleVar();
572 if (!grid_visible) {
573 ImGui::EndChild();
574 return absl::OkStatus();
575 }
576
577 gfx::Bitmap& atlas = editor_->tile16_blockset_.atlas;
578 bool atlas_ready = editor_->map_blockset_loaded_ && atlas.is_active();
579 auto result = editor_->blockset_selector_->Render(atlas, atlas_ready);
580
581 if (result.selection_changed) {
582 editor_->RequestTile16Selection(result.selected_tile);
583 // Note: We do NOT auto-scroll here because it breaks user interaction.
584 // The canvas should only scroll when explicitly requested (e.g., when
585 // selecting a tile from the overworld canvas via
586 // ScrollBlocksetCanvasToCurrentTile).
587 }
588
589 if (result.tile_double_clicked) {
590 open_tile16_editor(result.selected_tile);
591 }
592
593 ImGui::EndChild();
594 return absl::OkStatus();
595}
596
597void OverworldCanvasRenderer::DrawTile8Selector() {
598 // Configure canvas frame options for graphics bin
599 gui::CanvasFrameOptions frame_opts;
601 frame_opts.draw_grid = true;
602 frame_opts.grid_step = 16.0f; // Tile8 grid
603 frame_opts.draw_context_menu = true;
604 frame_opts.draw_overlay = true;
605 frame_opts.render_popups = true;
606 frame_opts.use_child_window = false;
607
608 auto canvas_rt = gui::BeginCanvas(editor_->graphics_bin_canvas_, frame_opts);
609
610 if (editor_->all_gfx_loaded_) {
611 int key = 0;
612 for (auto& value : gfx::Arena::Get().gfx_sheets()) {
613 int offset = 0x40 * (key + 1);
614 int top_left_y = canvas_rt.canvas_p0.y + 2;
615 if (key >= 1) {
616 top_left_y = canvas_rt.canvas_p0.y + 0x40 * key;
617 }
618 auto texture = value.texture();
619 canvas_rt.draw_list->AddImage(
620 (ImTextureID)(intptr_t)texture,
621 ImVec2(canvas_rt.canvas_p0.x + 2, top_left_y),
622 ImVec2(canvas_rt.canvas_p0.x + 0x100, canvas_rt.canvas_p0.y + offset),
623 ImVec2(0, 0), ImVec2(1, 1), ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
624 key++;
625 }
626 }
627
628 gui::EndCanvas(editor_->graphics_bin_canvas_, canvas_rt, frame_opts);
629}
630
631absl::Status OverworldCanvasRenderer::DrawAreaGraphics() {
632 if (editor_->overworld_.is_loaded()) {
633 // Always ensure current map graphics are loaded
634 if (!editor_->current_graphics_set_.contains(editor_->current_map_)) {
635 auto status = editor_->overworld_.EnsureMapBuilt(editor_->current_map_);
636 if (!status.ok()) {
637 return status;
638 }
639 const auto* map =
640 editor_->overworld_.overworld_map(editor_->current_map_);
641 if (!map) {
642 return absl::FailedPreconditionError(
643 "Current overworld map not loaded");
644 }
645 editor_->palette_ = map->current_palette();
646 auto bmp = std::make_unique<gfx::Bitmap>();
647 bmp->Create(0x80, kOverworldMapSize, 0x08, map->current_graphics());
648 bmp->SetPalette(editor_->palette_);
649 editor_->current_graphics_set_[editor_->current_map_] = std::move(bmp);
652 editor_->current_graphics_set_[editor_->current_map_].get());
653 }
654 }
655
656 const float available_width = ImGui::GetContentRegionAvail().x;
657 DrawSheetScaleControl("##AreaGraphicsScale", &area_graphics_scale_mode_);
659 available_width, 0x80, kOverworldMapSize, area_graphics_scale_mode_,
660 0.35f, 4.0f, ImGui::GetStyle().ScrollbarSize);
661 ImGui::SameLine();
662 ImGui::TextDisabled("%.2fx", sheet_layout.display_scale);
663
664 // Configure canvas frame options for area graphics
665 gui::CanvasFrameOptions frame_opts;
666 frame_opts.canvas_size = ImVec2(sheet_layout.displayed_width + 4.0f,
667 sheet_layout.displayed_height + 4.0f);
668 frame_opts.draw_grid = true;
669 frame_opts.grid_step = 16.0f * sheet_layout.display_scale;
670 frame_opts.draw_context_menu = true;
671 frame_opts.draw_overlay = true;
672 frame_opts.render_popups = true;
673 frame_opts.use_child_window = false;
674
676 ImGui::BeginGroup();
677 gui::BeginChildWithScrollbar("##AreaGraphicsScrollRegion");
678
679 auto canvas_rt = gui::BeginCanvas(editor_->current_gfx_canvas_, frame_opts);
681
682 if (editor_->current_graphics_set_.contains(editor_->current_map_) &&
683 editor_->current_graphics_set_[editor_->current_map_]->is_active()) {
684 editor_->current_gfx_canvas_.DrawBitmap(
685 *editor_->current_graphics_set_[editor_->current_map_], 2, 2,
686 sheet_layout.display_scale);
687 }
688 editor_->current_gfx_canvas_.DrawTileSelector(16.0f *
689 sheet_layout.display_scale);
690
691 gui::EndCanvas(editor_->current_gfx_canvas_, canvas_rt, frame_opts);
692 ImGui::EndChild();
693 ImGui::EndGroup();
694 return absl::OkStatus();
695}
696
697void OverworldCanvasRenderer::DrawV3Settings() {
698 // Lightweight v3 controls for map size and feature visibility.
699 ImGui::TextWrapped(tr("ZSCustomOverworld v3 settings"));
700 ImGui::Separator();
701
702 if (!editor_->rom_ || !editor_->rom_->is_loaded()) {
703 gui::CenterText("No ROM loaded");
704 return;
705 }
706
707 const uint8_t asm_version =
709 ImGui::Text(tr("ASM Version: 0x%02X"), asm_version);
710
711 if (asm_version == 0x00 || asm_version == 0xFF || asm_version < 0x03) {
712 ImGui::Spacing();
713 ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f),
714 tr("v3 controls require ZSCustomOverworld v3+"));
715 ImGui::TextDisabled(tr("Apply the v3 patch to enable map-size editing."));
716 return;
717 }
718
719 auto* map = editor_->overworld_.overworld_map(editor_->current_map_);
720 if (!map) {
721 ImGui::TextDisabled(tr("Current map is unavailable."));
722 return;
723 }
724
725 ImGui::Spacing();
726 ImGui::Text(tr("Current map: 0x%02X"), editor_->current_map_);
727 ImGui::Text(tr("Parent map: 0x%02X"), map->parent());
728
729 static int selected_area_size = 0;
730 selected_area_size = static_cast<int>(map->area_size());
731 const char* area_size_labels[] = {"Small", "Wide", "Tall", "Large"};
732 ImGui::SetNextItemWidth(220.0f);
733 ImGui::Combo(tr("Area Size"), &selected_area_size, area_size_labels,
734 IM_ARRAYSIZE(area_size_labels));
735
736 if (ImGui::Button(ICON_MD_SAVE " Apply Area Size")) {
737 auto status = editor_->overworld_.ConfigureMultiAreaMap(
738 map->parent(), static_cast<zelda3::AreaSizeEnum>(selected_area_size));
739 if (status.ok()) {
740 editor_->RefreshOverworldMap();
741 editor_->RefreshSiblingMapGraphics(editor_->current_map_, true);
742 }
743 }
744
745 ImGui::Spacing();
746 ImGui::TextDisabled(
747 tr("Area-size changes update sibling map relationships for this parent "
748 "map."));
749}
750
751void OverworldCanvasRenderer::FilterHoveredEntityByMode() {
752 auto* entity = editor_->entity_renderer_->hovered_entity();
753 if (!entity) {
754 return;
755 }
757 bool allowed = true;
758 switch (editor_->entity_edit_mode_) {
759 case EntityEditMode::ENTRANCES:
760 allowed = entity->entity_type_ == Type::kEntrance;
761 break;
762 case EntityEditMode::EXITS:
763 allowed = entity->entity_type_ == Type::kExit;
764 break;
765 case EntityEditMode::ITEMS:
766 allowed = entity->entity_type_ == Type::kItem;
767 break;
768 case EntityEditMode::SPRITES:
769 allowed = entity->entity_type_ == Type::kSprite;
770 break;
771 default:
772 break; // NONE / TRANSPORTS / MUSIC: every entity stays interactive.
773 }
774 if (!allowed) {
775 editor_->entity_renderer_->ResetHoveredEntity();
776 }
777}
778
779void OverworldCanvasRenderer::DrawMapProperties() {
780 // The Background Color and Visual Effects buttons open the same floating
781 // editors the canvas context menu uses (drawn in OverworldEditor::Update).
782 if (editor_->sidebar_) {
783 editor_->sidebar_->Draw(editor_->current_world_, editor_->current_map_,
784 editor_->current_map_lock_, editor_->game_state_,
785 editor_->show_custom_bg_color_editor_,
786 editor_->show_overlay_editor_,
787 editor_->dependencies_.project);
788 }
789}
790
791void OverworldCanvasRenderer::DrawOverworldProperties() {
792 static bool init_properties = false;
793
794 if (!init_properties) {
795 for (int i = 0; i < 0x40; i++) {
796 std::string area_graphics_str = absl::StrFormat(
797 "%02hX", editor_->overworld_.overworld_map(i)->area_graphics());
798 editor_->properties_canvas_
799 .mutable_labels(OverworldEditor::OverworldProperty::LW_AREA_GFX)
800 ->push_back(area_graphics_str);
801
802 area_graphics_str = absl::StrFormat(
803 "%02hX",
804 editor_->overworld_.overworld_map(i + 0x40)->area_graphics());
805 editor_->properties_canvas_
806 .mutable_labels(OverworldEditor::OverworldProperty::DW_AREA_GFX)
807 ->push_back(area_graphics_str);
808
809 std::string area_palette_str = absl::StrFormat(
810 "%02hX", editor_->overworld_.overworld_map(i)->area_palette());
811 editor_->properties_canvas_
812 .mutable_labels(OverworldEditor::OverworldProperty::LW_AREA_PAL)
813 ->push_back(area_palette_str);
814
815 area_palette_str = absl::StrFormat(
816 "%02hX", editor_->overworld_.overworld_map(i + 0x40)->area_palette());
817 editor_->properties_canvas_
818 .mutable_labels(OverworldEditor::OverworldProperty::DW_AREA_PAL)
819 ->push_back(area_palette_str);
820
821 std::string sprite_gfx_str = absl::StrFormat(
822 "%02hX", editor_->overworld_.overworld_map(i)->sprite_graphics(1));
823 editor_->properties_canvas_
824 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_GFX_PART1)
825 ->push_back(sprite_gfx_str);
826
827 sprite_gfx_str = absl::StrFormat(
828 "%02hX", editor_->overworld_.overworld_map(i)->sprite_graphics(2));
829 editor_->properties_canvas_
830 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_GFX_PART2)
831 ->push_back(sprite_gfx_str);
832
833 sprite_gfx_str = absl::StrFormat(
834 "%02hX",
835 editor_->overworld_.overworld_map(i + 0x40)->sprite_graphics(1));
836 editor_->properties_canvas_
837 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_GFX_PART1)
838 ->push_back(sprite_gfx_str);
839
840 sprite_gfx_str = absl::StrFormat(
841 "%02hX",
842 editor_->overworld_.overworld_map(i + 0x40)->sprite_graphics(2));
843 editor_->properties_canvas_
844 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_GFX_PART2)
845 ->push_back(sprite_gfx_str);
846
847 std::string sprite_palette_str = absl::StrFormat(
848 "%02hX", editor_->overworld_.overworld_map(i)->sprite_palette(1));
849 editor_->properties_canvas_
850 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_PAL_PART1)
851 ->push_back(sprite_palette_str);
852
853 sprite_palette_str = absl::StrFormat(
854 "%02hX", editor_->overworld_.overworld_map(i)->sprite_palette(2));
855 editor_->properties_canvas_
856 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_PAL_PART2)
857 ->push_back(sprite_palette_str);
858
859 sprite_palette_str = absl::StrFormat(
860 "%02hX",
861 editor_->overworld_.overworld_map(i + 0x40)->sprite_palette(1));
862 editor_->properties_canvas_
863 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_PAL_PART1)
864 ->push_back(sprite_palette_str);
865
866 sprite_palette_str = absl::StrFormat(
867 "%02hX",
868 editor_->overworld_.overworld_map(i + 0x40)->sprite_palette(2));
869 editor_->properties_canvas_
870 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_PAL_PART2)
871 ->push_back(sprite_palette_str);
872 }
873 init_properties = true;
874 }
875
876 ImGui::Text(tr("Area Gfx LW/DW"));
877 editor_->properties_canvas_.UpdateInfoGrid(
878 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::LW_AREA_GFX);
879 ImGui::SameLine();
880 editor_->properties_canvas_.UpdateInfoGrid(
881 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::DW_AREA_GFX);
882 ImGui::Separator();
883
884 ImGui::Text(tr("Sprite Gfx LW/DW"));
885 editor_->properties_canvas_.UpdateInfoGrid(
886 ImVec2(256, 256), 32,
887 OverworldEditor::OverworldProperty::LW_SPR_GFX_PART1);
888 ImGui::SameLine();
889 editor_->properties_canvas_.UpdateInfoGrid(
890 ImVec2(256, 256), 32,
891 OverworldEditor::OverworldProperty::DW_SPR_GFX_PART1);
892 ImGui::SameLine();
893 editor_->properties_canvas_.UpdateInfoGrid(
894 ImVec2(256, 256), 32,
895 OverworldEditor::OverworldProperty::LW_SPR_GFX_PART2);
896 ImGui::SameLine();
897 editor_->properties_canvas_.UpdateInfoGrid(
898 ImVec2(256, 256), 32,
899 OverworldEditor::OverworldProperty::DW_SPR_GFX_PART2);
900 ImGui::Separator();
901
902 ImGui::Text(tr("Area Pal LW/DW"));
903 editor_->properties_canvas_.UpdateInfoGrid(
904 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::LW_AREA_PAL);
905 ImGui::SameLine();
906 editor_->properties_canvas_.UpdateInfoGrid(
907 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::DW_AREA_PAL);
908
909 static bool show_gfx_group = false;
910 ImGui::Checkbox(tr("Show Gfx Group Editor"), &show_gfx_group);
911 if (show_gfx_group) {
912 gui::BeginWindowWithDisplaySettings("Gfx Group Editor", &show_gfx_group);
913 editor_->status_ = editor_->gfx_group_editor_.Update();
915 }
916}
917
918} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:187
EditorDependencies dependencies_
Definition editor.h:370
OverworldEditor * editor_
Non-owning pointer to the parent editor.
void DrawOverworldCanvas()
Draw the main overworld canvas with toolbar, maps, and entities. This is the primary entry point call...
bool PrepareContextMenu(const ImVec2 &screen_position)
Main UI class for editing overworld maps in A Link to the Past.
bool NormalizeCurrentSelectionState()
Handle overworld keyboard shortcuts and edit-mode hotkeys.
std::unique_ptr< OverworldToolbar > toolbar_
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
GraphicsSheets & gfx_sheets()
Definition arena.h:178
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
bool is_active() const
Definition bitmap.h:414
void SetContextMenuOpenCallback(std::function< bool(const ImVec2 &)> callback)
Definition canvas.h:204
void SetShowBuiltinContextMenu(bool show)
Definition canvas.h:220
static float CurrentScrollbarSize()
Current style scrollbar size, or ImGui's default without a context.
bool is_lyra_preview() const
Definition overworld.h:430
auto is_loaded() const
Definition overworld.h:803
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_SAVE
Definition icons.h:1644
void DrawSheetScaleControl(const char *id, gui::AdaptiveSheetScaleMode *mode)
Editors are the view controllers for the application.
constexpr ImVec2 kOverworldCanvasSize(kOverworldMapSize *8, kOverworldMapSize *8)
constexpr float kTile16SelectorScale
constexpr unsigned int kOverworldMapSize
constexpr int kTile16SelectorColumns
void MoveEntityOnGrid(zelda3::GameEntity *entity, ImVec2 canvas_p0, ImVec2 scrolling, bool free_movement, float scale)
Move entity to grid-aligned position based on mouse.
Definition entity.cc:66
constexpr float kTile16SelectorDrawOffsetX
constexpr ImVec2 kGraphicsBinCanvasSize(0x100+1, kNumSheetsToLoad *0x40+1)
void EndCanvas(Canvas &canvas)
void BeginPadding(int i)
Definition style.cc:296
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
bool AcceptTileDrop(TileDragPayload *out)
Accept a tile16 drop. Returns true if a payload was accepted.
Definition drag_drop.h:132
AdaptiveSheetLayout ResolveAdaptiveSheetLayout(float available_width, int source_width, int source_height, AdaptiveSheetScaleMode mode=AdaptiveSheetScaleMode::kFit, float min_scale=0.35f, float max_scale=4.0f, float horizontal_chrome=24.0f)
void EndNoPadding()
Definition style.cc:308
void CenterText(const char *text)
ImVec2 ScaledSize(float base_width, float base_height)
Returns a DPI-scaled version of a fixed pixel size.
Definition ui_config.h:148
void EndPadding()
Definition style.cc:300
void BeginNoPadding()
Definition style.cc:304
void EndWindowWithDisplaySettings()
Definition style.cc:291
void BeginWindowWithDisplaySettings(const char *id, bool *active, const ImVec2 &size, ImGuiWindowFlags flags)
Definition style.cc:270
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:312
constexpr int kNumTile16Individual
Definition overworld.h:243
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr int OverworldCustomASMHasBeenApplied
Definition common.h:89
int SubscreenOverlayScreen(uint16_t overlay_id)
#define IM_PI
project::YazeProject * project
Definition editor.h:174
ShortcutManager * shortcut_manager
Definition editor.h:186
WorkspaceWindowManager * window_manager
Definition editor.h:182
std::optional< float > grid_step
Declarative menu item definition.
Definition canvas_menu.h:64
std::function< bool()> enabled_condition
Definition canvas_menu.h:85
std::function< void()> callback
Definition canvas_menu.h:79