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
4#include "util/i18n/tr.h"
5
6#ifndef IM_PI
7#define IM_PI 3.14159265358979323846f
8#endif
9
10// C++ standard library headers
11#include <algorithm>
12#include <memory>
13#include <string>
14#include <vector>
15
16// Third-party library headers
17#include "absl/status/status.h"
18#include "absl/strings/ascii.h"
19#include "absl/strings/str_format.h"
20#include "imgui/imgui.h"
21
22// Project headers
33#include "app/gfx/core/bitmap.h"
40#include "app/gui/core/icons.h"
41#include "app/gui/core/style.h"
46#include "rom/rom.h"
47#include "util/log.h"
50
51namespace yaze::editor {
52
54 if (mode == nullptr) {
55 return;
56 }
57 constexpr const char* kLabels[] = {"Fit", "1x", "2x", "4x"};
58 int selected = static_cast<int>(*mode);
59 ImGui::AlignTextToFramePadding();
60 ImGui::TextDisabled("%s", tr("Scale"));
61 ImGui::SameLine();
62 ImGui::SetNextItemWidth(gui::ScaledSize(80.0f, 0.0f).x);
63 if (ImGui::Combo(id, &selected, kLabels, IM_ARRAYSIZE(kLabels))) {
64 *mode = static_cast<gui::AdaptiveSheetScaleMode>(selected);
65 }
66}
67
68namespace {} // namespace
69
71 : editor_(editor) {
72 if (editor_) {
75 [this](const ImVec2& position) {
76 return PrepareContextMenu(position);
77 });
78 }
79}
80
81// =============================================================================
82// Main Canvas Drawing
83// =============================================================================
84
86 if (!editor_) {
87 return;
88 }
89 const bool lyra_preview = editor_->overworld_.is_lyra_preview();
90 const bool lyra_writable = editor_->lyra_a0_terrain_writable();
91 if (lyra_preview) {
92 const auto layout = editor_->rom_->InspectLyraLayout();
93 ImGui::Text("Lyra ROM rev%d: %d stored / %d active / %s", layout.revision,
94 layout.capacity, layout.active_count,
95 lyra_writable ? "A0 terrain Save As" : "read-only");
96 ImGui::SameLine();
97 ImGui::TextDisabled("Sprite lists and area overlays unavailable");
98 int map_id = editor_->current_map_;
99 ImGui::SetNextItemWidth(210.0f);
100 if (ImGui::SliderInt("Inspect map", &map_id, 0x80, 0xBF, "0x%02X")) {
101 editor_->JumpToMap(map_id);
102 }
103 ImGui::SameLine();
104 ImGui::TextDisabled(
105 layout.offline_only
106 ? "Offline map data; gameplay requires an Oracle build"
107 : "Runtime-declared map data; inspection only");
108 if (lyra_writable) {
109 ImGui::RadioButton("Walkable floor", &editor_->lyra_terrain_brush_,
110 0x2103);
112 "OverworldLyra/radio:WalkableFloor", "radio", ImGui::GetItemID());
113 ImGui::SameLine();
114 ImGui::RadioButton("Blocking border", &editor_->lyra_terrain_brush_,
115 0x20B2);
117 "OverworldLyra/radio:BlockingBorder", "radio", ImGui::GetItemID());
118 ImGui::SameLine();
119 ImGui::TextDisabled(
120 "Click A0 terrain. Center entry/return cells stay walkable.");
121 }
122 }
123
124 // Keep appearance edits beside the canvas, using the same undo/refresh path
125 // as the full Map Properties panel.
126 if (!lyra_preview && editor_->rom_ != nullptr && editor_->rom_->is_loaded() &&
129 editor_->toolbar_->Draw(
134 editor_->game_state_, [this]() {
135 if (editor_->sidebar_) {
136 editor_->sidebar_->DrawQuickProperties(
137 editor_->current_map_, editor_->current_map_lock_,
138 editor_->dependencies_.project, /*compact=*/true);
139 }
140 });
142 }
143 if (!lyra_preview && editor_->current_world_ == 2 &&
144 !editor_->lyra_canvas_status_.ok()) {
145 ImGui::TextWrapped("%s", editor_->lyra_canvas_status_.ToString().c_str());
146 }
147
148 // ==========================================================================
149 // PHASE 3: Modern BeginCanvas/EndCanvas Pattern
150 // ==========================================================================
151 // Menu actions are captured by PrepareContextMenu only when it opens.
152 // Keep rendering an open menu even after entity hover changes. Paint modes
153 // keep right-click for sampling; Shift+right-click opens the menu there
154 // (PrepareContextMenu vetoes a plain right-click).
155 const bool show_context_menu = !lyra_preview;
156
157 // Configure canvas frame options
158 gui::CanvasFrameOptions frame_opts;
160 frame_opts.draw_grid = true;
161 frame_opts.draw_context_menu = show_context_menu;
162 frame_opts.draw_overlay = true;
163 frame_opts.render_popups = !lyra_preview;
164 frame_opts.use_child_window = false; // CRITICAL: Canvas has own pan logic
165
166 // Wrap in child window for scrollbars. The navigation manager owns all
167 // scrolling (wheel, drag pan, zoom anchoring), so ImGui's own wheel scroll
168 // is disabled on this child.
170 if (editor_->canvas_nav_) {
171 editor_->canvas_nav_->BeginCanvasViewport();
172 }
173 ImGui::BeginChild(ImGui::GetID(reinterpret_cast<void*>(intptr_t{7})),
174 ImGui::GetContentRegionAvail(), true,
175 ImGuiWindowFlags_AlwaysVerticalScrollbar |
176 ImGuiWindowFlags_AlwaysHorizontalScrollbar |
177 ImGuiWindowFlags_NoScrollWithMouse);
178
179 // Keep canvas scroll at 0 - ImGui's child window handles all scrolling
180 // The scrollbars scroll the child window which moves the entire canvas
181 editor_->ow_map_canvas_.set_scrolling(ImVec2(0, 0));
182
183 // Begin canvas frame - this handles DrawBackground + DrawContextMenu
184 auto canvas_rt = gui::BeginCanvas(editor_->ow_map_canvas_, frame_opts);
186
187 if (editor_->canvas_nav_) {
188 editor_->canvas_nav_->EndCanvasViewport(canvas_rt.hovered);
189 }
190
191 if (editor_->overworld_.is_loaded()) {
192 // Draw the 64 overworld map bitmaps
193 DrawOverworldMaps();
194 if (lyra_preview) {
195 editor_->status_ = editor_->CheckForCurrentMap();
196 if (lyra_writable && canvas_rt.hovered &&
197 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
198 const auto target = ResolveOverworldContextTarget(
199 editor_->current_world_, editor_->game_state_, ImGui::GetMousePos(),
200 canvas_rt.canvas_p0, canvas_rt.scrolling, canvas_rt.scale,
202 if (target && target->map_id == 0xA0) {
203 editor_->status_ = editor_->PaintLyraA0Terrain(
204 target->map_id,
205 static_cast<int>(target->world_position.x) % 512 / 32,
206 static_cast<int>(target->world_position.y) % 512 / 32,
207 static_cast<uint16_t>(editor_->lyra_terrain_brush_));
208 }
209 }
210 gui::EndCanvas(editor_->ow_map_canvas_, canvas_rt, frame_opts);
211 ImGui::EndChild();
212 return;
213 }
214
215 // The lower four Special World rows are project terrain. Navigation uses
216 // the same canvas, while these cells bypass the legacy ROM editing model.
217 editor_->status_ = editor_->CheckForCurrentMap();
218 gui::TileDragPayload tile_drop;
219 if (gui::AcceptTileDrop(&tile_drop)) {
220 editor_->RequestTile16Selection(tile_drop.tile_id);
221 editor_->current_mode = EditingMode::DRAW_TILE;
222 editor_->ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kTilePainting);
223 }
224 const bool placing_project_entity =
225 editor_->lyra_canvas_.entity_placement_active();
226 const bool project_terrain =
227 editor_->IsProjectTerrainMap(editor_->current_map_) ||
228 editor_->IsProjectTerrainMap(editor_->hovered_map_);
229 if (editor_->current_world_ == 2 && editor_->lyra_canvas_.loaded()) {
230 const auto assets = editor_->lyra_canvas_.PrepareMapAssets(
231 editor_->overworld_, editor_->game_state_);
232 if (!assets.ok())
233 editor_->status_ = assets;
234 editor_->lyra_canvas_.DrawSpecialWorld(canvas_rt);
235 if (editor_->show_entities_) {
236 editor_->lyra_canvas_.DrawEntities(canvas_rt, editor_->current_world_,
237 editor_->game_state_);
238 editor_->lyra_canvas_.HandleEntityInput(
239 canvas_rt, editor_->current_world_, editor_->overworld_,
240 editor_->undo_manager_);
241 }
242 if (editor_->IsProjectTerrainMap(editor_->current_map_)) {
243 const int local_map = editor_->current_map_ - 0x80;
244 editor_->ow_map_canvas_.DrawOutline(local_map % 8 * kOverworldMapSize,
245 local_map / 8 * kOverworldMapSize,
248 }
249 } else {
250 editor_->lyra_canvas_.FinishStroke(editor_->undo_manager_);
251 }
252 const bool tile_tool = !placing_project_entity &&
253 (editor_->current_mode == EditingMode::DRAW_TILE ||
254 editor_->current_mode == EditingMode::FILL_TILE);
255 if (!tile_tool && editor_->tile_painting_)
256 editor_->tile_painting_->SuspendCanvasGestures();
257
258 if (editor_->current_world_ != 2 && editor_->lyra_canvas_.loaded() &&
259 editor_->show_entities_) {
260 editor_->lyra_canvas_.DrawEntities(canvas_rt, editor_->current_world_,
261 editor_->game_state_);
262 editor_->lyra_canvas_.HandleEntityInput(
263 canvas_rt, editor_->current_world_, editor_->overworld_,
264 editor_->undo_manager_);
265 }
266 // Draw all entities using the new CanvasRuntime-based methods
267 if (editor_->entity_renderer_ && editor_->show_entities_) {
268 editor_->entity_renderer_->DrawExits(canvas_rt, editor_->current_world_);
269 editor_->entity_renderer_->DrawEntrances(canvas_rt,
270 editor_->current_world_);
271 editor_->entity_renderer_->DrawItems(canvas_rt, editor_->current_world_);
272 editor_->entity_renderer_->DrawSprites(canvas_rt, editor_->current_world_,
273 editor_->game_state_);
274 FilterHoveredEntityByMode();
275 DrawEntityHoverTooltip();
276 } else if (editor_->entity_renderer_) {
277 // Hidden entities can't be hovered, dragged or right-clicked.
278 editor_->entity_renderer_->ResetHoveredEntity();
279 }
280
281 // Area subscreen overlays are composited per map in DrawOverworldMaps()
282 // (toggle: show_overlay_preview_).
283
284 // Always refresh hover preview: when the canvas is not hovered this clears
285 // hovered_map_ so the status bar falls back to the selected map.
286 if (canvas_rt.hovered) {
287 editor_->HandleMapInteraction();
288 }
289
290 if (tile_tool) {
291 editor_->CheckForOverworldEdits();
292 }
293
294 if (placing_project_entity) {
295 gui::EndCanvas(editor_->ow_map_canvas_, canvas_rt, frame_opts);
296 ImGui::EndChild();
297 return;
298 }
299 // --- BEGIN ENTITY DRAG/DROP LOGIC ---
300 if (editor_->current_mode == EditingMode::MOUSE &&
301 editor_->TryPlaceArmedItem()) {
302 // The click placed the item chosen from Insert Item.
303 } else if (editor_->current_mode == EditingMode::MOUSE &&
304 editor_->entity_renderer_) {
305 auto hovered_entity = editor_->entity_renderer_->hovered_entity();
306
307 // 1. Initiate drag
308 if (!editor_->is_dragging_entity_ && hovered_entity &&
309 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
310 editor_->dragged_entity_ = hovered_entity;
311 entity_drag_origin_ = ImVec2(hovered_entity->x_, hovered_entity->y_);
312 editor_->is_dragging_entity_ = true;
313 editor_->drag_item_snapshot_.reset();
314 if (hovered_entity->entity_type_ ==
316 editor_->SelectItemByIdentity(
317 *static_cast<zelda3::OverworldItem*>(hovered_entity));
318 // Items have snapshot undo; capture before the drag moves it.
319 editor_->drag_item_snapshot_ = editor_->CaptureItemUndoSnapshot();
320 }
321 if (editor_->dragged_entity_->entity_type_ ==
323 editor_->dragged_entity_free_movement_ = true;
324 }
325 }
326
327 // 2. Update drag
328 if (editor_->is_dragging_entity_ && editor_->dragged_entity_ &&
329 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
330 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
331 ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
332 float scale = canvas_rt.scale;
333 if (scale > 0.0f) {
334 editor_->dragged_entity_->x_ += mouse_delta.x / scale;
335 editor_->dragged_entity_->y_ += mouse_delta.y / scale;
336 }
337 }
338
339 // 3. End drag
340 if (editor_->is_dragging_entity_ &&
341 ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
342 if (editor_->dragged_entity_) {
343 float end_scale = canvas_rt.scale;
344 MoveEntityOnGrid(editor_->dragged_entity_, canvas_rt.canvas_p0,
345 canvas_rt.scrolling,
346 editor_->dragged_entity_free_movement_, end_scale);
347 // Pass overworld context for proper area size detection
348 const int new_map = editor_->current_world_ * 64 +
349 editor_->dragged_entity_->x_ / 512 +
350 (editor_->dragged_entity_->y_ / 512) * 8;
351 if (new_map >= 0 && new_map < editor_->DisplayedMapCount()) {
352 editor_->dragged_entity_->UpdateMapProperties(new_map,
353 &editor_->overworld_);
354 } else {
355 editor_->dragged_entity_->UpdateMapProperties(
356 editor_->dragged_entity_->map_id_, &editor_->overworld_);
357 }
358 if (editor_->drag_item_snapshot_) {
359 // No-op if the item ended where it started.
360 editor_->PushItemUndoAction(
361 std::move(*editor_->drag_item_snapshot_),
362 "Move overworld item");
363 }
364 // TODO(overworld): entrances, exits and sprites have no undo action
365 // yet; their drags only mark the ROM dirty.
366 editor_->rom_->set_dirty(true);
367 }
368 editor_->drag_item_snapshot_.reset();
369 editor_->is_dragging_entity_ = false;
370 editor_->dragged_entity_ = nullptr;
371 editor_->dragged_entity_free_movement_ = false;
372 entity_drag_origin_.reset();
373 }
374 }
375 // --- END ENTITY DRAG/DROP LOGIC ---
376 }
377
378 // End canvas frame - draws grid/overlay based on frame_opts
379 gui::EndCanvas(editor_->ow_map_canvas_, canvas_rt, frame_opts);
380 ImGui::EndChild();
381 editor_->DrawItemInsertPopup();
382}
383
384// =============================================================================
385// Internal Canvas Drawing Helpers
386// =============================================================================
387
388// Draws one map the way the game layers it: the area backdrop color (palette
389// color 0, which the map bitmap keeps transparent), the map, then the area's
390// subscreen overlay layer. The layer is built in the map's own palette;
391// background overlays (sky, pyramid, lava) only cover backdrop pixels and are
392// drawn opaque, front overlays (fog, canopy, rain, curtains) at half alpha.
393void OverworldCanvasRenderer::DrawMapWithAreaLayers(int map_index, int map_x,
394 int map_y, float scale) {
395 auto& canvas = editor_->ow_map_canvas_;
396 auto& bitmap = editor_->maps_bmp_[map_index];
397 const float size = kOverworldMapSize * scale;
398 const ImVec2 origin(canvas.zero_point().x + canvas.scrolling().x + map_x,
399 canvas.zero_point().y + canvas.scrolling().y + map_y);
400
401 if (!bitmap.palette().empty()) {
402 const ImVec4 rgb = bitmap.palette()[0].rgb();
403 ImGui::GetWindowDrawList()->AddRectFilled(
404 origin, ImVec2(origin.x + size, origin.y + size),
405 IM_COL32(static_cast<int>(rgb.x), static_cast<int>(rgb.y),
406 static_cast<int>(rgb.z), 255));
407 }
408
409 canvas.DrawBitmap(bitmap, map_x, map_y, scale);
410
411 if (!editor_->show_overlay_preview_) {
412 return;
413 }
414 auto* layer = EnsureOverlayLayer(map_index);
415 if (layer && layer->bitmap.is_active() && layer->bitmap.texture()) {
416 canvas.DrawBitmap(layer->bitmap, map_x, map_y, scale,
417 layer->background ? 255 : 128);
418 }
419}
420
421// Returns the cached overlay layer for a map. The layer is built once per
422// map (building the map if the build cache evicted it) and rebuilt when the
423// map's pixels change while it is built, so an evicted map keeps its last
424// layer instead of being rebuilt every frame.
426OverworldCanvasRenderer::EnsureOverlayLayer(int map_index) {
427 const auto* map = editor_->overworld_.overworld_map(map_index);
428 if (!map) {
429 return nullptr;
430 }
431 const bool has_overlay =
432 zelda3::SubscreenOverlayScreen(map->render_subscreen_overlay()) >= 0;
433 auto it = overlay_layers_.find(map_index);
434 const bool missing =
435 it == overlay_layers_.end() || it->second.has_overlay != has_overlay;
436 const bool stale = !missing && map->is_built() &&
437 it->second.bitmap_serial != map->bitmap_serial();
438 if (missing || stale) {
439 auto& entry = overlay_layers_[map_index];
440 entry.has_overlay = has_overlay;
441 entry.bitmap_serial = map->bitmap_serial();
442 if (has_overlay) {
443 auto layer = editor_->overworld_.BuildSubscreenOverlayLayer(map_index);
444 if (!layer.ok() || layer->overlay_screen < 0) {
445 entry.has_overlay = false;
446 return nullptr;
447 }
448 map = editor_->overworld_.overworld_map(map_index);
449 entry.bitmap_serial = map->bitmap_serial();
450 entry.background = layer->background;
451 if (!entry.bitmap.is_active()) {
452 entry.bitmap.Create(kOverworldMapSize, kOverworldMapSize, 8,
453 layer->pixels);
454 } else {
455 entry.bitmap.set_data(layer->pixels);
456 }
457 entry.bitmap.SetPalette(map->current_palette());
459 entry.bitmap.texture() ? gfx::Arena::TextureCommandType::UPDATE
461 &entry.bitmap);
462 }
463 it = overlay_layers_.find(map_index);
464 }
465 if (!it->second.has_overlay) {
466 return nullptr;
467 }
468 return &it->second;
469}
470
471void OverworldCanvasRenderer::DrawOverworldMaps() {
472 // Get the current zoom scale for positioning and sizing
473 float scale = editor_->ow_map_canvas_.global_scale();
474 if (scale <= 0.0f)
475 scale = 1.0f;
476
477 // Build only maps that intersect the visible canvas, and at most a few per
478 // frame: building all 64 maps of a world in the first frame stalled the UI.
479 // Off-screen maps are built when scrolled into view; visible ones that are
480 // over the budget show the loading placeholder for a frame or two.
481 constexpr int kMaxMapBuildsPerFrame = 4;
482 int builds_this_frame = 0;
483 const ImVec2 clip_min = ImGui::GetWindowDrawList()->GetClipRectMin();
484 const ImVec2 clip_max = ImGui::GetWindowDrawList()->GetClipRectMax();
485 const ImVec2 canvas_origin(editor_->ow_map_canvas_.zero_point().x +
486 editor_->ow_map_canvas_.scrolling().x,
487 editor_->ow_map_canvas_.zero_point().y +
488 editor_->ow_map_canvas_.scrolling().y);
489 const float map_extent = kOverworldMapSize * scale;
490
491 for (int i = 0; i < 0x40; i++) {
492 const int xx = i % 8;
493 const int yy = i / 8;
494 int world_index = i + (editor_->current_world_ * 0x40);
495
496 // Bounds checking to prevent crashes
497 if (world_index < 0 ||
498 world_index >= editor_->overworld_.inspected_map_count()) {
499 continue; // Skip invalid map index
500 }
501
502 // Apply scale to positions for proper zoom support
503 int map_x = static_cast<int>(xx * kOverworldMapSize * scale);
504 int map_y = static_cast<int>(yy * kOverworldMapSize * scale);
505
506 const float left = canvas_origin.x + map_x;
507 const float top = canvas_origin.y + map_y;
508 if (left >= clip_max.x || top >= clip_max.y ||
509 left + map_extent <= clip_min.x || top + map_extent <= clip_min.y) {
510 continue; // Not visible
511 }
512
513 // Ensure visible maps are materialized on demand before drawing. A map
514 // whose bitmap exists only waits for its queued texture (cheap).
515 auto& map_bitmap = editor_->maps_bmp_[world_index];
516 if (!map_bitmap.is_active()) {
517 if (builds_this_frame < kMaxMapBuildsPerFrame) {
518 editor_->EnsureMapTexture(world_index);
519 ++builds_this_frame;
520 }
521 } else if (!map_bitmap.texture()) {
522 editor_->EnsureMapTexture(world_index);
523 }
524
525 // Only draw if the map has a valid texture AND is active (has bitmap data)
526 // The current_map_ check was causing crashes when hovering over unbuilt maps
527 // because the bitmap would be drawn before EnsureMapBuilt() was called
528 bool can_draw = editor_->maps_bmp_[world_index].texture() &&
529 editor_->maps_bmp_[world_index].is_active();
530
531 if (can_draw) {
532 DrawMapWithAreaLayers(world_index, map_x, map_y, scale);
533 } else {
534 // Draw a placeholder for maps that haven't loaded yet
535 ImDrawList* draw_list = ImGui::GetWindowDrawList();
536 ImVec2 canvas_pos = editor_->ow_map_canvas_.zero_point();
537 ImVec2 scrolling = editor_->ow_map_canvas_.scrolling();
538 // Apply scrolling offset and use already-scaled map_x/map_y
539 ImVec2 placeholder_pos = ImVec2(canvas_pos.x + scrolling.x + map_x,
540 canvas_pos.y + scrolling.y + map_y);
541 // Scale the placeholder size to match zoomed maps
542 float scaled_size = kOverworldMapSize * scale;
543 ImVec2 placeholder_size = ImVec2(scaled_size, scaled_size);
544
545 // Modern loading indicator with theme colors
546 const auto& theme = AgentUI::GetTheme();
547 draw_list->AddRectFilled(
548 placeholder_pos,
549 ImVec2(placeholder_pos.x + placeholder_size.x,
550 placeholder_pos.y + placeholder_size.y),
551 ImGui::GetColorU32(
552 theme.editor_background)); // Theme-aware background
553
554 // Animated loading spinner - scale spinner radius with zoom
555 ImVec2 spinner_pos = ImVec2(placeholder_pos.x + placeholder_size.x / 2,
556 placeholder_pos.y + placeholder_size.y / 2);
557
558 const float spinner_radius = 8.0f * scale;
559 const float rotation = static_cast<float>(ImGui::GetTime()) * 3.0f;
560 const float start_angle = rotation;
561 const float end_angle = rotation + IM_PI * 1.5f;
562
563 draw_list->PathArcTo(spinner_pos, spinner_radius, start_angle, end_angle,
564 12);
565 draw_list->PathStroke(ImGui::GetColorU32(theme.status_active), 0,
566 2.5f * scale);
567 }
568 }
569}
570
571// =============================================================================
572// Panel Drawing Methods
573// =============================================================================
574
575absl::Status OverworldCanvasRenderer::DrawTile16Selector() {
576 if (!editor_->blockset_selector_) {
577 gui::TileSelectorWidget::Config selector_config;
578 const auto& theme = AgentUI::GetTheme();
579 selector_config.tile_size = 16;
580 selector_config.display_scale = kTile16SelectorScale;
581 selector_config.tiles_per_row = kTile16SelectorColumns;
583 selector_config.draw_offset = ImVec2(kTile16SelectorDrawOffsetX, 0.0f);
584 selector_config.highlight_color = theme.selection_primary;
585
586 selector_config.enable_drag = true;
587 selector_config.show_hover_tooltip = true;
588
589 editor_->blockset_selector_ = std::make_unique<gui::TileSelectorWidget>(
590 "OwBlocksetSelector", selector_config);
591 editor_->blockset_selector_->AttachCanvas(&editor_->blockset_canvas_);
592 // Only the selector's own items (Edit Tile16, Copy Tile ID) belong here;
593 // the canvas debug/scale items do not apply to a fixed tile grid.
594 editor_->blockset_canvas_.SetShowBuiltinContextMenu(false);
595 editor_->blockset_selector_->SetHoverDetailProvider(
596 [this](int tile_id) -> std::string {
597 if (tile_id < 0 ||
598 tile_id >= static_cast<int>(tile16_usage_counts_.size())) {
599 return {};
600 }
601 return absl::StrFormat("Placed %d times (%s)",
602 tile16_usage_counts_[tile_id],
603 Tile16ScopeLabel());
604 });
605 }
606
607 editor_->UpdateBlocksetSelectorState();
608 editor_->blockset_canvas_.ClearContextMenuItems();
609
610 auto open_tile16_editor = [this](int tile_id) {
611 editor_->RequestTile16Selection(tile_id);
612 if (editor_->dependencies_.window_manager) {
613 const size_t session_id =
614 editor_->dependencies_.window_manager->GetActiveSessionId();
615 editor_->dependencies_.window_manager->OpenWindowFloating(
616 session_id, OverworldPanelIds::kTile16Editor);
617 editor_->dependencies_.window_manager->MarkWindowRecentlyUsed(
618 OverworldPanelIds::kTile16Editor);
619 }
620 };
621
622 gui::CanvasMenuItem edit_tile_item;
623 edit_tile_item.label = "Edit Tile16";
624 edit_tile_item.icon = ICON_MD_GRID_VIEW;
625 edit_tile_item.shortcut = "Double-click";
626 edit_tile_item.enabled_condition = [this]() {
627 return editor_->blockset_selector_ &&
628 editor_->blockset_selector_->GetSelectedTileID() >= 0;
629 };
630 edit_tile_item.callback = [this, open_tile16_editor]() {
631 if (!editor_->blockset_selector_) {
632 return;
633 }
634 open_tile16_editor(editor_->blockset_selector_->GetSelectedTileID());
635 };
636 editor_->blockset_canvas_.AddContextMenuItem(edit_tile_item);
637 auto graphics_item = edit_tile_item;
638 graphics_item.label = "Edit Graphics...";
639 graphics_item.icon = ICON_MD_IMAGE;
640 graphics_item.callback = [this]() {
641 if (editor_->blockset_selector_) {
642 editor_->status_ = editor_->OpenGraphicsForTile16Quadrant(
643 editor_->blockset_selector_->GetSelectedTileID(), 0);
644 }
645 };
646 editor_->blockset_canvas_.AddContextMenuItem(std::move(graphics_item));
647 editor_->blockset_canvas_.AddContextMenuItem(
648 editor_->blockset_selector_->CopyTileIdMenuItem());
649
650 const float selector_available_width = ImGui::GetContentRegionAvail().x;
651 DrawSheetScaleControl("##Tile16Scale", &tile16_scale_mode_);
652 const int tile_rows =
654 kTile16SelectorColumns;
655 const gui::AdaptiveSheetLayout selector_layout =
657 selector_available_width, 16 * kTile16SelectorColumns, 16 * tile_rows,
658 tile16_scale_mode_, 0.25f, 4.0f,
660 editor_->blockset_selector_->SetDisplayScale(selector_layout.display_scale);
661 ImGui::SameLine();
662 ImGui::TextDisabled("%.2fx", selector_layout.display_scale);
663
664 // Tile lookup and filtering share the scale row; only the grid scrolls.
665 // The child fills the panel width (the panel's preferred width is grid +
666 // scrollbar), has no padding or border, and never scrolls horizontally.
667 ImGui::SameLine();
668 if (editor_->blockset_selector_->DrawFilterBar()) {
669 editor_->RequestTile16Selection(
670 editor_->blockset_selector_->GetSelectedTileID());
671 }
672 DrawTile16ViewControls();
673 RefreshTile16Order(tile16_grid_hovered_);
674
675 // Padding is read at BeginChild; pop it right away so the grid's tooltip
676 // and context menu keep normal popup padding.
677 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
678 const bool grid_visible = ImGui::BeginChild(
679 "##Tile16Grid", ImVec2(0.0f, 0.0f), ImGuiChildFlags_None,
680 ImGuiWindowFlags_AlwaysVerticalScrollbar);
681 ImGui::PopStyleVar();
682 if (!grid_visible) {
683 ImGui::EndChild();
684 return absl::OkStatus();
685 }
686
687 const bool project_terrain =
688 editor_->IsProjectTerrainMap(editor_->current_map_);
689 gfx::Bitmap& atlas =
690 project_terrain ? editor_->lyra_canvas_.MapAtlas(editor_->current_map_)
691 : editor_->tile16_blockset_.atlas;
692 bool atlas_ready =
693 atlas.is_active() && (project_terrain || editor_->map_blockset_loaded_);
694 // Adaptive tile cells can extend beyond the legacy fixed canvas bounds.
695 // Size this selector's hit area to its displayed grid, including filters.
696 const ImVec2 grid_size = editor_->blockset_selector_->GetGridContentSize();
697 const float canvas_scale = editor_->blockset_canvas_.global_scale();
698 editor_->blockset_canvas_.SetCanvasSize(
699 ImVec2(grid_size.x / canvas_scale, grid_size.y / canvas_scale));
700 auto result = editor_->blockset_selector_->Render(atlas, atlas_ready);
701
702 if (result.selection_changed) {
703 editor_->RequestTile16Selection(result.selected_tile);
704 // Note: We do NOT auto-scroll here because it breaks user interaction.
705 // The canvas should only scroll when explicitly requested (e.g., when
706 // selecting a tile from the overworld canvas via
707 // ScrollBlocksetCanvasToCurrentTile).
708 }
709
710 if (result.tile_double_clicked) {
711 open_tile16_editor(result.selected_tile);
712 }
713
714 tile16_grid_hovered_ = ImGui::IsWindowHovered();
715 ImGui::EndChild();
716 return absl::OkStatus();
717}
718
719std::string OverworldCanvasRenderer::Tile16ScopeLabel() const {
720 switch (tile16_scope_) {
721 case Tile16UsageScope::kAllMaps:
722 return tr("All maps");
723 case Tile16UsageScope::kCurrentWorld:
724 switch (editor_->current_world_) {
725 case 1:
726 return tr("Dark World");
727 case 2:
728 return tr("Special World");
729 default:
730 return tr("Light World");
731 }
732 case Tile16UsageScope::kCurrentArea:
733 return tr("This area");
734 case Tile16UsageScope::kArea:
735 return absl::StrFormat("%02X %s", tile16_scope_area_,
736 zelda3::GetOverworldMapLabel(tile16_scope_area_));
737 }
738 return {};
739}
740
741std::vector<int> OverworldCanvasRenderer::Tile16ScopeMaps() const {
742 const auto& maps = editor_->overworld_.overworld_maps();
743 const int map_count = static_cast<int>(maps.size());
744 std::vector<int> map_ids;
745 map_ids.reserve(map_count);
746 switch (tile16_scope_) {
747 case Tile16UsageScope::kAllMaps:
748 for (int id = 0; id < map_count; ++id) {
749 map_ids.push_back(id);
750 }
751 break;
752 case Tile16UsageScope::kCurrentWorld:
753 for (int id = 0; id < map_count; ++id) {
754 if (std::min(id / 0x40, 2) == editor_->current_world_) {
755 map_ids.push_back(id);
756 }
757 }
758 break;
759 case Tile16UsageScope::kCurrentArea:
760 case Tile16UsageScope::kArea: {
761 const int anchor = tile16_scope_ == Tile16UsageScope::kArea
762 ? tile16_scope_area_
763 : editor_->current_map_;
764 if (anchor < 0 || anchor >= map_count) {
765 break;
766 }
767 const int parent = maps[anchor].parent();
768 for (int id = 0; id < map_count; ++id) {
769 if (maps[id].parent() == parent) {
770 map_ids.push_back(id);
771 }
772 }
773 break;
774 }
775 }
776 return map_ids;
777}
778
779void OverworldCanvasRenderer::RefreshTile16Order(bool grid_hovered) {
780 constexpr double kRecountIntervalSeconds = 0.5;
781 auto* selector = editor_->blockset_selector_.get();
782 if (selector == nullptr) {
783 return;
784 }
785 if (!editor_->overworld_.is_loaded()) {
786 ResetTile16Order();
787 return;
788 }
789
790 const auto* current_map =
791 editor_->overworld_.overworld_map(editor_->current_map_);
792 const int current_parent =
793 current_map != nullptr ? current_map->parent() : editor_->current_map_;
794 const bool inputs_changed =
795 (tile16_scope_ == Tile16UsageScope::kCurrentWorld &&
796 editor_->current_world_ != tile16_counted_world_) ||
797 (tile16_scope_ == Tile16UsageScope::kCurrentArea &&
798 current_parent != tile16_counted_parent_);
799 const double now = ImGui::GetTime();
800 const bool busy = grid_hovered || ImGui::IsAnyMouseDown();
801 const bool stale = now - tile16_last_count_time_ >= kRecountIntervalSeconds;
802 if (!tile16_order_dirty_ && (busy || (!inputs_changed && !stale))) {
803 return;
804 }
805
806 std::vector<int> counts(zelda3::kNumTile16Individual, 0);
807 for (const int map_id : Tile16ScopeMaps()) {
808 if (map_id <
809 static_cast<int>(editor_->overworld_.overworld_maps().size())) {
811 editor_->overworld_.GetMapTiles(std::min(map_id / 0x40, 2)),
812 map_id % 0x40, &counts);
813 }
814 }
815 if (editor_->lyra_canvas_.loaded() && editor_->lyra_canvas_.document()) {
816 const auto* doc = editor_->lyra_canvas_.document();
817 const auto& lyra_maps = doc->maps();
818 for (int m = 0; m < core::LyraTerrainDocument::kMapCount; ++m) {
819 const int map_id = core::LyraTerrainDocument::kFirstMap + m;
820 bool include_map = false;
821 if (tile16_scope_ == Tile16UsageScope::kAllMaps) {
822 include_map = true;
823 } else if (tile16_scope_ == Tile16UsageScope::kCurrentWorld &&
824 editor_->current_world_ == 2) {
825 include_map = true;
826 } else if (tile16_scope_ == Tile16UsageScope::kCurrentArea &&
827 editor_->current_map_ == map_id) {
828 include_map = true;
829 } else if (tile16_scope_ == Tile16UsageScope::kArea &&
830 tile16_scope_area_ == map_id) {
831 include_map = true;
832 }
833 if (include_map) {
834 for (uint16_t tile : lyra_maps[m]) {
835 if (tile < counts.size()) {
836 counts[tile]++;
837 }
838 }
839 }
840 }
841 }
842 tile16_last_count_time_ = now;
843 tile16_counted_world_ = editor_->current_world_;
844 tile16_counted_parent_ = current_parent;
845 const bool order_changed =
846 tile16_order_dirty_ || counts != tile16_usage_counts_;
847 tile16_usage_counts_ = std::move(counts);
848 tile16_order_dirty_ = false;
849 if (!order_changed) {
850 return;
851 }
852
853 if (tile16_sort_ == zelda3::Tile16SortMode::kTileId && !tile16_hide_unused_) {
854 selector->ClearDisplayOrder();
855 } else {
856 selector->SetDisplayOrder(zelda3::BuildTile16DisplayOrder(
857 tile16_usage_counts_, tile16_sort_, tile16_hide_unused_));
858 }
859}
860
861void OverworldCanvasRenderer::ResetTile16Order() {
862 if (editor_->blockset_selector_) {
863 editor_->blockset_selector_->ClearDisplayOrder();
864 }
865 tile16_usage_counts_.clear();
866 tile16_order_dirty_ = true;
867}
868
869void OverworldCanvasRenderer::DrawTile16ViewControls() {
870 constexpr const char* kSortLabels[] = {"Most used", "Tile ID"};
871 int sort = tile16_sort_ == zelda3::Tile16SortMode::kUsage ? 0 : 1;
872 ImGui::AlignTextToFramePadding();
873 ImGui::TextDisabled("%s", tr("Sort"));
874 ImGui::SameLine();
875 ImGui::SetNextItemWidth(gui::ScaledSize(96.0f, 0.0f).x);
876 if (ImGui::Combo("##Tile16Sort", &sort, kSortLabels,
877 IM_ARRAYSIZE(kSortLabels))) {
878 tile16_sort_ = sort == 0 ? zelda3::Tile16SortMode::kUsage
880 tile16_order_dirty_ = true;
881 }
882 gui::AutoRegisterLastItem("combo", "tile16_sort");
883
884 ImGui::SameLine();
885 std::string scope_label = Tile16ScopeLabel();
886 constexpr size_t kMaxScopeButtonChars = 18;
887 if (scope_label.size() > kMaxScopeButtonChars) {
888 scope_label = scope_label.substr(0, kMaxScopeButtonChars - 3) + "...";
889 }
890 const std::string button_label =
891 absl::StrFormat("%s %s%s###Tile16UsageScopeButton", ICON_MD_FILTER_LIST,
892 scope_label, tile16_hide_unused_ ? "*" : "");
893 if (ImGui::Button(button_label.c_str())) {
894 ImGui::OpenPopup("Tile16UsageScope");
895 }
896 gui::AutoRegisterLastItem("button", "tile16_usage_scope");
897 if (ImGui::IsItemHovered()) {
898 ImGui::SetTooltip(tr("Maps that usage counts and \"Hide unused\" use"));
899 }
900
901 ImGui::SetNextWindowSizeConstraints(ImVec2(260.0f, 0.0f),
902 ImVec2(380.0f, 480.0f));
903 if (ImGui::BeginPopup("Tile16UsageScope")) {
904 ImGui::TextDisabled("%s", tr("Count placements in"));
905 const auto scope_radio = [this](const char* label, Tile16UsageScope scope) {
906 if (ImGui::RadioButton(label, tile16_scope_ == scope)) {
907 tile16_scope_ = scope;
908 tile16_order_dirty_ = true;
909 }
910 };
911 scope_radio(tr("All maps"), Tile16UsageScope::kAllMaps);
912 scope_radio(tr("Current world"), Tile16UsageScope::kCurrentWorld);
913 scope_radio(tr("Current area"), Tile16UsageScope::kCurrentArea);
914 if (ImGui::Checkbox(tr("Hide unused tiles"), &tile16_hide_unused_)) {
915 tile16_order_dirty_ = true;
916 }
917 ImGui::Separator();
918 DrawTile16AreaPicker();
919 ImGui::EndPopup();
920 }
921}
922
923void OverworldCanvasRenderer::DrawTile16AreaPicker() {
924 ImGui::TextDisabled("%s", tr("Area"));
925 ImGui::SetNextItemWidth(-1.0f);
926 ImGui::InputTextWithHint("##Tile16AreaSearch",
927 tr("Search map names or IDs..."),
928 tile16_area_search_, sizeof(tile16_area_search_));
929 const std::string needle = absl::AsciiStrToLower(tile16_area_search_);
930
931 if (ImGui::BeginChild("##Tile16AreaList",
932 ImVec2(0.0f, gui::ScaledSize(0.0f, 200.0f).y),
933 ImGuiChildFlags_Borders)) {
934 const auto& maps = editor_->overworld_.overworld_maps();
935 for (int id = 0; id < static_cast<int>(maps.size()); ++id) {
936 if (maps[id].parent() != id) {
937 continue; // One row per area; large areas list only their parent.
938 }
939 const char* world_tag = id < 0x40 ? "LW" : (id < 0x80 ? "DW" : "SW");
940 const std::string row = absl::StrFormat("%s %02X %s", world_tag, id,
942 if (!needle.empty() &&
943 absl::AsciiStrToLower(row).find(needle) == std::string::npos) {
944 continue;
945 }
946 const bool selected =
947 tile16_scope_ == Tile16UsageScope::kArea && tile16_scope_area_ == id;
948 if (ImGui::Selectable(row.c_str(), selected)) {
949 tile16_scope_ = Tile16UsageScope::kArea;
950 tile16_scope_area_ = id;
951 tile16_order_dirty_ = true;
952 ImGui::CloseCurrentPopup();
953 }
954 }
955 }
956 ImGui::EndChild();
957}
958
959void OverworldCanvasRenderer::DrawTile8Selector() {
960 // Configure canvas frame options for graphics bin
961 gui::CanvasFrameOptions frame_opts;
963 frame_opts.draw_grid = true;
964 frame_opts.grid_step = 16.0f; // Tile8 grid
965 frame_opts.draw_context_menu = true;
966 frame_opts.draw_overlay = true;
967 frame_opts.render_popups = true;
968 frame_opts.use_child_window = false;
969
970 auto canvas_rt = gui::BeginCanvas(editor_->graphics_bin_canvas_, frame_opts);
971
972 if (editor_->all_gfx_loaded_) {
973 int key = 0;
974 for (auto& value : gfx::Arena::Get().gfx_sheets()) {
975 int offset = 0x40 * (key + 1);
976 int top_left_y = canvas_rt.canvas_p0.y + 2;
977 if (key >= 1) {
978 top_left_y = canvas_rt.canvas_p0.y + 0x40 * key;
979 }
980 auto texture = value.texture();
981 canvas_rt.draw_list->AddImage(
982 (ImTextureID)(intptr_t)texture,
983 ImVec2(canvas_rt.canvas_p0.x + 2, top_left_y),
984 ImVec2(canvas_rt.canvas_p0.x + 0x100, canvas_rt.canvas_p0.y + offset),
985 ImVec2(0, 0), ImVec2(1, 1), ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
986 key++;
987 }
988 }
989
990 gui::EndCanvas(editor_->graphics_bin_canvas_, canvas_rt, frame_opts);
991}
992
993absl::Status OverworldCanvasRenderer::DrawAreaGraphics() {
994 const int graphics_map = editor_->IsProjectTerrainMap(editor_->current_map_)
995 ? 0x3A
996 : editor_->current_map_;
997 const bool project_graphics =
998 editor_->IsProjectTerrainMap(editor_->current_map_);
999 if (project_graphics) {
1000 RETURN_IF_ERROR(editor_->lyra_canvas_.PrepareMapAssets(
1001 editor_->overworld_, editor_->game_state_));
1002 }
1003 if (!project_graphics && editor_->overworld_.is_loaded()) {
1004 // Always ensure current map graphics are loaded
1005 if (!editor_->current_graphics_set_.contains(graphics_map)) {
1006 auto status = editor_->overworld_.EnsureMapBuilt(graphics_map);
1007 if (!status.ok()) {
1008 return status;
1009 }
1010 const auto* map = editor_->overworld_.overworld_map(graphics_map);
1011 if (!map) {
1012 return absl::FailedPreconditionError(
1013 "Current overworld map not loaded");
1014 }
1015 editor_->palette_ = map->current_palette();
1016 auto bmp = std::make_unique<gfx::Bitmap>();
1017 bmp->Create(0x80, kOverworldMapSize, 0x08, map->current_graphics());
1018 bmp->SetPalette(editor_->palette_);
1019 editor_->current_graphics_set_[graphics_map] = std::move(bmp);
1022 editor_->current_graphics_set_[graphics_map].get());
1023 }
1024 }
1025
1026 const float available_width = ImGui::GetContentRegionAvail().x;
1027 DrawSheetScaleControl("##AreaGraphicsScale", &area_graphics_scale_mode_);
1029 available_width, 0x80, kOverworldMapSize, area_graphics_scale_mode_,
1030 0.35f, 4.0f, ImGui::GetStyle().ScrollbarSize);
1031 ImGui::SameLine();
1032 ImGui::TextDisabled("%.2fx", sheet_layout.display_scale);
1033
1034 // Configure canvas frame options for area graphics
1035 gui::CanvasFrameOptions frame_opts;
1036 frame_opts.canvas_size = ImVec2(sheet_layout.displayed_width + 4.0f,
1037 sheet_layout.displayed_height + 4.0f);
1038 frame_opts.draw_grid = true;
1039 frame_opts.grid_step = 16.0f * sheet_layout.display_scale;
1040 frame_opts.draw_context_menu = true;
1041 frame_opts.draw_overlay = true;
1042 frame_opts.render_popups = true;
1043 frame_opts.use_child_window = false;
1044
1046 ImGui::BeginGroup();
1047 gui::BeginChildWithScrollbar("##AreaGraphicsScrollRegion");
1048
1049 auto canvas_rt = gui::BeginCanvas(editor_->current_gfx_canvas_, frame_opts);
1051
1052 if (project_graphics &&
1053 editor_->lyra_canvas_.MapGraphics(editor_->current_map_)) {
1054 editor_->current_gfx_canvas_.DrawBitmap(
1055 *editor_->lyra_canvas_.MapGraphics(editor_->current_map_), 2, 2,
1056 sheet_layout.display_scale);
1057 } else if (editor_->current_graphics_set_.contains(graphics_map) &&
1058 editor_->current_graphics_set_[graphics_map]->is_active()) {
1059 editor_->current_gfx_canvas_.DrawBitmap(
1060 *editor_->current_graphics_set_[graphics_map], 2, 2,
1061 sheet_layout.display_scale);
1062 }
1063 editor_->current_gfx_canvas_.DrawTileSelector(16.0f *
1064 sheet_layout.display_scale);
1065
1066 gui::EndCanvas(editor_->current_gfx_canvas_, canvas_rt, frame_opts);
1067 ImGui::EndChild();
1068 ImGui::EndGroup();
1069 return absl::OkStatus();
1070}
1071
1072void OverworldCanvasRenderer::DrawV3Settings() {
1073 // Lightweight v3 controls for map size and feature visibility.
1074 ImGui::TextWrapped(tr("ZSCustomOverworld v3 settings"));
1075 ImGui::Separator();
1076
1077 if (!editor_->rom_ || !editor_->rom_->is_loaded()) {
1078 gui::CenterText("No ROM loaded");
1079 return;
1080 }
1081
1082 const uint8_t asm_version =
1084 ImGui::Text(tr("ASM Version: 0x%02X"), asm_version);
1085
1086 if (asm_version == 0x00 || asm_version == 0xFF || asm_version < 0x03) {
1087 ImGui::Spacing();
1088 ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f),
1089 tr("v3 controls require ZSCustomOverworld v3+"));
1090 ImGui::TextDisabled(tr("Apply the v3 patch to enable map-size editing."));
1091 return;
1092 }
1093
1094 auto* map = editor_->overworld_.overworld_map(editor_->current_map_);
1095 if (!map) {
1096 ImGui::TextDisabled(tr("Current map is unavailable."));
1097 return;
1098 }
1099
1100 ImGui::Spacing();
1101 ImGui::Text(tr("Current map: 0x%02X"), editor_->current_map_);
1102 ImGui::Text(tr("Parent map: 0x%02X"), map->parent());
1103
1104 static int selected_area_size = 0;
1105 selected_area_size = static_cast<int>(map->area_size());
1106 const char* area_size_labels[] = {"Small", "Wide", "Tall", "Large"};
1107 ImGui::SetNextItemWidth(220.0f);
1108 ImGui::Combo(tr("Area Size"), &selected_area_size, area_size_labels,
1109 IM_ARRAYSIZE(area_size_labels));
1110
1111 if (ImGui::Button(ICON_MD_SAVE " Apply Area Size")) {
1112 auto status = editor_->overworld_.ConfigureMultiAreaMap(
1113 map->parent(), static_cast<zelda3::AreaSizeEnum>(selected_area_size));
1114 if (status.ok()) {
1115 editor_->RefreshOverworldMap();
1116 editor_->RefreshSiblingMapGraphics(editor_->current_map_, true);
1117 }
1118 }
1119
1120 ImGui::Spacing();
1121 ImGui::TextDisabled(
1122 tr("Area-size changes update sibling map relationships for this parent "
1123 "map."));
1124}
1125
1126void OverworldCanvasRenderer::FilterHoveredEntityByMode() {
1127 auto* entity = editor_->entity_renderer_->hovered_entity();
1128 if (!entity) {
1129 return;
1130 }
1131 using Type = zelda3::GameEntity::EntityType;
1132 bool allowed = true;
1133 switch (editor_->entity_edit_mode_) {
1134 case EntityEditMode::ENTRANCES:
1135 allowed = entity->entity_type_ == Type::kEntrance;
1136 break;
1137 case EntityEditMode::EXITS:
1138 allowed = entity->entity_type_ == Type::kExit;
1139 break;
1140 case EntityEditMode::ITEMS:
1141 allowed = entity->entity_type_ == Type::kItem;
1142 break;
1143 case EntityEditMode::SPRITES:
1144 allowed = entity->entity_type_ == Type::kSprite;
1145 break;
1146 default:
1147 break; // NONE / TRANSPORTS / MUSIC: every entity stays interactive.
1148 }
1149 if (!allowed) {
1150 editor_->entity_renderer_->ResetHoveredEntity();
1151 }
1152}
1153
1154void OverworldCanvasRenderer::DrawEntityHoverTooltip() {
1155 if (!editor_->entity_renderer_ ||
1156 !editor_->ow_map_canvas_.IsMouseHovering()) {
1157 return;
1158 }
1159 const auto* entity = editor_->entity_renderer_->hovered_entity();
1160 if (!entity) {
1161 return;
1162 }
1163 if (ImGui::BeginTooltip()) {
1164 switch (entity->entity_type_) {
1166 const auto* entrance =
1167 static_cast<const zelda3::OverworldEntrance*>(entity);
1168 ImGui::Text("%s", entrance->is_hole_ ? ICON_MD_CYCLONE " Hole"
1169 : ICON_MD_DOOR_FRONT " Entrance");
1170 ImGui::Text("Entrance ID: 0x%02X | Map: 0x%02X", entrance->entrance_id_,
1171 entrance->map_id_);
1172 ImGui::TextDisabled("Pos: (%d, %d) · Game: (%d, %d)", entrance->x_,
1173 entrance->y_, entrance->game_x_, entrance->game_y_);
1174 break;
1175 }
1177 const auto* exit = static_cast<const zelda3::OverworldExit*>(entity);
1178 ImGui::Text("%s", ICON_MD_DOOR_BACK " Exit");
1179 ImGui::Text("Room ID: 0x%02X | Map: 0x%02X", exit->room_id_,
1180 exit->map_id_);
1181 ImGui::TextDisabled("Pos: (%d, %d)", exit->x_, exit->y_);
1182 break;
1183 }
1185 const auto* item = static_cast<const zelda3::OverworldItem*>(entity);
1186 std::string item_name = (item->id_ < zelda3::kSecretItemNames.size())
1187 ? zelda3::kSecretItemNames[item->id_]
1188 : absl::StrFormat("0x%02X", item->id_);
1189 ImGui::Text("%s %s", ICON_MD_GRASS, item_name.c_str());
1190 ImGui::Text("Item ID: 0x%02X | Map: 0x%02X", item->id_,
1191 item->room_map_id_);
1192 ImGui::TextDisabled("Pos: (%d, %d) · Game: (%d, %d)", item->x_,
1193 item->y_, item->game_x_, item->game_y_);
1194 break;
1195 }
1197 const auto* sprite = static_cast<const zelda3::Sprite*>(entity);
1198 ImGui::Text("%s %s", ICON_MD_PEST_CONTROL_RODENT,
1199 sprite->name().c_str());
1200 ImGui::Text("Sprite ID: 0x%02X | Map: 0x%02X", sprite->id(),
1201 sprite->map_id());
1202 ImGui::TextDisabled("Pos: (%d, %d) · Game: (%d, %d)", sprite->x_,
1203 sprite->y_, sprite->game_x_, sprite->game_y_);
1204 break;
1205 }
1206 default:
1207 break;
1208 }
1209 ImGui::EndTooltip();
1210 }
1211}
1212
1213void OverworldCanvasRenderer::DrawMapProperties() {
1214 if (editor_->IsProjectTerrainMap(editor_->current_map_)) {
1215 if (ImGui::BeginChild("OverworldSidebar", ImVec2(0, 0), false))
1216 editor_->lyra_canvas_.DrawProperties(
1217 editor_->current_map_, editor_->game_state_, editor_->overworld_,
1218 editor_->undo_manager_);
1219 ImGui::EndChild();
1220 if (editor_->lyra_canvas_.entity_placement_active())
1221 editor_->show_entities_ = true;
1222 return;
1223 }
1224 // The Background Color and Visual Effects buttons open the same floating
1225 // editors the canvas context menu uses (drawn in OverworldEditor::Update).
1226 if (editor_->sidebar_) {
1227 editor_->sidebar_->Draw(editor_->current_world_, editor_->current_map_,
1228 editor_->current_map_lock_, editor_->game_state_,
1229 editor_->show_custom_bg_color_editor_,
1230 editor_->show_overlay_editor_,
1231 editor_->dependencies_.project);
1232 }
1233}
1234
1235void OverworldCanvasRenderer::DrawOverworldProperties() {
1236 static bool init_properties = false;
1237
1238 if (!init_properties) {
1239 for (int i = 0; i < 0x40; i++) {
1240 std::string area_graphics_str = absl::StrFormat(
1241 "%02hX", editor_->overworld_.overworld_map(i)->area_graphics());
1242 editor_->properties_canvas_
1243 .mutable_labels(OverworldEditor::OverworldProperty::LW_AREA_GFX)
1244 ->push_back(area_graphics_str);
1245
1246 area_graphics_str = absl::StrFormat(
1247 "%02hX",
1248 editor_->overworld_.overworld_map(i + 0x40)->area_graphics());
1249 editor_->properties_canvas_
1250 .mutable_labels(OverworldEditor::OverworldProperty::DW_AREA_GFX)
1251 ->push_back(area_graphics_str);
1252
1253 std::string area_palette_str = absl::StrFormat(
1254 "%02hX", editor_->overworld_.overworld_map(i)->area_palette());
1255 editor_->properties_canvas_
1256 .mutable_labels(OverworldEditor::OverworldProperty::LW_AREA_PAL)
1257 ->push_back(area_palette_str);
1258
1259 area_palette_str = absl::StrFormat(
1260 "%02hX", editor_->overworld_.overworld_map(i + 0x40)->area_palette());
1261 editor_->properties_canvas_
1262 .mutable_labels(OverworldEditor::OverworldProperty::DW_AREA_PAL)
1263 ->push_back(area_palette_str);
1264
1265 std::string sprite_gfx_str = absl::StrFormat(
1266 "%02hX", editor_->overworld_.overworld_map(i)->sprite_graphics(1));
1267 editor_->properties_canvas_
1268 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_GFX_PART1)
1269 ->push_back(sprite_gfx_str);
1270
1271 sprite_gfx_str = absl::StrFormat(
1272 "%02hX", editor_->overworld_.overworld_map(i)->sprite_graphics(2));
1273 editor_->properties_canvas_
1274 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_GFX_PART2)
1275 ->push_back(sprite_gfx_str);
1276
1277 sprite_gfx_str = absl::StrFormat(
1278 "%02hX",
1279 editor_->overworld_.overworld_map(i + 0x40)->sprite_graphics(1));
1280 editor_->properties_canvas_
1281 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_GFX_PART1)
1282 ->push_back(sprite_gfx_str);
1283
1284 sprite_gfx_str = absl::StrFormat(
1285 "%02hX",
1286 editor_->overworld_.overworld_map(i + 0x40)->sprite_graphics(2));
1287 editor_->properties_canvas_
1288 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_GFX_PART2)
1289 ->push_back(sprite_gfx_str);
1290
1291 std::string sprite_palette_str = absl::StrFormat(
1292 "%02hX", editor_->overworld_.overworld_map(i)->sprite_palette(1));
1293 editor_->properties_canvas_
1294 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_PAL_PART1)
1295 ->push_back(sprite_palette_str);
1296
1297 sprite_palette_str = absl::StrFormat(
1298 "%02hX", editor_->overworld_.overworld_map(i)->sprite_palette(2));
1299 editor_->properties_canvas_
1300 .mutable_labels(OverworldEditor::OverworldProperty::LW_SPR_PAL_PART2)
1301 ->push_back(sprite_palette_str);
1302
1303 sprite_palette_str = absl::StrFormat(
1304 "%02hX",
1305 editor_->overworld_.overworld_map(i + 0x40)->sprite_palette(1));
1306 editor_->properties_canvas_
1307 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_PAL_PART1)
1308 ->push_back(sprite_palette_str);
1309
1310 sprite_palette_str = absl::StrFormat(
1311 "%02hX",
1312 editor_->overworld_.overworld_map(i + 0x40)->sprite_palette(2));
1313 editor_->properties_canvas_
1314 .mutable_labels(OverworldEditor::OverworldProperty::DW_SPR_PAL_PART2)
1315 ->push_back(sprite_palette_str);
1316 }
1317 init_properties = true;
1318 }
1319
1320 ImGui::Text(tr("Area Gfx LW/DW"));
1321 editor_->properties_canvas_.UpdateInfoGrid(
1322 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::LW_AREA_GFX);
1323 ImGui::SameLine();
1324 editor_->properties_canvas_.UpdateInfoGrid(
1325 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::DW_AREA_GFX);
1326 ImGui::Separator();
1327
1328 ImGui::Text(tr("Sprite Gfx LW/DW"));
1329 editor_->properties_canvas_.UpdateInfoGrid(
1330 ImVec2(256, 256), 32,
1331 OverworldEditor::OverworldProperty::LW_SPR_GFX_PART1);
1332 ImGui::SameLine();
1333 editor_->properties_canvas_.UpdateInfoGrid(
1334 ImVec2(256, 256), 32,
1335 OverworldEditor::OverworldProperty::DW_SPR_GFX_PART1);
1336 ImGui::SameLine();
1337 editor_->properties_canvas_.UpdateInfoGrid(
1338 ImVec2(256, 256), 32,
1339 OverworldEditor::OverworldProperty::LW_SPR_GFX_PART2);
1340 ImGui::SameLine();
1341 editor_->properties_canvas_.UpdateInfoGrid(
1342 ImVec2(256, 256), 32,
1343 OverworldEditor::OverworldProperty::DW_SPR_GFX_PART2);
1344 ImGui::Separator();
1345
1346 ImGui::Text(tr("Area Pal LW/DW"));
1347 editor_->properties_canvas_.UpdateInfoGrid(
1348 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::LW_AREA_PAL);
1349 ImGui::SameLine();
1350 editor_->properties_canvas_.UpdateInfoGrid(
1351 ImVec2(256, 256), 32, OverworldEditor::OverworldProperty::DW_AREA_PAL);
1352
1353 static bool show_gfx_group = false;
1354 ImGui::Checkbox(tr("Show Gfx Group Editor"), &show_gfx_group);
1355 if (show_gfx_group) {
1356 gui::BeginWindowWithDisplaySettings("Gfx Group Editor", &show_gfx_group);
1357 editor_->status_ = editor_->gfx_group_editor_.Update();
1359 }
1360}
1361
1362} // namespace yaze::editor
rom::LyraLayoutInspection InspectLyraLayout() const
Definition rom.cc:421
bool is_loaded() const
Definition rom.h:199
EditorDependencies dependencies_
Definition editor.h:382
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
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.
void RegisterWidget(const std::string &full_path, const std::string &type, ImGuiID imgui_id, const std::string &description="", const WidgetMetadata &metadata=WidgetMetadata())
static WidgetIdRegistry & Instance()
Represents an overworld exit that transitions from dungeon to overworld.
bool is_lyra_preview() const
Definition overworld.h:444
auto is_loaded() const
Definition overworld.h:836
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_GRASS
Definition icons.h:891
#define ICON_MD_DOOR_BACK
Definition icons.h:612
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_FILTER_LIST
Definition icons.h:768
#define ICON_MD_PEST_CONTROL_RODENT
Definition icons.h:1430
#define ICON_MD_CYCLONE
Definition icons.h:514
int DisplayedMapCount(const CanvasNavigationContext &ctx, const CanvasNavigationCallbacks &callbacks)
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)
std::optional< OverworldContextTarget > ResolveOverworldContextTarget(int world, int game_state, ImVec2 screen_position, ImVec2 canvas_origin, ImVec2 scrolling, float scale, int map_count=zelda3::kNumOverworldMaps)
void DrawSheetScaleControl(const char *id, gui::AdaptiveSheetScaleMode *mode)
void EndCanvas(Canvas &canvas)
void BeginPadding(int i)
Definition style.cc:299
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
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:311
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:303
void BeginNoPadding()
Definition style.cc:307
void EndWindowWithDisplaySettings()
Definition style.cc:294
void BeginWindowWithDisplaySettings(const char *id, bool *active, const ImVec2 &size, ImGuiWindowFlags flags)
Definition style.cc:273
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:315
constexpr int kNumTile16Individual
Definition overworld.h:244
const std::vector< std::string > kSecretItemNames
AreaSizeEnum
Area size enumeration for v3+ ROMs.
void AccumulateTile16MapUsage(const std::vector< std::vector< uint16_t > > &world_tiles, int local_map, std::vector< int > *counts)
constexpr int OverworldCustomASMHasBeenApplied
Definition common.h:90
std::vector< int > BuildTile16DisplayOrder(const std::vector< int > &counts, Tile16SortMode sort_mode, bool hide_unused)
std::string GetOverworldMapLabel(int id)
Convenience function to get an overworld map label.
constexpr int kExpandedMapCount
Definition common.h:86
int SubscreenOverlayScreen(uint16_t overlay_id)
#define IM_PI
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
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
Automatic widget registration helpers for ImGui Test Engine integration.