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
5#include "absl/status/status.h"
7#include "core/features.h"
8#include "imgui/imgui.h"
9#include "util/log.h"
10#include "util/macro.h"
13
14namespace yaze::editor {
15
16// =============================================================================
17// Anonymous helpers (moved from overworld_editor.cc)
18// =============================================================================
19
20namespace {
21
22// Calculate the total canvas content size based on world layout
23ImVec2 CalculateOverworldContentSize(float scale) {
24 // 8x8 grid of 512x512 maps = 4096x4096 total
25 constexpr float kWorldSize = 512.0f * 8.0f; // 4096
26 return ImVec2(kWorldSize * scale, kWorldSize * scale);
27}
28
29// Clamp scroll position to valid bounds
30ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size,
31 ImVec2 visible_size) {
32 float max_scroll_x = std::max(0.0f, content_size.x - visible_size.x);
33 float max_scroll_y = std::max(0.0f, content_size.y - visible_size.y);
34
35 float clamped_x = std::clamp(scroll.x, -max_scroll_x, 0.0f);
36 float clamped_y = std::clamp(scroll.y, -max_scroll_y, 0.0f);
37
38 return ImVec2(clamped_x, clamped_y);
39}
40
41} // namespace
42
43// =============================================================================
44// Initialization
45// =============================================================================
46
48 const CanvasNavigationContext& context,
49 const CanvasNavigationCallbacks& callbacks) {
50 ctx_ = context;
51 callbacks_ = callbacks;
52}
53
54// =============================================================================
55// Map Detection and Loading
56// =============================================================================
57
59 // 4096x4096, 512x512 maps and some are large maps 1024x1024
60 // hover_mouse_pos() returns canvas-local coordinates but they're scaled
61 // Unscale to get world coordinates for map detection
62 const auto scaled_position = ctx_.ow_map_canvas->hover_mouse_pos();
63 float scale = ctx_.ow_map_canvas->global_scale();
64 if (scale <= 0.0f)
65 scale = 1.0f;
66 const int large_map_size = 1024;
67
68 // Calculate which small map the mouse is currently over
69 // Unscale coordinates to get world position
70 int map_x = static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
71 int map_y = static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
72
73 // Bounds check to prevent out-of-bounds access
74 if (map_x < 0 || map_x >= 8 || map_y < 0 || map_y >= 8) {
75 return absl::OkStatus();
76 }
77
78 const bool allow_special_tail =
80 if (!allow_special_tail && *ctx_.current_world == 2 && map_y >= 4) {
81 // Special world is only 4 rows high unless expansion is enabled
82 return absl::OkStatus();
83 }
84
85 // Calculate the index of the map in the `maps_bmp_` array
86 int hovered_map = map_x + map_y * 8;
87 if (*ctx_.current_world == 1) {
88 hovered_map += 0x40;
89 } else if (*ctx_.current_world == 2) {
90 hovered_map += 0x80;
91 }
92
93 // Only update current_map if not locked
94 if (!*ctx_.current_map_lock) {
95 *ctx_.current_map = hovered_map;
98
99 // Hover debouncing: Only build expensive maps after dwelling on them
100 // This prevents lag when rapidly moving mouse across the overworld
101 bool should_build = false;
102 if (hovered_map != last_hovered_map_) {
103 // New map hovered - reset timer
104 last_hovered_map_ = hovered_map;
105 hover_time_ = 0.0f;
106 // Check if already built (instant display)
107 should_build = ctx_.overworld->overworld_map(hovered_map)->is_built();
108 } else {
109 // Same map - accumulate hover time
110 hover_time_ += ImGui::GetIO().DeltaTime;
111 // Build after delay OR if clicking
112 should_build = (hover_time_ >= kHoverBuildDelay) ||
113 ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
114 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
115 }
116
117 // Only trigger expensive build if debounce threshold met
118 if (should_build) {
120 }
121
122 // After dwelling longer, start pre-loading adjacent maps
125 }
126
127 // Process one preload per frame (background optimization)
129 }
130
131 const int current_highlighted_map = *ctx_.current_map;
132
133 // Use centralized version detection
135 bool use_v3_area_sizes =
137
138 // Get area size for v3+ ROMs, otherwise use legacy logic
139 if (use_v3_area_sizes) {
141 auto area_size =
143 const int highlight_parent =
144 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
145
146 // Calculate parent map coordinates accounting for world offset
147 int parent_map_x;
148 int parent_map_y;
149 if (*ctx_.current_world == 0) {
150 parent_map_x = highlight_parent % 8;
151 parent_map_y = highlight_parent / 8;
152 } else if (*ctx_.current_world == 1) {
153 parent_map_x = (highlight_parent - 0x40) % 8;
154 parent_map_y = (highlight_parent - 0x40) / 8;
155 } else {
156 parent_map_x = (highlight_parent - 0x80) % 8;
157 parent_map_y = (highlight_parent - 0x80) / 8;
158 }
159
160 // Draw outline based on area size
161 switch (area_size) {
162 case AreaSizeEnum::LargeArea:
164 parent_map_y * kOverworldMapSize,
165 large_map_size, large_map_size);
166 break;
167 case AreaSizeEnum::WideArea:
169 parent_map_y * kOverworldMapSize,
170 large_map_size, kOverworldMapSize);
171 break;
172 case AreaSizeEnum::TallArea:
174 parent_map_y * kOverworldMapSize,
175 kOverworldMapSize, large_map_size);
176 break;
177 case AreaSizeEnum::SmallArea:
178 default:
180 parent_map_y * kOverworldMapSize,
182 break;
183 }
184 } else {
185 // Legacy logic for vanilla and v2 ROMs
186 int world_offset = *ctx_.current_world * 0x40;
187 if (ctx_.overworld->overworld_map(*ctx_.current_map)->is_large_map() ||
188 ctx_.overworld->overworld_map(*ctx_.current_map)->large_index() != 0) {
189 const int highlight_parent =
190 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
191
192 int parent_map_x;
193 int parent_map_y;
194 if (*ctx_.current_world == 0) {
195 parent_map_x = highlight_parent % 8;
196 parent_map_y = highlight_parent / 8;
197 } else if (*ctx_.current_world == 1) {
198 parent_map_x = (highlight_parent - 0x40) % 8;
199 parent_map_y = (highlight_parent - 0x40) / 8;
200 } else {
201 parent_map_x = (highlight_parent - 0x80) % 8;
202 parent_map_y = (highlight_parent - 0x80) / 8;
203 }
204
206 parent_map_y * kOverworldMapSize,
207 large_map_size, large_map_size);
208 } else {
209 int current_map_x;
210 int current_map_y;
211 if (*ctx_.current_world == 0) {
212 current_map_x = current_highlighted_map % 8;
213 current_map_y = current_highlighted_map / 8;
214 } else if (*ctx_.current_world == 1) {
215 current_map_x = (current_highlighted_map - 0x40) % 8;
216 current_map_y = (current_highlighted_map - 0x40) / 8;
217 } else {
218 current_map_x = (current_highlighted_map - 0x80) % 8;
219 current_map_y = (current_highlighted_map - 0x80) / 8;
220 }
222 current_map_y * kOverworldMapSize,
224 }
225 }
226
227 // Ensure current map has texture created for rendering
229
230 if ((*ctx_.maps_bmp)[*ctx_.current_map].modified()) {
233
234 // Ensure tile16 blockset is fully updated before rendering
239 }
240
241 // Update map texture with the traditional direct update approach
245 (*ctx_.maps_bmp)[*ctx_.current_map].set_modified(false);
246 }
247
249 ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
251 }
252
253 // If double clicked, toggle the current map
255 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) {
257 }
258
259 return absl::OkStatus();
260}
261
262// =============================================================================
263// Map Interaction
264// =============================================================================
265
267 // Paint-mode eyedropper: right-click samples tile16 under cursor.
270 ImGui::IsMouseClicked(ImGuiMouseButton_Right) &&
271 ImGui::IsItemHovered()) {
273 return;
274 }
275
276 // Handle middle-click for map interaction instead of right-click
277 if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle) &&
278 ImGui::IsItemHovered()) {
279 // Get the current map from mouse position (unscale coordinates)
280 auto scaled_position = ctx_.ow_map_canvas->drawn_tile_position();
281 float scale = ctx_.ow_map_canvas->global_scale();
282 if (scale <= 0.0f)
283 scale = 1.0f;
284 int map_x = static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
285 int map_y = static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
286 int hovered_map = map_x + map_y * 8;
287 if (*ctx_.current_world == 1) {
288 hovered_map += 0x40;
289 } else if (*ctx_.current_world == 2) {
290 hovered_map += 0x80;
291 }
292
293 // Only interact if we're hovering over a valid map
294 if (hovered_map >= 0 && hovered_map < 0xA0) {
295 // Toggle map lock or open properties panel
296 if (*ctx_.current_map_lock && *ctx_.current_map == hovered_map) {
297 *ctx_.current_map_lock = false;
298 } else {
299 *ctx_.current_map_lock = true;
300 *ctx_.current_map = hovered_map;
302 }
303 }
304 }
305
306 // Handle double-click to open properties panel (original behavior)
307 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) &&
308 ImGui::IsItemHovered()) {
310 }
311}
312
313// =============================================================================
314// Pan and Zoom
315// =============================================================================
316
318 // Determine if panning should occur:
319 // 1. Middle-click drag always pans (all modes)
320 // 2. Left-click drag pans in mouse mode when not hovering over an entity
321 bool should_pan = false;
322
323 if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
324 should_pan = true;
325 } else if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
327 // In mouse mode, left-click pans unless hovering over an entity
328 bool over_entity = callbacks_.is_entity_hovered &&
330 // Also don't pan if we're currently dragging an entity
331 if (!over_entity && !*ctx_.is_dragging_entity) {
332 should_pan = true;
333 }
334 }
335
336 if (!should_pan) {
337 return;
338 }
339
340 // Pan by adjusting ImGui's scroll position (scrollbars handle actual scroll)
341 ImVec2 delta = ImGui::GetIO().MouseDelta;
342 float new_scroll_x = ImGui::GetScrollX() - delta.x;
343 float new_scroll_y = ImGui::GetScrollY() - delta.y;
344
345 // Get scroll limits from ImGui
346 float max_scroll_x = ImGui::GetScrollMaxX();
347 float max_scroll_y = ImGui::GetScrollMaxY();
348
349 // Clamp to valid scroll range
350 new_scroll_x = std::clamp(new_scroll_x, 0.0f, max_scroll_x);
351 new_scroll_y = std::clamp(new_scroll_y, 0.0f, max_scroll_y);
352
353 ImGui::SetScrollX(new_scroll_x);
354 ImGui::SetScrollY(new_scroll_y);
355}
356
358 // Scroll wheel is reserved for canvas navigation/panning
359 // Use toolbar buttons or context menu for zoom control
360}
361
363 float new_scale =
364 std::min(kOverworldMaxZoom,
367 // Scroll will be clamped automatically by ImGui on next frame
368}
369
371 float new_scale =
372 std::max(kOverworldMinZoom,
375 // Scroll will be clamped automatically by ImGui on next frame
376}
377
379 // ImGui handles scroll clamping automatically via GetScrollMaxX/Y
380 // This function is now a no-op but kept for API compatibility
381}
382
384 // Reset ImGui scroll to top-left
385 ImGui::SetScrollX(0);
386 ImGui::SetScrollY(0);
388}
389
391 // Center the view on the current map
392 float scale = ctx_.ow_map_canvas->global_scale();
393 if (scale <= 0.0f) scale = 1.0f;
394
395 // Calculate map position within the world
396 int map_in_world = *ctx_.current_map % 0x40;
397 int map_x = (map_in_world % 8) * kOverworldMapSize;
398 int map_y = (map_in_world / 8) * kOverworldMapSize;
399
400 // Get viewport size
401 ImVec2 viewport_px = ImGui::GetContentRegionAvail();
402
403 // Calculate scroll to center the current map (in ImGui's positive scroll
404 // space)
405 float center_x = (map_x + kOverworldMapSize / 2.0f) * scale;
406 float center_y = (map_y + kOverworldMapSize / 2.0f) * scale;
407
408 float scroll_x = center_x - viewport_px.x / 2.0f;
409 float scroll_y = center_y - viewport_px.y / 2.0f;
410
411 // Clamp to valid scroll range
412 scroll_x = std::clamp(scroll_x, 0.0f, ImGui::GetScrollMaxX());
413 scroll_y = std::clamp(scroll_y, 0.0f, ImGui::GetScrollMaxY());
414
415 ImGui::SetScrollX(scroll_x);
416 ImGui::SetScrollY(scroll_y);
417}
418
420 // Legacy wrapper - now calls HandleOverworldPan
422}
423
424// =============================================================================
425// Blockset Selector Synchronization
426// =============================================================================
427
429 if (*ctx_.blockset_selector) {
430 (*ctx_.blockset_selector)->ScrollToTile(*ctx_.current_tile16);
431 return;
432 }
433
434 // CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
435 // The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT
436 // window, and when called from CheckForSelectRectangle() during overworld
437 // canvas rendering, it incorrectly scrolls the overworld canvas instead of
438 // the tile16 selector.
439 //
440 // The blockset_selector_ should always be available in modern code paths.
441 // If it's not available, we skip scrolling rather than scroll the wrong
442 // window.
443}
444
453
454// =============================================================================
455// Background Pre-loading
456// =============================================================================
457
459#ifdef __EMSCRIPTEN__
460 // WASM: Skip pre-loading entirely - it blocks the main thread and causes
461 // stuttering. The tileset cache and debouncing provide enough optimization.
462 return;
463#endif
464
465 if (center_map < 0 || center_map >= zelda3::kNumOverworldMaps) {
466 return;
467 }
468
469 preload_queue_.clear();
470
471 // Calculate grid position (8x8 maps per world)
472 int world_offset = (center_map / 64) * 64;
473 int local_index = center_map % 64;
474 int map_x = local_index % 8;
475 int map_y = local_index / 8;
476 int max_rows = (center_map >= zelda3::kSpecialWorldMapIdStart) ? 4 : 8;
477
478 // Add adjacent maps (4-connected neighbors)
479 static const int dx[] = {-1, 1, 0, 0};
480 static const int dy[] = {0, 0, -1, 1};
481
482 for (int i = 0; i < 4; ++i) {
483 int nx = map_x + dx[i];
484 int ny = map_y + dy[i];
485
486 // Check bounds (world grid; special world is only 4 rows high)
487 if (nx >= 0 && nx < 8 && ny >= 0 && ny < max_rows) {
488 int neighbor_index = world_offset + ny * 8 + nx;
489 // Only queue if not already built
490 if (neighbor_index >= 0 && neighbor_index < zelda3::kNumOverworldMaps &&
491 !ctx_.overworld->overworld_map(neighbor_index)->is_built()) {
492 preload_queue_.push_back(neighbor_index);
493 }
494 }
495 }
496}
497
499#ifdef __EMSCRIPTEN__
500 // WASM: Pre-loading disabled - each EnsureMapBuilt call blocks for 100-200ms
501 // which causes unacceptable frame drops. Native builds use this for smoother
502 // UX.
503 return;
504#endif
505
506 if (preload_queue_.empty()) {
507 return;
508 }
509
510 // Process one map per frame to avoid blocking (native only)
511 int map_to_preload = preload_queue_.back();
512 preload_queue_.pop_back();
513
514 // Silent build - don't update UI state
515 auto status = ctx_.overworld->EnsureMapBuilt(map_to_preload);
516 if (!status.ok()) {
517 // Log but don't interrupt - this is background work
518 LOG_DEBUG("CanvasNavigationManager",
519 "Background preload of map %d failed: %s", map_to_preload,
520 status.message().data());
521 }
522}
523
524} // namespace yaze::editor
static Flags & get()
Definition features.h:118
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:36
static Arena & Get()
Definition arena.cc:21
bool is_active() const
Definition bitmap.h:384
auto global_scale() const
Definition canvas.h:491
auto hover_mouse_pos() const
Definition canvas.h:555
auto drawn_tile_position() const
Definition canvas.h:450
void set_global_scale(float scale)
Definition canvas.cc:225
void DrawOutline(int x, int y, int w, int h)
Definition canvas.cc:1221
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)
auto overworld_map(int i) const
Definition overworld.h:528
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
Definition overworld.cc:888
#define LOG_DEBUG(category, format,...)
Definition log.h:103
ImVec2 ClampScrollPosition(ImVec2 scroll, ImVec2 content_size, ImVec2 visible_size)
Editors are the view controllers for the application.
constexpr unsigned int kOverworldMapSize
constexpr float kOverworldMaxZoom
constexpr float kOverworldMinZoom
constexpr float kOverworldZoomStep
constexpr int kNumTile16Individual
Definition overworld.h:239
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
struct yaze::core::FeatureFlags::Flags::Overworld overworld
Callbacks for operations that remain in the OverworldEditor.
std::function< absl::Status()> refresh_tile16_blockset
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