yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
canvas_navigation_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <optional>
5
6#include "absl/status/status.h"
9#include "imgui/imgui.h"
10#include "util/log.h"
11#include "util/macro.h"
14
15namespace yaze::editor {
16
17// =============================================================================
18// Anonymous helpers (moved from overworld_editor.cc)
19// =============================================================================
20
21namespace {
22
23// Calculate the total canvas content size based on world layout
24ImVec2 CalculateOverworldContentSize(float scale) {
25 // 8x8 grid of 512x512 maps = 4096x4096 total
26 constexpr float kWorldSize = 512.0f * 8.0f; // 4096
27 return ImVec2(kWorldSize * scale, kWorldSize * scale);
28}
29
30// Clamp scroll position to valid bounds
31ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size,
32 ImVec2 visible_size) {
33 float max_scroll_x = std::max(0.0f, content_size.x - visible_size.x);
34 float max_scroll_y = std::max(0.0f, content_size.y - visible_size.y);
35
36 float clamped_x = std::clamp(scroll.x, -max_scroll_x, 0.0f);
37 float clamped_y = std::clamp(scroll.y, -max_scroll_y, 0.0f);
38
39 return ImVec2(clamped_x, clamped_y);
40}
41
42int AllocatedRowsForWorld(int world) {
43 const int clamped_world = std::clamp(world, 0, 2);
44 const int world_start = clamped_world * 0x40;
45 const int maps_available =
46 std::clamp(zelda3::kNumOverworldMaps - world_start, 0, 0x40);
47 return (maps_available + 7) / 8;
48}
49
50std::optional<int> MapFromCanvasPosition(const CanvasNavigationContext& ctx,
51 ImVec2 scaled_position) {
52 if (!ctx.ow_map_canvas || !ctx.overworld || !ctx.current_world) {
53 return std::nullopt;
54 }
55
56 float scale = ctx.ow_map_canvas->global_scale();
57 if (scale <= 0.0f)
58 scale = 1.0f;
59
60 const int map_x =
61 static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
62 const int map_y =
63 static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
64
65 if (map_x < 0 || map_x >= 8 || map_y < 0 || map_y >= 8) {
66 return std::nullopt;
67 }
68
69 if (map_y >= AllocatedRowsForWorld(*ctx.current_world)) {
70 return std::nullopt;
71 }
72
73 int map_id = map_x + map_y * 8;
74 if (*ctx.current_world == 1) {
75 map_id += 0x40;
76 } else if (*ctx.current_world == 2) {
77 map_id += 0x80;
78 }
79
80 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps ||
81 ctx.overworld->overworld_map(map_id) == nullptr) {
82 return std::nullopt;
83 }
84 return map_id;
85}
86
87void SetHoveredMap(const CanvasNavigationContext& ctx, int map_id) {
88 if (ctx.hovered_map) {
89 *ctx.hovered_map = map_id;
90 }
91}
92
93void SelectMapFallback(const CanvasNavigationContext& ctx, int map_id,
94 bool respect_pin) {
95 if (!ctx.current_map || !ctx.current_world || !ctx.current_parent ||
96 !ctx.current_map_lock || !ctx.overworld) {
97 return;
98 }
99 if (respect_pin && *ctx.current_map_lock) {
100 return;
101 }
102 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
103 return;
104 }
105 const auto* map = ctx.overworld->overworld_map(map_id);
106 if (!map) {
107 return;
108 }
109 *ctx.current_map = map_id;
110 *ctx.current_world = std::clamp(map_id / 0x40, 0, 2);
111 *ctx.current_parent = map->parent();
112 ctx.overworld->set_current_map(map_id);
114}
115
117 const CanvasNavigationCallbacks& callbacks, int map_id,
118 bool respect_pin) {
119 if (callbacks.select_map_for_editing) {
120 callbacks.select_map_for_editing(map_id, respect_pin);
121 return;
122 }
123 SelectMapFallback(ctx, map_id, respect_pin);
124}
125
126} // namespace
127
128// =============================================================================
129// Initialization
130// =============================================================================
131
133 const CanvasNavigationContext& context,
134 const CanvasNavigationCallbacks& callbacks) {
135 ctx_ = context;
136 callbacks_ = callbacks;
137}
138
139// =============================================================================
140// Map Detection and Loading
141// =============================================================================
142
144 if (!ctx_.ow_map_canvas || !ctx_.overworld || !ctx_.rom ||
147 return absl::OkStatus();
148 }
149
150 // Leave / no-hover must clear preview state so status falls back to selection.
151 // hover_mouse_pos() can retain the last in-canvas point after the cursor
152 // exits, so do not trust MapFromCanvasPosition unless we are still hovering.
154 SetHoveredMap(ctx_, -1);
155 return absl::OkStatus();
156 }
157
158 const int large_map_size = 1024;
159
160 const auto hovered_map =
161 MapFromCanvasPosition(ctx_, ctx_.ow_map_canvas->hover_mouse_pos());
162 if (!hovered_map.has_value()) {
163 SetHoveredMap(ctx_, -1);
164 return absl::OkStatus();
165 }
166 SetHoveredMap(ctx_, *hovered_map);
167
168 // Lightweight hover identity near the cursor when it differs from selection.
169 if (ctx_.current_map && *hovered_map != *ctx_.current_map &&
171 ImGui::SetTooltip(
173 .c_str());
174 }
175
176 // Hover is only a preview/loading signal. The editable map is changed by
177 // explicit click selection so toolbar/sidebar fields do not retarget while
178 // the cursor crosses another area.
179 bool should_build = false;
180 if (*hovered_map != last_hovered_map_) {
181 last_hovered_map_ = *hovered_map;
182 hover_time_ = 0.0f;
183 should_build = ctx_.overworld->overworld_map(*hovered_map)->is_built();
184 } else {
185 hover_time_ += ImGui::GetIO().DeltaTime;
186 should_build = (hover_time_ >= kHoverBuildDelay) ||
187 ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
188 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
189 }
190
191 if (should_build) {
193 }
194
196 QueueAdjacentMapsForPreload(*hovered_map);
197 }
198
200
201 const int current_highlighted_map = *ctx_.current_map;
202 if (current_highlighted_map < 0 ||
203 current_highlighted_map >= zelda3::kNumOverworldMaps ||
204 ctx_.overworld->overworld_map(current_highlighted_map) == nullptr) {
205 return absl::OkStatus();
206 }
207
208 // Use centralized version detection
210 bool use_v3_area_sizes =
212
213 // Get area size for v3+ ROMs, otherwise use legacy logic
214 if (use_v3_area_sizes) {
216 auto area_size =
217 ctx_.overworld->overworld_map(current_highlighted_map)->area_size();
218 const int highlight_parent =
219 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
220
221 // Calculate parent map coordinates accounting for world offset
222 int parent_map_x;
223 int parent_map_y;
224 if (*ctx_.current_world == 0) {
225 parent_map_x = highlight_parent % 8;
226 parent_map_y = highlight_parent / 8;
227 } else if (*ctx_.current_world == 1) {
228 parent_map_x = (highlight_parent - 0x40) % 8;
229 parent_map_y = (highlight_parent - 0x40) / 8;
230 } else {
231 parent_map_x = (highlight_parent - 0x80) % 8;
232 parent_map_y = (highlight_parent - 0x80) / 8;
233 }
234
235 // Draw outline based on area size
236 switch (area_size) {
237 case AreaSizeEnum::LargeArea:
239 parent_map_y * kOverworldMapSize,
240 large_map_size, large_map_size);
241 break;
242 case AreaSizeEnum::WideArea:
244 parent_map_y * kOverworldMapSize,
245 large_map_size, kOverworldMapSize);
246 break;
247 case AreaSizeEnum::TallArea:
249 parent_map_y * kOverworldMapSize,
250 kOverworldMapSize, large_map_size);
251 break;
252 case AreaSizeEnum::SmallArea:
253 default:
255 parent_map_y * kOverworldMapSize,
257 break;
258 }
259 } else {
260 // Legacy logic for vanilla and v2 ROMs
261 if (ctx_.overworld->overworld_map(current_highlighted_map)
262 ->is_large_map() ||
263 ctx_.overworld->overworld_map(current_highlighted_map)->large_index() !=
264 0) {
265 const int highlight_parent =
266 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
267
268 int parent_map_x;
269 int parent_map_y;
270 if (*ctx_.current_world == 0) {
271 parent_map_x = highlight_parent % 8;
272 parent_map_y = highlight_parent / 8;
273 } else if (*ctx_.current_world == 1) {
274 parent_map_x = (highlight_parent - 0x40) % 8;
275 parent_map_y = (highlight_parent - 0x40) / 8;
276 } else {
277 parent_map_x = (highlight_parent - 0x80) % 8;
278 parent_map_y = (highlight_parent - 0x80) / 8;
279 }
280
282 parent_map_y * kOverworldMapSize,
283 large_map_size, large_map_size);
284 } else {
285 int current_map_x;
286 int current_map_y;
287 if (*ctx_.current_world == 0) {
288 current_map_x = current_highlighted_map % 8;
289 current_map_y = current_highlighted_map / 8;
290 } else if (*ctx_.current_world == 1) {
291 current_map_x = (current_highlighted_map - 0x40) % 8;
292 current_map_y = (current_highlighted_map - 0x40) / 8;
293 } else {
294 current_map_x = (current_highlighted_map - 0x80) % 8;
295 current_map_y = (current_highlighted_map - 0x80) / 8;
296 }
298 current_map_y * kOverworldMapSize,
300 }
301 }
302
303 // Ensure current map has texture created for rendering
306 if (*hovered_map != *ctx_.current_map) {
307 callbacks_.ensure_map_texture(*hovered_map);
308 }
309 }
310
311 if ((*ctx_.maps_bmp)[*ctx_.current_map].modified()) {
314 }
317 }
318
319 // Ensure tile16 blockset is fully updated before rendering
323 }
324
325 // Update map texture with the traditional direct update approach
329 (*ctx_.maps_bmp)[*ctx_.current_map].set_modified(false);
330 }
331
333 ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
336 }
337 }
338
339 return absl::OkStatus();
340}
341
342// =============================================================================
343// Map Interaction
344// =============================================================================
345
349 !ctx_.current_map) {
350 return;
351 }
353 return;
354 }
355
356 // Paint-mode eyedropper: right-click samples tile16 under cursor.
359 ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
362 }
363 return;
364 }
365
367 return;
368 }
370 return;
371 }
372
373 auto map_from_cursor = [&]() -> std::optional<int> {
374 if (ctx_.hovered_map && *ctx_.hovered_map >= 0) {
375 return *ctx_.hovered_map;
376 }
377 return MapFromCanvasPosition(ctx_, ctx_.ow_map_canvas->hover_mouse_pos());
378 };
379
380 const auto hovered_map = map_from_cursor();
381 if (!hovered_map.has_value()) {
382 return;
383 }
384
385 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
386 SelectMapForEditing(ctx_, callbacks_, *hovered_map, true);
387 }
388
389 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
390 SelectMapForEditing(ctx_, callbacks_, *hovered_map, true);
392 }
393
394 if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
395 if (*ctx_.current_map_lock && *ctx_.current_map == *hovered_map) {
396 *ctx_.current_map_lock = false;
397 return;
398 }
399 SelectMapForEditing(ctx_, callbacks_, *hovered_map, false);
400 *ctx_.current_map_lock = true;
402 }
403}
404
405// =============================================================================
406// Pan and Zoom
407// =============================================================================
408
410 // Determine if panning should occur:
411 // 1. Middle-click drag always pans (all modes)
412 // 2. Left-click drag pans in mouse mode when not hovering over an entity
413 bool should_pan = false;
414
415 if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
416 should_pan = true;
417 } else if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
419 // In mouse mode, left-click pans unless hovering over an entity
420 bool over_entity =
422 // Also don't pan if we're currently dragging an entity
423 if (!over_entity && !*ctx_.is_dragging_entity) {
424 should_pan = true;
425 }
426 }
427
428 if (!should_pan) {
429 return;
430 }
431
432 // Pan by adjusting ImGui's scroll position (scrollbars handle actual scroll)
433 ImVec2 delta = ImGui::GetIO().MouseDelta;
434 float new_scroll_x = ImGui::GetScrollX() - delta.x;
435 float new_scroll_y = ImGui::GetScrollY() - delta.y;
436
437 // Get scroll limits from ImGui
438 float max_scroll_x = ImGui::GetScrollMaxX();
439 float max_scroll_y = ImGui::GetScrollMaxY();
440
441 // Clamp to valid scroll range
442 new_scroll_x = std::clamp(new_scroll_x, 0.0f, max_scroll_x);
443 new_scroll_y = std::clamp(new_scroll_y, 0.0f, max_scroll_y);
444
445 ImGui::SetScrollX(new_scroll_x);
446 ImGui::SetScrollY(new_scroll_y);
447}
448
450 // Scroll wheel is reserved for canvas navigation/panning
451 // Use toolbar buttons or context menu for zoom control
452}
453
455 float new_scale =
456 std::min(kOverworldMaxZoom,
459 // Scroll will be clamped automatically by ImGui on next frame
460}
461
463 float new_scale =
464 std::max(kOverworldMinZoom,
467 // Scroll will be clamped automatically by ImGui on next frame
468}
469
471 // ImGui handles scroll clamping automatically via GetScrollMaxX/Y
472 // This function is now a no-op but kept for API compatibility
473}
474
476 // Reset ImGui scroll to top-left
477 ImGui::SetScrollX(0);
478 ImGui::SetScrollY(0);
480}
481
483 // Center the view on the current map
484 float scale = ctx_.ow_map_canvas->global_scale();
485 if (scale <= 0.0f)
486 scale = 1.0f;
487
488 // Calculate map position within the world
489 int map_in_world = *ctx_.current_map % 0x40;
490 int map_x = (map_in_world % 8) * kOverworldMapSize;
491 int map_y = (map_in_world / 8) * kOverworldMapSize;
492
493 // Get viewport size
494 ImVec2 viewport_px = ImGui::GetContentRegionAvail();
495
496 // Calculate scroll to center the current map (in ImGui's positive scroll
497 // space)
498 float center_x = (map_x + kOverworldMapSize / 2.0f) * scale;
499 float center_y = (map_y + kOverworldMapSize / 2.0f) * scale;
500
501 float scroll_x = center_x - viewport_px.x / 2.0f;
502 float scroll_y = center_y - viewport_px.y / 2.0f;
503
504 // Clamp to valid scroll range
505 scroll_x = std::clamp(scroll_x, 0.0f, ImGui::GetScrollMaxX());
506 scroll_y = std::clamp(scroll_y, 0.0f, ImGui::GetScrollMaxY());
507
508 ImGui::SetScrollX(scroll_x);
509 ImGui::SetScrollY(scroll_y);
510}
511
513 // Legacy wrapper - now calls HandleOverworldPan
515}
516
517// =============================================================================
518// Blockset Selector Synchronization
519// =============================================================================
520
522 if (*ctx_.blockset_selector) {
523 (*ctx_.blockset_selector)->ScrollToTile(*ctx_.current_tile16);
524 return;
525 }
526
527 // CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
528 // The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT
529 // window, and when called from CheckForSelectRectangle() during overworld
530 // canvas rendering, it incorrectly scrolls the overworld canvas instead of
531 // the tile16 selector.
532 //
533 // The blockset_selector_ should always be available in modern code paths.
534 // If it's not available, we skip scrolling rather than scroll the wrong
535 // window.
536}
537
546
547// =============================================================================
548// Background Pre-loading
549// =============================================================================
550
552#ifdef __EMSCRIPTEN__
553 // WASM: Skip pre-loading entirely - it blocks the main thread and causes
554 // stuttering. The tileset cache and debouncing provide enough optimization.
555 return;
556#endif
557
558 if (center_map < 0 || center_map >= zelda3::kNumOverworldMaps) {
559 return;
560 }
561
562 preload_queue_.clear();
563
564 // Calculate grid position (8x8 maps per world)
565 int world_offset = (center_map / 64) * 64;
566 int local_index = center_map % 64;
567 int map_x = local_index % 8;
568 int map_y = local_index / 8;
569 int max_rows = (center_map >= zelda3::kSpecialWorldMapIdStart) ? 4 : 8;
570
571 // Add adjacent maps (4-connected neighbors)
572 static const int dx[] = {-1, 1, 0, 0};
573 static const int dy[] = {0, 0, -1, 1};
574
575 for (int i = 0; i < 4; ++i) {
576 int nx = map_x + dx[i];
577 int ny = map_y + dy[i];
578
579 // Check bounds (world grid; special world is only 4 rows high)
580 if (nx >= 0 && nx < 8 && ny >= 0 && ny < max_rows) {
581 int neighbor_index = world_offset + ny * 8 + nx;
582 // Only queue if not already built
583 if (neighbor_index >= 0 && neighbor_index < zelda3::kNumOverworldMaps &&
584 !ctx_.overworld->overworld_map(neighbor_index)->is_built()) {
585 preload_queue_.push_back(neighbor_index);
586 }
587 }
588 }
589}
590
592#ifdef __EMSCRIPTEN__
593 // WASM: Pre-loading disabled - each EnsureMapBuilt call blocks for 100-200ms
594 // which causes unacceptable frame drops. Native builds use this for smoother
595 // UX.
596 return;
597#endif
598
599 if (preload_queue_.empty()) {
600 return;
601 }
602
603 // Process one map per frame to avoid blocking (native only)
604 int map_to_preload = preload_queue_.back();
605 preload_queue_.pop_back();
606
607 // Silent build - don't update UI state
608 auto status = ctx_.overworld->EnsureMapBuilt(map_to_preload);
609 if (!status.ok()) {
610 // Log but don't interrupt - this is background work
611 LOG_DEBUG("CanvasNavigationManager",
612 "Background preload of map %d failed: %s", map_to_preload,
613 status.message().data());
614 }
615}
616
617} // namespace yaze::editor
void ScrollBlocksetCanvasToCurrentTile()
Scroll the blockset (tile16 selector) to show the currently selected tile16.
absl::Status CheckForCurrentMap()
Detect which map the mouse is over, trigger lazy loading, draw the selection outline,...
void UpdateBlocksetSelectorState()
Push current tile count and selection into the blockset widget.
void ProcessPreloadQueue()
Process one map from the preload queue (call once per frame).
void HandleOverworldPan()
Pan the overworld canvas via middle-click or left-click drag (in MOUSE mode when not hovering an enti...
void QueueAdjacentMapsForPreload(int center_map)
Queue the 4-connected neighbors of center_map for lazy build.
void Initialize(const CanvasNavigationContext &context, const CanvasNavigationCallbacks &callbacks)
Initialize with shared state and callbacks.
void HandleMapInteraction()
Handle tile-mode right-click (eyedropper) and middle-click (lock/properties toggle),...
void ZoomOut()
Decrease canvas zoom by one step.
void CenterOverworldView()
Center the viewport on the current map.
void CheckForMousePan()
Legacy wrapper – delegates to HandleOverworldPan().
void ZoomIn()
Increase canvas zoom by one step.
void ResetOverworldView()
Reset scroll to top-left and scale to 1.0.
void HandleOverworldZoom()
No-op stub preserved for API compatibility.
void ClampOverworldScroll()
No-op stub – ImGui handles scroll clamping automatically.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
bool is_active() const
Definition bitmap.h:407
auto global_scale() const
Definition canvas.h:403
auto hover_mouse_pos() const
Definition canvas.h:466
void set_global_scale(float scale)
Definition canvas.cc:239
bool IsMouseHovering() const
Definition canvas.h:344
void DrawOutline(int x, int y, int w, int h)
Definition canvas.cc:1237
static OverworldVersion GetVersion(const Rom &rom)
Detect ROM version from ASM marker byte.
static bool SupportsAreaEnum(OverworldVersion version)
Check if ROM supports area enum system (v3+ only)
void set_current_world(int world)
Definition overworld.h:735
auto overworld_map(int i) const
Definition overworld.h:662
void set_current_map(int i)
Definition overworld.h:734
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
#define LOG_DEBUG(category, format,...)
Definition log.h:103
void SelectMapForEditing(const CanvasNavigationContext &ctx, const CanvasNavigationCallbacks &callbacks, int map_id, bool respect_pin)
void SetHoveredMap(const CanvasNavigationContext &ctx, int map_id)
std::optional< int > MapFromCanvasPosition(const CanvasNavigationContext &ctx, ImVec2 scaled_position)
ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size, ImVec2 visible_size)
void SelectMapFallback(const CanvasNavigationContext &ctx, int map_id, bool respect_pin)
Editors are the view controllers for the application.
constexpr unsigned int kOverworldMapSize
constexpr float kOverworldMaxZoom
constexpr float kOverworldMinZoom
std::string FormatOverworldMapStatusSegment(int current_map, int hovered_map)
constexpr float kOverworldZoomStep
constexpr int kNumTile16Individual
Definition overworld.h:241
constexpr int kSpecialWorldMapIdStart
constexpr int kNumOverworldMaps
Definition common.h:85
AreaSizeEnum
Area size enumeration for v3+ ROMs.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Callbacks for operations that remain in the OverworldEditor.
std::function< absl::Status()> refresh_tile16_blockset
std::function< void(int, bool)> select_map_for_editing
std::function< bool()> is_entity_hovered
Returns true if an entity is currently hovered (for pan suppression).
Shared state pointers that the navigation manager reads/writes.
std::unique_ptr< gui::TileSelectorWidget > * blockset_selector
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > * maps_bmp
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119