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.
1// imgui_internal.h (InnerRect) requires the math operators.
2#define IMGUI_DEFINE_MATH_OPERATORS
4
5#include <algorithm>
6#include <cmath>
7#include <optional>
8
9#include "absl/status/status.h"
14#include "imgui/imgui.h"
15#include "imgui/imgui_internal.h"
16#include "util/log.h"
17#include "util/macro.h"
20
21namespace yaze::editor {
22
23// =============================================================================
24// Anonymous helpers (moved from overworld_editor.cc)
25// =============================================================================
26
27namespace {
28
29std::optional<int> MapFromCanvasPosition(const CanvasNavigationContext& ctx,
30 ImVec2 scaled_position) {
31 if (!ctx.ow_map_canvas || !ctx.overworld || !ctx.current_world) {
32 return std::nullopt;
33 }
34 // Use the same bounds and scale validation as right-click targeting. Integer
35 // truncation previously mapped small negative positions onto screen zero.
36 const auto target = ResolveOverworldContextTarget(
37 *ctx.current_world, 0, scaled_position, ImVec2(0, 0), ImVec2(0, 0),
39 if (!target || !ctx.overworld->overworld_map(target->map_id)) {
40 return std::nullopt;
41 }
42 return target->map_id;
43}
44
45void SetHoveredMap(const CanvasNavigationContext& ctx, int map_id) {
46 if (ctx.hovered_map) {
47 *ctx.hovered_map = map_id;
48 }
49}
50
51void SelectMapFallback(const CanvasNavigationContext& ctx, int map_id,
52 bool respect_pin) {
53 if (!ctx.current_map || !ctx.current_world || !ctx.current_parent ||
54 !ctx.current_map_lock || !ctx.overworld) {
55 return;
56 }
57 if (respect_pin && *ctx.current_map_lock) {
58 return;
59 }
60 if (map_id < 0 || map_id >= ctx.overworld->inspected_map_count()) {
61 return;
62 }
63 const auto* map = ctx.overworld->overworld_map(map_id);
64 if (!map) {
65 return;
66 }
67 *ctx.current_map = map_id;
68 *ctx.current_world = std::clamp(map_id / 0x40, 0, 2);
69 *ctx.current_parent = map->parent();
70 ctx.overworld->set_current_map(map_id);
72}
73
75 const CanvasNavigationCallbacks& callbacks, int map_id,
76 bool respect_pin) {
77 if (callbacks.select_map_for_editing) {
78 callbacks.select_map_for_editing(map_id, respect_pin);
79 return;
80 }
81 SelectMapFallback(ctx, map_id, respect_pin);
82}
83
84} // namespace
85
86// =============================================================================
87// Initialization
88// =============================================================================
89
91 const CanvasNavigationContext& context,
92 const CanvasNavigationCallbacks& callbacks) {
93 ctx_ = context;
94 callbacks_ = callbacks;
95}
96
97// =============================================================================
98// Map Detection and Loading
99// =============================================================================
100
102 ImVec2 scaled_position) {
103 const auto hovered_map = MapFromCanvasPosition(ctx_, scaled_position);
104 SetHoveredMap(ctx_, hovered_map.value_or(-1));
106 const ImVec2 mouse =
107 ImGui::GetCurrentContext() ? ImGui::GetIO().MousePos : ImVec2(0, 0);
108 if (mouse.x == hover_follow_suspended_at_->x &&
109 mouse.y == hover_follow_suspended_at_->y) {
110 return hovered_map;
111 }
113 }
114 if (hovered_map && ctx_.current_map && ctx_.current_map_lock &&
115 !*ctx_.current_map_lock && *ctx_.current_map != *hovered_map &&
117 SelectMapForEditing(ctx_, callbacks_, *hovered_map, true);
118 }
119 return hovered_map;
120}
121
123 if (!ctx_.ow_map_canvas || !ctx_.overworld || !ctx_.rom ||
126 return absl::OkStatus();
127 }
128
129 // Leave / no-hover must clear preview state so status falls back to selection.
130 // hover_mouse_pos() can retain the last in-canvas point after the cursor
131 // exits, so do not trust MapFromCanvasPosition unless we are still hovering.
133 SetHoveredMap(ctx_, -1);
134 return absl::OkStatus();
135 }
136
137 const int large_map_size = 1024;
138
139 const auto hovered_map =
141 if (!hovered_map.has_value()) {
142 SetHoveredMap(ctx_, -1);
143 return absl::OkStatus();
144 }
145 SetHoveredMap(ctx_, *hovered_map);
146
147 // Lightweight hover identity near the cursor when it differs from selection.
148 if (ctx_.current_map && *hovered_map != *ctx_.current_map &&
150 ImGui::SetTooltip(
152 .c_str());
153 }
154
155 // Unpinned selection follows the cursor in every editing mode.
156 bool should_build = false;
157 if (*hovered_map != last_hovered_map_) {
158 last_hovered_map_ = *hovered_map;
159 hover_time_ = 0.0f;
160 should_build = ctx_.overworld->overworld_map(*hovered_map)->is_built();
161 } else {
162 hover_time_ += ImGui::GetIO().DeltaTime;
163 should_build = (hover_time_ >= kHoverBuildDelay) ||
164 ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
165 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
166 }
167
168 if (should_build) {
170 }
171
173 QueueAdjacentMapsForPreload(*hovered_map);
174 }
175
177
178 const int current_highlighted_map = *ctx_.current_map;
179 if (current_highlighted_map < 0 ||
180 current_highlighted_map >= ctx_.overworld->inspected_map_count() ||
181 ctx_.overworld->overworld_map(current_highlighted_map) == nullptr) {
182 return absl::OkStatus();
183 }
184
185 // Use centralized version detection
187 bool use_v3_area_sizes =
189
190 // Get area size for v3+ ROMs, otherwise use legacy logic
191 if (use_v3_area_sizes) {
193 auto area_size =
194 ctx_.overworld->overworld_map(current_highlighted_map)->area_size();
195 const int highlight_parent =
196 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
197
198 // Calculate parent map coordinates accounting for world offset
199 int parent_map_x;
200 int parent_map_y;
201 if (*ctx_.current_world == 0) {
202 parent_map_x = highlight_parent % 8;
203 parent_map_y = highlight_parent / 8;
204 } else if (*ctx_.current_world == 1) {
205 parent_map_x = (highlight_parent - 0x40) % 8;
206 parent_map_y = (highlight_parent - 0x40) / 8;
207 } else {
208 parent_map_x = (highlight_parent - 0x80) % 8;
209 parent_map_y = (highlight_parent - 0x80) / 8;
210 }
211
212 // Draw outline based on area size
213 switch (area_size) {
214 case AreaSizeEnum::LargeArea:
216 parent_map_y * kOverworldMapSize,
217 large_map_size, large_map_size);
218 break;
219 case AreaSizeEnum::WideArea:
221 parent_map_y * kOverworldMapSize,
222 large_map_size, kOverworldMapSize);
223 break;
224 case AreaSizeEnum::TallArea:
226 parent_map_y * kOverworldMapSize,
227 kOverworldMapSize, large_map_size);
228 break;
229 case AreaSizeEnum::SmallArea:
230 default:
232 parent_map_y * kOverworldMapSize,
234 break;
235 }
236 } else {
237 // Legacy logic for vanilla and v2 ROMs
238 if (ctx_.overworld->overworld_map(current_highlighted_map)
239 ->is_large_map() ||
240 ctx_.overworld->overworld_map(current_highlighted_map)->large_index() !=
241 0) {
242 const int highlight_parent =
243 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
244
245 int parent_map_x;
246 int parent_map_y;
247 if (*ctx_.current_world == 0) {
248 parent_map_x = highlight_parent % 8;
249 parent_map_y = highlight_parent / 8;
250 } else if (*ctx_.current_world == 1) {
251 parent_map_x = (highlight_parent - 0x40) % 8;
252 parent_map_y = (highlight_parent - 0x40) / 8;
253 } else {
254 parent_map_x = (highlight_parent - 0x80) % 8;
255 parent_map_y = (highlight_parent - 0x80) / 8;
256 }
257
259 parent_map_y * kOverworldMapSize,
260 large_map_size, large_map_size);
261 } else {
262 int current_map_x;
263 int current_map_y;
264 if (*ctx_.current_world == 0) {
265 current_map_x = current_highlighted_map % 8;
266 current_map_y = current_highlighted_map / 8;
267 } else if (*ctx_.current_world == 1) {
268 current_map_x = (current_highlighted_map - 0x40) % 8;
269 current_map_y = (current_highlighted_map - 0x40) / 8;
270 } else {
271 current_map_x = (current_highlighted_map - 0x80) % 8;
272 current_map_y = (current_highlighted_map - 0x80) / 8;
273 }
275 current_map_y * kOverworldMapSize,
277 }
278 }
279
280 // Ensure current map has texture created for rendering
283 if (*hovered_map != *ctx_.current_map) {
284 callbacks_.ensure_map_texture(*hovered_map);
285 }
286 }
287
288 if ((*ctx_.maps_bmp)[*ctx_.current_map].modified()) {
291 }
294 }
295
296 // Ensure tile16 blockset is fully updated before rendering
300 }
301
302 // Update map texture with the traditional direct update approach
306 (*ctx_.maps_bmp)[*ctx_.current_map].set_modified(false);
307 }
308
309 return absl::OkStatus();
310}
311
312// =============================================================================
313// Map Interaction
314// =============================================================================
315
316bool IsMapSelectClick(const MapClickInput& input) {
317 switch (input.mode) {
320 return input.right_clicked && !input.shift;
322 return input.left_released && input.left_press_owned &&
323 !input.left_dragged && !input.entity_hovered;
324 }
325 return false;
326}
327
328std::optional<int> CanvasNavigationManager::MapUnderCursor() const {
329 if (!ctx_.ow_map_canvas) {
330 return std::nullopt;
331 }
332 if (ctx_.hovered_map && *ctx_.hovered_map >= 0) {
333 return *ctx_.hovered_map;
334 }
335 return MapFromCanvasPosition(ctx_, ctx_.ow_map_canvas->hover_mouse_pos());
336}
337
340 return false;
341 }
342 const std::optional<int> map = MapUnderCursor();
343 if (!map) {
344 return false;
345 }
346 if (*ctx_.current_map != *map) {
347 SelectMapForEditing(ctx_, callbacks_, *map, /*respect_pin=*/false);
348 }
349 return true;
350}
351
354 !ctx_.current_map) {
355 return;
356 }
358 return;
359 }
360
361 const ImGuiIO& io = ImGui::GetIO();
362 const float threshold = io.MouseDragThreshold;
363
364 // Press ownership. This runs only while the canvas item is hovered (not
365 // blocked by a popup), so it records only presses that land on the canvas.
366 // io.MouseClickedTime names the latest left press: a release ends a press
367 // this canvas owns only when that time still matches, so a press that
368 // dismissed a popup or began on another window never selects on release.
369 const std::optional<int> map_under_cursor = MapUnderCursor();
370 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
371 left_press_time_ = io.MouseClickedTime[ImGuiMouseButton_Left];
372 left_press_map_ = map_under_cursor;
373 }
374
375 MapClickInput input;
376 input.mode = *ctx_.current_mode;
377 input.left_released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
378 input.left_dragged = io.MouseDragMaxDistanceSqr[ImGuiMouseButton_Left] >=
379 threshold * threshold;
380 input.left_press_owned =
381 left_press_map_.has_value() && map_under_cursor == left_press_map_ &&
382 left_press_time_ == io.MouseClickedTime[ImGuiMouseButton_Left];
383 input.right_clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
384 input.shift = io.KeyShift;
385 input.entity_hovered =
387
388 // Paint modes: the right-click Tile16 sample (and right-drag brush capture)
389 // stays in TilePaintingManager::CheckForSelectRectangle, which runs after
390 // this; sampling here too made every right-click select the tile twice.
391 if (IsMapSelectClick(input)) {
392 (void)SelectMapUnderCursor();
393 }
394 if (input.left_released) {
395 left_press_map_.reset();
396 }
397
399 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) &&
402 }
403
404 // Middle-drag is exclusively navigation. Pinning lives in the toolbar,
405 // Ctrl+L shortcut, and context menu so panning cannot silently freeze tracking.
406}
407
408// =============================================================================
409// Pan and Zoom
410// =============================================================================
411
412ImVec2 StickyWheelPan::Consume(ImVec2 wheel, float dt, float step_px,
413 float snap_px) {
414 if (std::fabs(wheel.x) < kOverworldWheelDeadzone)
415 wheel.x = 0.0f;
416 if (std::fabs(wheel.y) < kOverworldWheelDeadzone)
417 wheel.y = 0.0f;
418 if (wheel.x == 0.0f && wheel.y == 0.0f) {
419 idle_seconds += dt;
421 residual = ImVec2(0.0f, 0.0f);
422 }
423 return ImVec2(0.0f, 0.0f);
424 }
425 idle_seconds = 0.0f;
426
427 const float snap = std::max(snap_px, 1.0f);
428 auto axis = [&](float input, float& rest) -> float {
429 if (input == 0.0f)
430 return 0.0f;
431 // Wheel up/left (positive) scrolls toward the origin.
432 const float delta = -input * step_px;
433 if ((delta > 0.0f) != (rest > 0.0f) && rest != 0.0f) {
434 rest = 0.0f; // Reversal: respond immediately, no leftover travel.
435 }
436 rest += delta;
437 const float out = std::trunc(rest / snap) * snap;
438 rest -= out;
439 return out;
440 };
441 return ImVec2(axis(wheel.x, residual.x), axis(wheel.y, residual.y));
442}
443
444ImVec2 ScrollForZoomAtAnchor(ImVec2 scroll, ImVec2 anchor, float old_scale,
445 float new_scale) {
446 if (old_scale <= 0.0f || new_scale <= 0.0f) {
447 return scroll;
448 }
449 const float ratio = new_scale / old_scale;
450 return ImVec2((scroll.x + anchor.x) * ratio - anchor.x,
451 (scroll.y + anchor.y) * ratio - anchor.y);
452}
453
455 const float scale =
457 constexpr float kWorldSize = kOverworldMapSize * 8.0f; // 4096
458 return ImVec2(kWorldSize * scale, kWorldSize * scale);
459}
460
461ImVec2 CanvasNavigationManager::ClampScroll(ImVec2 scroll) const {
462 const ImVec2 content = ContentSize();
463 const float max_x = std::max(0.0f, content.x - viewport_size_.x);
464 const float max_y = std::max(0.0f, content.y - viewport_size_.y);
465 // Whole pixels: fractional scroll makes pixel art shimmer while moving.
466 return ImVec2(std::round(std::clamp(scroll.x, 0.0f, max_x)),
467 std::round(std::clamp(scroll.y, 0.0f, max_y)));
468}
469
471 ImVec2 anchor) {
472 if (!ctx_.ow_map_canvas)
473 return;
474 const float old_scale = ctx_.ow_map_canvas->global_scale();
475 new_scale = std::clamp(new_scale, kOverworldMinZoom, kOverworldMaxZoom);
476 if (new_scale == old_scale)
477 return;
478 const ImVec2 base = pending_scroll_.value_or(last_scroll_);
480 pending_scroll_ = ScrollForZoomAtAnchor(base, anchor, old_scale, new_scale);
481}
482
483void CanvasNavigationManager::ZoomBySteps(int steps, ImVec2 anchor) {
484 if (!ctx_.ow_map_canvas || steps == 0)
485 return;
488}
489
491 if (!ctx_.ow_map_canvas) {
492 return;
493 }
494 const ImGuiIO& io = ImGui::GetIO();
495 std::optional<ImVec2> target = pending_scroll_;
496 pending_scroll_.reset();
497
498 // ---- Drag pan -------------------------------------------------------------
499 // Only a press that lands on the canvas starts a pan; drags that begin on
500 // scrollbars, other panels or popups never move the map.
501 const bool over_entity =
503 const bool dragging_entity =
506 if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
507 pan_button_ = ImGuiMouseButton_Middle;
508 pan_active_ = true; // Middle only pans: follow the cursor at once.
509 } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
511 !over_entity && !dragging_entity) {
512 pan_button_ = ImGuiMouseButton_Left;
513 pan_active_ = false; // Becomes a pan once past the drag threshold.
514 }
515 if (pan_button_ >= 0) {
516 pan_anchor_mouse_ = io.MousePos;
518 }
519 }
520 if (pan_button_ >= 0) {
521 if (!ImGui::IsMouseDown(pan_button_)) {
522 pan_button_ = -1;
523 pan_active_ = false;
524 } else {
525 if (!pan_active_ && ImGui::IsMouseDragging(pan_button_)) {
526 pan_active_ = !dragging_entity;
527 }
528 if (pan_active_) {
529 // Content stays pinned under the grab point: no drift, no inertia.
530 target = ImVec2(
531 pan_anchor_scroll_.x - (io.MousePos.x - pan_anchor_mouse_.x),
532 pan_anchor_scroll_.y - (io.MousePos.y - pan_anchor_mouse_.y));
533 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
534 }
535 }
536 }
537
538 // ---- Wheel: pan in detents, Cmd/Ctrl+wheel zooms at the cursor -------------
539 const bool zoom_modifier =
540 io.KeyCtrl || (gui::IsMacPlatform() && io.KeySuper);
542 ImVec2 wheel(io.MouseWheelH, io.MouseWheel);
543 if (!gui::IsMacPlatform() && io.KeyShift && wheel.x == 0.0f) {
544 wheel = ImVec2(wheel.y, 0.0f); // Shift+wheel scrolls horizontally.
545 }
546 if (zoom_modifier) {
547 if (wheel.y != 0.0f) {
548 zoom_idle_seconds_ = 0.0f;
549 zoom_residual_ += wheel.y;
550 const int steps = static_cast<int>(
552 if (steps != 0) {
554 if (target) {
555 pending_scroll_ = target;
556 }
557 const ImVec2 anchor(io.MousePos.x - viewport_min_.x,
558 io.MousePos.y - viewport_min_.y);
559 ZoomBySteps(steps, anchor);
560 target = pending_scroll_;
561 pending_scroll_.reset();
562 }
563 }
564 } else {
565 const float scale = ctx_.ow_map_canvas->global_scale();
566 const float snap = std::max(1.0f, kOverworldPanSnapMapPx * scale);
567 const float step =
568 std::max(snap, std::round(kOverworldWheelPanPx / snap) * snap);
569 const ImVec2 delta = wheel_pan_.Consume(wheel, io.DeltaTime, step, snap);
570 if (delta.x != 0.0f || delta.y != 0.0f) {
571 const ImVec2 base = target.value_or(last_scroll_);
572 target = ImVec2(base.x + delta.x, base.y + delta.y);
573 }
574 }
575 }
576 if (!zoom_modifier || io.MouseWheel == 0.0f) {
577 zoom_idle_seconds_ += io.DeltaTime;
579 zoom_residual_ = 0.0f;
580 }
581 }
582
583 // Explicit content size keeps ImGui's clamp correct on the frame the scale
584 // changes (the canvas item that would define it is submitted later).
585 ImGui::SetNextWindowContentSize(ContentSize());
586 if (target) {
587 const ImVec2 clamped = viewport_known_ ? ClampScroll(*target)
588 : ImVec2(std::max(0.0f, target->x),
589 std::max(0.0f, target->y));
590 ImGui::SetNextWindowScroll(clamped);
591 last_scroll_ = clamped;
592 }
593}
594
595void CanvasNavigationManager::EndCanvasViewport(bool canvas_item_hovered) {
596 ImGuiWindow* window = ImGui::GetCurrentWindow();
597 if (!window) {
598 return;
599 }
600 const ImVec2 previous_size = viewport_size_;
601 const bool had_viewport = viewport_known_;
602 viewport_min_ = window->InnerRect.Min;
603 viewport_size_ = window->InnerRect.GetSize();
604 last_scroll_ = window->Scroll;
605 canvas_item_hovered_ = canvas_item_hovered;
606 canvas_window_hovered_ = ImGui::IsWindowHovered();
607 viewport_known_ = viewport_size_.x > 0.0f && viewport_size_.y > 0.0f;
609 const int map = *pending_center_map_;
610 // Scrollbars can change InnerRect after the first content frame. Recenter
611 // until the measured size settles, then release this startup request.
612 if (had_viewport && previous_size.x == viewport_size_.x &&
613 previous_size.y == viewport_size_.y) {
614 pending_center_map_.reset();
615 }
616 CenterOnMap(map);
617 }
618}
619
622 ImGui::GetCurrentContext() ? ImGui::GetIO().MousePos : ImVec2(0, 0);
623}
624
626 ZoomBySteps(1, ImVec2(viewport_size_.x * 0.5f, viewport_size_.y * 0.5f));
627}
628
630 ZoomBySteps(-1, ImVec2(viewport_size_.x * 0.5f, viewport_size_.y * 0.5f));
631}
632
635 return;
636 }
637 constexpr float kWorldSize = kOverworldMapSize * 8.0f;
638 // The special world only allocates four rows of screens.
639 const bool special_world =
642 const float world_height = special_world ? kWorldSize * 0.5f : kWorldSize;
643 const float scale =
644 std::min(viewport_size_.x / kWorldSize, viewport_size_.y / world_height);
646 std::clamp(scale, kOverworldMinZoom, kOverworldMaxZoom));
647 pending_scroll_ = ImVec2(0.0f, 0.0f);
648}
649
651 pending_center_map_.reset();
652 if (ctx_.ow_map_canvas) {
654 }
655 pending_scroll_ = ImVec2(0.0f, 0.0f);
656}
657
663
665 const int map_count = ctx_.overworld ? ctx_.overworld->inspected_map_count()
667 if (!ctx_.ow_map_canvas || map_id < 0 || map_id >= map_count) {
668 return;
669 }
670 // Startup jumps arrive before the canvas child has a measured viewport.
671 // Wait for that size rather than placing the map's center at the top-left.
672 if (!viewport_known_) {
673 pending_center_map_ = map_id;
674 return;
675 }
676 float scale = ctx_.ow_map_canvas->global_scale();
677 if (scale <= 0.0f)
678 scale = 1.0f;
679 const int map_in_world = map_id % 0x40;
680 const float center_x =
681 ((map_in_world % 8) * kOverworldMapSize + kOverworldMapSize / 2.0f) *
682 scale;
683 const float center_y =
684 ((map_in_world / 8) * kOverworldMapSize + kOverworldMapSize / 2.0f) *
685 scale;
686 pending_scroll_ = ImVec2(center_x - viewport_size_.x * 0.5f,
687 center_y - viewport_size_.y * 0.5f);
688}
689
690void CanvasNavigationManager::CenterOnWorldTile(int world_x, int world_y) {
692 return;
693 }
694 const float scale = std::max(ctx_.ow_map_canvas->global_scale(), 0.01f);
696 ImVec2((world_x * 16.0f + 8.0f) * scale - viewport_size_.x * 0.5f,
697 (world_y * 16.0f + 8.0f) * scale - viewport_size_.y * 0.5f);
698}
699
700// =============================================================================
701// Blockset Selector Synchronization
702// =============================================================================
703
705 if (*ctx_.blockset_selector) {
706 (*ctx_.blockset_selector)->ScrollToTile(*ctx_.current_tile16);
707 return;
708 }
709
710 // CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
711 // The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT
712 // window, and when called from CheckForSelectRectangle() during overworld
713 // canvas rendering, it incorrectly scrolls the overworld canvas instead of
714 // the tile16 selector.
715 //
716 // The blockset_selector_ should always be available in modern code paths.
717 // If it's not available, we skip scrolling rather than scroll the wrong
718 // window.
719}
720
729
730// =============================================================================
731// Background Pre-loading
732// =============================================================================
733
735#ifdef __EMSCRIPTEN__
736 // WASM: Skip pre-loading entirely - it blocks the main thread and causes
737 // stuttering. The tileset cache and debouncing provide enough optimization.
738 return;
739#endif
740
741 if (!ctx_.overworld || center_map < 0 ||
742 center_map >= ctx_.overworld->inspected_map_count()) {
743 return;
744 }
745
746 preload_queue_.clear();
747
748 // Calculate grid position (8x8 maps per world)
749 int world_offset = (center_map / 64) * 64;
750 int local_index = center_map % 64;
751 int map_x = local_index % 8;
752 int map_y = local_index / 8;
753 int max_rows = center_map >= zelda3::kSpecialWorldMapIdStart &&
755 ? 4
756 : 8;
757
758 // Add adjacent maps (4-connected neighbors)
759 static const int dx[] = {-1, 1, 0, 0};
760 static const int dy[] = {0, 0, -1, 1};
761
762 for (int i = 0; i < 4; ++i) {
763 int nx = map_x + dx[i];
764 int ny = map_y + dy[i];
765
766 // Check bounds (world grid; special world is only 4 rows high)
767 if (nx >= 0 && nx < 8 && ny >= 0 && ny < max_rows) {
768 int neighbor_index = world_offset + ny * 8 + nx;
769 // Only queue if not already built
770 if (neighbor_index >= 0 &&
771 neighbor_index < ctx_.overworld->inspected_map_count() &&
772 !ctx_.overworld->overworld_map(neighbor_index)->is_built()) {
773 preload_queue_.push_back(neighbor_index);
774 }
775 }
776 }
777}
778
780#ifdef __EMSCRIPTEN__
781 // WASM: Pre-loading disabled - each EnsureMapBuilt call blocks for 100-200ms
782 // which causes unacceptable frame drops. Native builds use this for smoother
783 // UX.
784 return;
785#endif
786
787 if (preload_queue_.empty()) {
788 return;
789 }
790
791 // Process one map per frame to avoid blocking (native only)
792 int map_to_preload = preload_queue_.back();
793 preload_queue_.pop_back();
794
795 // Silent build - don't update UI state
796 auto status = ctx_.overworld->EnsureMapBuilt(map_to_preload);
797 if (!status.ok()) {
798 // Log but don't interrupt - this is background work
799 LOG_DEBUG("CanvasNavigationManager",
800 "Background preload of map %d failed: %s", map_to_preload,
801 status.message().data());
802 }
803}
804
805} // 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 CenterOnMap(int map_id)
Center the viewport on map_id (same world grid).
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 SetScaleAboutAnchor(float new_scale, ImVec2 anchor)
std::optional< int > TrackMapAtCanvasPosition(ImVec2 scaled_position)
void CenterOnWorldTile(int world_x, int world_y)
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()
Click selection (see IsMapSelectClick) and double-click to open properties. The paint-tool right-clic...
void ZoomToFit()
Scale so the whole current world fits the viewport.
void ZoomOut()
Decrease canvas zoom by one step, keeping the view center fixed.
void CenterOverworldView()
Center the viewport on the current map.
bool SelectMapUnderCursor()
Make the map under the cursor the current map, ignoring the pin (the pin, if on, then holds this map)...
void EndCanvasViewport(bool canvas_item_hovered)
void ZoomIn()
Increase canvas zoom by one step, keeping the view center fixed.
void ResetOverworldView()
Reset scroll to top-left and scale to 1.0.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
bool is_active() const
Definition bitmap.h:414
auto global_scale() const
Definition canvas.h:413
auto hover_mouse_pos() const
Definition canvas.h:476
void set_global_scale(float scale)
Definition canvas.cc:323
bool IsMouseHovering() const
Definition canvas.h:354
void DrawOutline(int x, int y, int w, int h)
Definition canvas.cc:1359
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:810
bool is_lyra_preview() const
Definition overworld.h:430
int inspected_map_count() const
Definition overworld.h:433
auto overworld_map(int i) const
Definition overworld.h:737
void set_current_map(int i)
Definition overworld.h:809
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:133
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)
void SelectMapFallback(const CanvasNavigationContext &ctx, int map_id, bool respect_pin)
Editors are the view controllers for the application.
constexpr float kOverworldWheelPanPx
constexpr float kOverworldWheelIdleResetSec
constexpr unsigned int kOverworldMapSize
constexpr float kOverworldMaxZoom
constexpr float kOverworldMinZoom
constexpr float kOverworldWheelDeadzone
std::string FormatOverworldMapStatusSegment(int current_map, int hovered_map)
constexpr float kOverworldZoomStep
constexpr float kOverworldWheelZoomUnitsPerStep
constexpr float kOverworldPanSnapMapPx
std::optional< OverworldContextTarget > ResolveOverworldContextTarget(int world, int game_state, ImVec2 screen_position, ImVec2 canvas_origin, ImVec2 scrolling, float scale, int map_count=zelda3::kNumOverworldMaps)
ImVec2 ScrollForZoomAtAnchor(ImVec2 scroll, ImVec2 anchor, float old_scale, float new_scale)
bool IsMapSelectClick(const MapClickInput &input)
bool IsMacPlatform()
Check if running on macOS (native or web)
constexpr int kNumTile16Individual
Definition overworld.h:243
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).
std::function< void()> open_map_properties
Opens (and focuses) the Map Properties window. Double-click target.
Shared state pointers that the navigation manager reads/writes.
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > * maps_bmp
std::unique_ptr< gui::TileSelectorWidget > * blockset_selector
One frame of canvas mouse input, reduced to what map selection needs.
ImVec2 Consume(ImVec2 wheel, float dt, float step_px, float snap_px)
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119