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"
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
41int AllocatedRowsForWorld(int world) {
42 const int clamped_world = std::clamp(world, 0, 2);
43 const int world_start = clamped_world * 0x40;
44 const int maps_available =
45 std::clamp(zelda3::kNumOverworldMaps - world_start, 0, 0x40);
46 return (maps_available + 7) / 8;
47}
48
49std::optional<int> MapFromCanvasPosition(const CanvasNavigationContext& ctx,
50 ImVec2 scaled_position) {
51 if (!ctx.ow_map_canvas || !ctx.overworld || !ctx.current_world) {
52 return std::nullopt;
53 }
54
55 float scale = ctx.ow_map_canvas->global_scale();
56 if (scale <= 0.0f)
57 scale = 1.0f;
58
59 const int map_x =
60 static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
61 const int map_y =
62 static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
63
64 if (map_x < 0 || map_x >= 8 || map_y < 0 || map_y >= 8) {
65 return std::nullopt;
66 }
67
68 if (map_y >= AllocatedRowsForWorld(*ctx.current_world)) {
69 return std::nullopt;
70 }
71
72 int map_id = map_x + map_y * 8;
73 if (*ctx.current_world == 1) {
74 map_id += 0x40;
75 } else if (*ctx.current_world == 2) {
76 map_id += 0x80;
77 }
78
79 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps ||
80 ctx.overworld->overworld_map(map_id) == nullptr) {
81 return std::nullopt;
82 }
83 return map_id;
84}
85
86void SetHoveredMap(const CanvasNavigationContext& ctx, int map_id) {
87 if (ctx.hovered_map) {
88 *ctx.hovered_map = map_id;
89 }
90}
91
92void SelectMapFallback(const CanvasNavigationContext& ctx, int map_id,
93 bool respect_pin) {
94 if (!ctx.current_map || !ctx.current_world || !ctx.current_parent ||
95 !ctx.current_map_lock || !ctx.overworld) {
96 return;
97 }
98 if (respect_pin && *ctx.current_map_lock) {
99 return;
100 }
101 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
102 return;
103 }
104 const auto* map = ctx.overworld->overworld_map(map_id);
105 if (!map) {
106 return;
107 }
108 *ctx.current_map = map_id;
109 *ctx.current_world = std::clamp(map_id / 0x40, 0, 2);
110 *ctx.current_parent = map->parent();
111 ctx.overworld->set_current_map(map_id);
113}
114
116 const CanvasNavigationCallbacks& callbacks, int map_id,
117 bool respect_pin) {
118 if (callbacks.select_map_for_editing) {
119 callbacks.select_map_for_editing(map_id, respect_pin);
120 return;
121 }
122 SelectMapFallback(ctx, map_id, respect_pin);
123}
124
125} // namespace
126
127// =============================================================================
128// Initialization
129// =============================================================================
130
132 const CanvasNavigationContext& context,
133 const CanvasNavigationCallbacks& callbacks) {
134 ctx_ = context;
135 callbacks_ = callbacks;
136}
137
138// =============================================================================
139// Map Detection and Loading
140// =============================================================================
141
143 if (!ctx_.ow_map_canvas || !ctx_.overworld || !ctx_.rom ||
146 return absl::OkStatus();
147 }
148
149 const int large_map_size = 1024;
150
151 const auto hovered_map =
152 MapFromCanvasPosition(ctx_, ctx_.ow_map_canvas->hover_mouse_pos());
153 if (!hovered_map.has_value()) {
154 SetHoveredMap(ctx_, -1);
155 return absl::OkStatus();
156 }
157 SetHoveredMap(ctx_, *hovered_map);
158
159 // Hover is only a preview/loading signal. The editable map is changed by
160 // explicit click selection so toolbar/sidebar fields do not retarget while
161 // the cursor crosses another area.
162 bool should_build = false;
163 if (*hovered_map != last_hovered_map_) {
164 last_hovered_map_ = *hovered_map;
165 hover_time_ = 0.0f;
166 should_build = ctx_.overworld->overworld_map(*hovered_map)->is_built();
167 } else {
168 hover_time_ += ImGui::GetIO().DeltaTime;
169 should_build = (hover_time_ >= kHoverBuildDelay) ||
170 ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
171 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
172 }
173
174 if (should_build) {
176 }
177
179 QueueAdjacentMapsForPreload(*hovered_map);
180 }
181
183
184 const int current_highlighted_map = *ctx_.current_map;
185 if (current_highlighted_map < 0 ||
186 current_highlighted_map >= zelda3::kNumOverworldMaps ||
187 ctx_.overworld->overworld_map(current_highlighted_map) == nullptr) {
188 return absl::OkStatus();
189 }
190
191 // Use centralized version detection
193 bool use_v3_area_sizes =
195
196 // Get area size for v3+ ROMs, otherwise use legacy logic
197 if (use_v3_area_sizes) {
199 auto area_size =
200 ctx_.overworld->overworld_map(current_highlighted_map)->area_size();
201 const int highlight_parent =
202 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
203
204 // Calculate parent map coordinates accounting for world offset
205 int parent_map_x;
206 int parent_map_y;
207 if (*ctx_.current_world == 0) {
208 parent_map_x = highlight_parent % 8;
209 parent_map_y = highlight_parent / 8;
210 } else if (*ctx_.current_world == 1) {
211 parent_map_x = (highlight_parent - 0x40) % 8;
212 parent_map_y = (highlight_parent - 0x40) / 8;
213 } else {
214 parent_map_x = (highlight_parent - 0x80) % 8;
215 parent_map_y = (highlight_parent - 0x80) / 8;
216 }
217
218 // Draw outline based on area size
219 switch (area_size) {
220 case AreaSizeEnum::LargeArea:
222 parent_map_y * kOverworldMapSize,
223 large_map_size, large_map_size);
224 break;
225 case AreaSizeEnum::WideArea:
227 parent_map_y * kOverworldMapSize,
228 large_map_size, kOverworldMapSize);
229 break;
230 case AreaSizeEnum::TallArea:
232 parent_map_y * kOverworldMapSize,
233 kOverworldMapSize, large_map_size);
234 break;
235 case AreaSizeEnum::SmallArea:
236 default:
238 parent_map_y * kOverworldMapSize,
240 break;
241 }
242 } else {
243 // Legacy logic for vanilla and v2 ROMs
244 if (ctx_.overworld->overworld_map(current_highlighted_map)
245 ->is_large_map() ||
246 ctx_.overworld->overworld_map(current_highlighted_map)->large_index() !=
247 0) {
248 const int highlight_parent =
249 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
250
251 int parent_map_x;
252 int parent_map_y;
253 if (*ctx_.current_world == 0) {
254 parent_map_x = highlight_parent % 8;
255 parent_map_y = highlight_parent / 8;
256 } else if (*ctx_.current_world == 1) {
257 parent_map_x = (highlight_parent - 0x40) % 8;
258 parent_map_y = (highlight_parent - 0x40) / 8;
259 } else {
260 parent_map_x = (highlight_parent - 0x80) % 8;
261 parent_map_y = (highlight_parent - 0x80) / 8;
262 }
263
265 parent_map_y * kOverworldMapSize,
266 large_map_size, large_map_size);
267 } else {
268 int current_map_x;
269 int current_map_y;
270 if (*ctx_.current_world == 0) {
271 current_map_x = current_highlighted_map % 8;
272 current_map_y = current_highlighted_map / 8;
273 } else if (*ctx_.current_world == 1) {
274 current_map_x = (current_highlighted_map - 0x40) % 8;
275 current_map_y = (current_highlighted_map - 0x40) / 8;
276 } else {
277 current_map_x = (current_highlighted_map - 0x80) % 8;
278 current_map_y = (current_highlighted_map - 0x80) / 8;
279 }
281 current_map_y * kOverworldMapSize,
283 }
284 }
285
286 // Ensure current map has texture created for rendering
289 if (*hovered_map != *ctx_.current_map) {
290 callbacks_.ensure_map_texture(*hovered_map);
291 }
292 }
293
294 if ((*ctx_.maps_bmp)[*ctx_.current_map].modified()) {
297 }
300 }
301
302 // Ensure tile16 blockset is fully updated before rendering
306 }
307
308 // Update map texture with the traditional direct update approach
312 (*ctx_.maps_bmp)[*ctx_.current_map].set_modified(false);
313 }
314
316 ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
319 }
320 }
321
322 return absl::OkStatus();
323}
324
325// =============================================================================
326// Map Interaction
327// =============================================================================
328
332 !ctx_.current_map) {
333 return;
334 }
336 return;
337 }
338
339 // Paint-mode eyedropper: right-click samples tile16 under cursor.
342 ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
345 }
346 return;
347 }
348
350 return;
351 }
353 return;
354 }
355
356 auto map_from_cursor = [&]() -> std::optional<int> {
357 if (ctx_.hovered_map && *ctx_.hovered_map >= 0) {
358 return *ctx_.hovered_map;
359 }
360 return MapFromCanvasPosition(ctx_, ctx_.ow_map_canvas->hover_mouse_pos());
361 };
362
363 const auto hovered_map = map_from_cursor();
364 if (!hovered_map.has_value()) {
365 return;
366 }
367
368 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
369 SelectMapForEditing(ctx_, callbacks_, *hovered_map, true);
370 }
371
372 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
373 SelectMapForEditing(ctx_, callbacks_, *hovered_map, true);
375 }
376
377 if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
378 if (*ctx_.current_map_lock && *ctx_.current_map == *hovered_map) {
379 *ctx_.current_map_lock = false;
380 return;
381 }
382 SelectMapForEditing(ctx_, callbacks_, *hovered_map, false);
383 *ctx_.current_map_lock = true;
385 }
386}
387
388// =============================================================================
389// Pan and Zoom
390// =============================================================================
391
393 // Determine if panning should occur:
394 // 1. Middle-click drag always pans (all modes)
395 // 2. Left-click drag pans in mouse mode when not hovering over an entity
396 bool should_pan = false;
397
398 if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
399 should_pan = true;
400 } else if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
402 // In mouse mode, left-click pans unless hovering over an entity
403 bool over_entity =
405 // Also don't pan if we're currently dragging an entity
406 if (!over_entity && !*ctx_.is_dragging_entity) {
407 should_pan = true;
408 }
409 }
410
411 if (!should_pan) {
412 return;
413 }
414
415 // Pan by adjusting ImGui's scroll position (scrollbars handle actual scroll)
416 ImVec2 delta = ImGui::GetIO().MouseDelta;
417 float new_scroll_x = ImGui::GetScrollX() - delta.x;
418 float new_scroll_y = ImGui::GetScrollY() - delta.y;
419
420 // Get scroll limits from ImGui
421 float max_scroll_x = ImGui::GetScrollMaxX();
422 float max_scroll_y = ImGui::GetScrollMaxY();
423
424 // Clamp to valid scroll range
425 new_scroll_x = std::clamp(new_scroll_x, 0.0f, max_scroll_x);
426 new_scroll_y = std::clamp(new_scroll_y, 0.0f, max_scroll_y);
427
428 ImGui::SetScrollX(new_scroll_x);
429 ImGui::SetScrollY(new_scroll_y);
430}
431
433 // Scroll wheel is reserved for canvas navigation/panning
434 // Use toolbar buttons or context menu for zoom control
435}
436
438 float new_scale =
439 std::min(kOverworldMaxZoom,
442 // Scroll will be clamped automatically by ImGui on next frame
443}
444
446 float new_scale =
447 std::max(kOverworldMinZoom,
450 // Scroll will be clamped automatically by ImGui on next frame
451}
452
454 // ImGui handles scroll clamping automatically via GetScrollMaxX/Y
455 // This function is now a no-op but kept for API compatibility
456}
457
459 // Reset ImGui scroll to top-left
460 ImGui::SetScrollX(0);
461 ImGui::SetScrollY(0);
463}
464
466 // Center the view on the current map
467 float scale = ctx_.ow_map_canvas->global_scale();
468 if (scale <= 0.0f)
469 scale = 1.0f;
470
471 // Calculate map position within the world
472 int map_in_world = *ctx_.current_map % 0x40;
473 int map_x = (map_in_world % 8) * kOverworldMapSize;
474 int map_y = (map_in_world / 8) * kOverworldMapSize;
475
476 // Get viewport size
477 ImVec2 viewport_px = ImGui::GetContentRegionAvail();
478
479 // Calculate scroll to center the current map (in ImGui's positive scroll
480 // space)
481 float center_x = (map_x + kOverworldMapSize / 2.0f) * scale;
482 float center_y = (map_y + kOverworldMapSize / 2.0f) * scale;
483
484 float scroll_x = center_x - viewport_px.x / 2.0f;
485 float scroll_y = center_y - viewport_px.y / 2.0f;
486
487 // Clamp to valid scroll range
488 scroll_x = std::clamp(scroll_x, 0.0f, ImGui::GetScrollMaxX());
489 scroll_y = std::clamp(scroll_y, 0.0f, ImGui::GetScrollMaxY());
490
491 ImGui::SetScrollX(scroll_x);
492 ImGui::SetScrollY(scroll_y);
493}
494
496 // Legacy wrapper - now calls HandleOverworldPan
498}
499
500// =============================================================================
501// Blockset Selector Synchronization
502// =============================================================================
503
505 if (*ctx_.blockset_selector) {
506 (*ctx_.blockset_selector)->ScrollToTile(*ctx_.current_tile16);
507 return;
508 }
509
510 // CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
511 // The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT
512 // window, and when called from CheckForSelectRectangle() during overworld
513 // canvas rendering, it incorrectly scrolls the overworld canvas instead of
514 // the tile16 selector.
515 //
516 // The blockset_selector_ should always be available in modern code paths.
517 // If it's not available, we skip scrolling rather than scroll the wrong
518 // window.
519}
520
529
530// =============================================================================
531// Background Pre-loading
532// =============================================================================
533
535#ifdef __EMSCRIPTEN__
536 // WASM: Skip pre-loading entirely - it blocks the main thread and causes
537 // stuttering. The tileset cache and debouncing provide enough optimization.
538 return;
539#endif
540
541 if (center_map < 0 || center_map >= zelda3::kNumOverworldMaps) {
542 return;
543 }
544
545 preload_queue_.clear();
546
547 // Calculate grid position (8x8 maps per world)
548 int world_offset = (center_map / 64) * 64;
549 int local_index = center_map % 64;
550 int map_x = local_index % 8;
551 int map_y = local_index / 8;
552 int max_rows = (center_map >= zelda3::kSpecialWorldMapIdStart) ? 4 : 8;
553
554 // Add adjacent maps (4-connected neighbors)
555 static const int dx[] = {-1, 1, 0, 0};
556 static const int dy[] = {0, 0, -1, 1};
557
558 for (int i = 0; i < 4; ++i) {
559 int nx = map_x + dx[i];
560 int ny = map_y + dy[i];
561
562 // Check bounds (world grid; special world is only 4 rows high)
563 if (nx >= 0 && nx < 8 && ny >= 0 && ny < max_rows) {
564 int neighbor_index = world_offset + ny * 8 + nx;
565 // Only queue if not already built
566 if (neighbor_index >= 0 && neighbor_index < zelda3::kNumOverworldMaps &&
567 !ctx_.overworld->overworld_map(neighbor_index)->is_built()) {
568 preload_queue_.push_back(neighbor_index);
569 }
570 }
571 }
572}
573
575#ifdef __EMSCRIPTEN__
576 // WASM: Pre-loading disabled - each EnsureMapBuilt call blocks for 100-200ms
577 // which causes unacceptable frame drops. Native builds use this for smoother
578 // UX.
579 return;
580#endif
581
582 if (preload_queue_.empty()) {
583 return;
584 }
585
586 // Process one map per frame to avoid blocking (native only)
587 int map_to_preload = preload_queue_.back();
588 preload_queue_.pop_back();
589
590 // Silent build - don't update UI state
591 auto status = ctx_.overworld->EnsureMapBuilt(map_to_preload);
592 if (!status.ok()) {
593 // Log but don't interrupt - this is background work
594 LOG_DEBUG("CanvasNavigationManager",
595 "Background preload of map %d failed: %s", map_to_preload,
596 status.message().data());
597 }
598}
599
600} // 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:36
static Arena & Get()
Definition arena.cc:21
bool is_active() const
Definition bitmap.h:405
auto global_scale() const
Definition canvas.h:397
auto hover_mouse_pos() const
Definition canvas.h:460
void set_global_scale(float scale)
Definition canvas.cc:239
bool IsMouseHovering() const
Definition canvas.h:338
void DrawOutline(int x, int y, int w, int h)
Definition canvas.cc:1226
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
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