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
30 const CanvasNavigationCallbacks& callbacks) {
31 return callbacks.displayed_map_count
32 ? callbacks.displayed_map_count()
35}
36
38 int map_id) {
39 return callbacks.is_project_terrain_map &&
40 callbacks.is_project_terrain_map(map_id);
41}
42
43std::optional<int> MapFromCanvasPosition(
44 const CanvasNavigationContext& ctx,
45 const CanvasNavigationCallbacks& callbacks, ImVec2 scaled_position) {
46 if (!ctx.ow_map_canvas || !ctx.overworld || !ctx.current_world) {
47 return std::nullopt;
48 }
49 // Use the same bounds and scale validation as right-click targeting. Integer
50 // truncation previously mapped small negative positions onto screen zero.
51 const auto target = ResolveOverworldContextTarget(
52 *ctx.current_world, 0, scaled_position, ImVec2(0, 0), ImVec2(0, 0),
53 ctx.ow_map_canvas->global_scale(), DisplayedMapCount(ctx, callbacks));
54 if (!target || (!IsProjectTerrainMap(callbacks, target->map_id) &&
55 !ctx.overworld->overworld_map(target->map_id))) {
56 return std::nullopt;
57 }
58 return target->map_id;
59}
60
61void SetHoveredMap(const CanvasNavigationContext& ctx, int map_id) {
62 if (ctx.hovered_map) {
63 *ctx.hovered_map = map_id;
64 }
65}
66
68 const CanvasNavigationCallbacks& callbacks, int map_id,
69 bool respect_pin) {
70 if (!ctx.current_map || !ctx.current_world || !ctx.current_parent ||
71 !ctx.current_map_lock || !ctx.overworld) {
72 return;
73 }
74 if (respect_pin && *ctx.current_map_lock) {
75 return;
76 }
77 if (map_id < 0 || map_id >= DisplayedMapCount(ctx, callbacks)) {
78 return;
79 }
80 const auto* map = ctx.overworld->overworld_map(map_id);
81 const bool project_terrain = IsProjectTerrainMap(callbacks, map_id);
82 if (!project_terrain && !map) {
83 return;
84 }
85 *ctx.current_map = map_id;
86 *ctx.current_world = std::clamp(map_id / 0x40, 0, 2);
87 *ctx.current_parent = project_terrain ? map_id : map->parent();
88 if (project_terrain) {
89 return;
90 }
91 ctx.overworld->set_current_map(map_id);
93}
94
96 const CanvasNavigationCallbacks& callbacks, int map_id,
97 bool respect_pin) {
98 if (callbacks.select_map_for_editing) {
99 callbacks.select_map_for_editing(map_id, respect_pin);
100 return;
101 }
102 SelectMapFallback(ctx, callbacks, map_id, respect_pin);
103}
104
105} // namespace
106
107// =============================================================================
108// Initialization
109// =============================================================================
110
112 const CanvasNavigationContext& context,
113 const CanvasNavigationCallbacks& callbacks) {
114 ctx_ = context;
115 callbacks_ = callbacks;
116}
117
118// =============================================================================
119// Map Detection and Loading
120// =============================================================================
121
123 ImVec2 scaled_position) {
124 const auto hovered_map =
125 MapFromCanvasPosition(ctx_, callbacks_, scaled_position);
126 SetHoveredMap(ctx_, hovered_map.value_or(-1));
128 const ImVec2 mouse =
129 ImGui::GetCurrentContext() ? ImGui::GetIO().MousePos : ImVec2(0, 0);
130 if (mouse.x == hover_follow_suspended_at_->x &&
131 mouse.y == hover_follow_suspended_at_->y) {
132 return hovered_map;
133 }
135 }
136 if (hovered_map && ctx_.current_map && ctx_.current_map_lock &&
137 !*ctx_.current_map_lock && *ctx_.current_map != *hovered_map &&
139 SelectMapForEditing(ctx_, callbacks_, *hovered_map, true);
140 }
141 return hovered_map;
142}
143
145 if (!ctx_.ow_map_canvas || !ctx_.overworld || !ctx_.rom ||
148 return absl::OkStatus();
149 }
150
151 // Leave / no-hover must clear preview state so status falls back to selection.
152 // hover_mouse_pos() can retain the last in-canvas point after the cursor
153 // exits, so do not trust MapFromCanvasPosition unless we are still hovering.
155 SetHoveredMap(ctx_, -1);
156 return absl::OkStatus();
157 }
158
159 const int large_map_size = 1024;
160
161 const auto hovered_map =
163 if (!hovered_map.has_value()) {
164 SetHoveredMap(ctx_, -1);
165 return absl::OkStatus();
166 }
167 SetHoveredMap(ctx_, *hovered_map);
168
169 // Lightweight hover identity near the cursor when it differs from selection.
170 if (ctx_.current_map && *hovered_map != *ctx_.current_map &&
172 ImGui::SetTooltip(
174 .c_str());
175 }
176
177 // Unpinned selection follows the cursor in every editing mode.
178 const bool hovered_project_terrain =
179 IsProjectTerrainMap(callbacks_, *hovered_map);
180 const auto* hovered_model = ctx_.overworld->overworld_map(*hovered_map);
181 bool should_build = false;
182 if (*hovered_map != last_hovered_map_) {
183 last_hovered_map_ = *hovered_map;
184 hover_time_ = 0.0f;
185 should_build = hovered_model && hovered_model->is_built();
186 } else {
187 hover_time_ += ImGui::GetIO().DeltaTime;
188 should_build = (hover_time_ >= kHoverBuildDelay) ||
189 ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
190 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
191 }
192
193 if (!hovered_project_terrain && hovered_model && should_build) {
195 }
196
197 if (!hovered_project_terrain && hover_time_ >= kPreloadStartDelay &&
198 preload_queue_.empty()) {
199 QueueAdjacentMapsForPreload(*hovered_map);
200 }
201
203
204 const int current_highlighted_map = *ctx_.current_map;
205 if (current_highlighted_map < 0 ||
206 current_highlighted_map >= DisplayedMapCount(ctx_, callbacks_)) {
207 return absl::OkStatus();
208 }
209 if (IsProjectTerrainMap(callbacks_, current_highlighted_map)) {
210 const int local_map = current_highlighted_map % 0x40;
212 (local_map / 8) * kOverworldMapSize,
214 if (*hovered_map >= 0 && *hovered_map != current_highlighted_map) {
215 const int h_local = *hovered_map % 0x40;
217 (h_local / 8) * kOverworldMapSize,
219 }
220 if (!hovered_project_terrain && callbacks_.ensure_map_texture) {
221 callbacks_.ensure_map_texture(*hovered_map);
222 }
223 return absl::OkStatus();
224 }
225 if (!ctx_.overworld->overworld_map(current_highlighted_map)) {
226 return absl::OkStatus();
227 }
228
229 // Use centralized version detection
231 bool use_v3_area_sizes =
233
234 // Get area size for v3+ ROMs, otherwise use legacy logic
235 if (use_v3_area_sizes) {
237 auto area_size =
238 ctx_.overworld->overworld_map(current_highlighted_map)->area_size();
239 const int highlight_parent =
240 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
241
242 // Calculate parent map coordinates accounting for world offset
243 int parent_map_x;
244 int parent_map_y;
245 if (*ctx_.current_world == 0) {
246 parent_map_x = highlight_parent % 8;
247 parent_map_y = highlight_parent / 8;
248 } else if (*ctx_.current_world == 1) {
249 parent_map_x = (highlight_parent - 0x40) % 8;
250 parent_map_y = (highlight_parent - 0x40) / 8;
251 } else {
252 parent_map_x = (highlight_parent - 0x80) % 8;
253 parent_map_y = (highlight_parent - 0x80) / 8;
254 }
255
256 // Draw outline based on area size
257 switch (area_size) {
258 case AreaSizeEnum::LargeArea:
260 parent_map_y * kOverworldMapSize,
261 large_map_size, large_map_size);
262 break;
263 case AreaSizeEnum::WideArea:
265 parent_map_y * kOverworldMapSize,
266 large_map_size, kOverworldMapSize);
267 break;
268 case AreaSizeEnum::TallArea:
270 parent_map_y * kOverworldMapSize,
271 kOverworldMapSize, large_map_size);
272 break;
273 case AreaSizeEnum::SmallArea:
274 default:
276 parent_map_y * kOverworldMapSize,
278 break;
279 }
280 } else {
281 // Legacy logic for vanilla and v2 ROMs
282 if (ctx_.overworld->overworld_map(current_highlighted_map)
283 ->is_large_map() ||
284 ctx_.overworld->overworld_map(current_highlighted_map)->large_index() !=
285 0) {
286 const int highlight_parent =
287 ctx_.overworld->overworld_map(current_highlighted_map)->parent();
288
289 int parent_map_x;
290 int parent_map_y;
291 if (*ctx_.current_world == 0) {
292 parent_map_x = highlight_parent % 8;
293 parent_map_y = highlight_parent / 8;
294 } else if (*ctx_.current_world == 1) {
295 parent_map_x = (highlight_parent - 0x40) % 8;
296 parent_map_y = (highlight_parent - 0x40) / 8;
297 } else {
298 parent_map_x = (highlight_parent - 0x80) % 8;
299 parent_map_y = (highlight_parent - 0x80) / 8;
300 }
301
303 parent_map_y * kOverworldMapSize,
304 large_map_size, large_map_size);
305 } else {
306 int current_map_x;
307 int current_map_y;
308 if (*ctx_.current_world == 0) {
309 current_map_x = current_highlighted_map % 8;
310 current_map_y = current_highlighted_map / 8;
311 } else if (*ctx_.current_world == 1) {
312 current_map_x = (current_highlighted_map - 0x40) % 8;
313 current_map_y = (current_highlighted_map - 0x40) / 8;
314 } else {
315 current_map_x = (current_highlighted_map - 0x80) % 8;
316 current_map_y = (current_highlighted_map - 0x80) / 8;
317 }
319 current_map_y * kOverworldMapSize,
321 }
322 }
323
324 // Ensure current map has texture created for rendering
327 if (!hovered_project_terrain && *hovered_map != *ctx_.current_map) {
328 callbacks_.ensure_map_texture(*hovered_map);
329 }
330 }
331
332 if ((*ctx_.maps_bmp)[*ctx_.current_map].modified()) {
335 }
338 }
339
340 // Ensure tile16 blockset is fully updated before rendering
344 }
345
346 // Update map texture with the traditional direct update approach
350 (*ctx_.maps_bmp)[*ctx_.current_map].set_modified(false);
351 }
352
353 return absl::OkStatus();
354}
355
356// =============================================================================
357// Map Interaction
358// =============================================================================
359
360bool IsMapSelectClick(const MapClickInput& input) {
361 switch (input.mode) {
364 return input.right_clicked && !input.shift;
366 return input.left_released && input.left_press_owned &&
367 !input.left_dragged && !input.entity_hovered;
368 }
369 return false;
370}
371
372std::optional<int> CanvasNavigationManager::MapUnderCursor() const {
373 if (!ctx_.ow_map_canvas) {
374 return std::nullopt;
375 }
376 if (ctx_.hovered_map && *ctx_.hovered_map >= 0 &&
377 *ctx_.hovered_map < DisplayedMapCount(ctx_, callbacks_) &&
378 (IsProjectTerrainMap(callbacks_, *ctx_.hovered_map) ||
380 return *ctx_.hovered_map;
381 }
382 return MapFromCanvasPosition(ctx_, callbacks_,
384}
385
388 return false;
389 }
390 const std::optional<int> map = MapUnderCursor();
391 if (!map) {
392 return false;
393 }
394 if (*ctx_.current_map != *map) {
395 SelectMapForEditing(ctx_, callbacks_, *map, /*respect_pin=*/false);
396 }
397 return true;
398}
399
402 !ctx_.current_map) {
403 return;
404 }
406 return;
407 }
408
409 const ImGuiIO& io = ImGui::GetIO();
410 const float threshold = io.MouseDragThreshold;
411
412 // Press ownership. This runs only while the canvas item is hovered (not
413 // blocked by a popup), so it records only presses that land on the canvas.
414 // io.MouseClickedTime names the latest left press: a release ends a press
415 // this canvas owns only when that time still matches, so a press that
416 // dismissed a popup or began on another window never selects on release.
417 const std::optional<int> map_under_cursor = MapUnderCursor();
418 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
419 left_press_time_ = io.MouseClickedTime[ImGuiMouseButton_Left];
420 left_press_map_ = map_under_cursor;
421 }
422
423 MapClickInput input;
424 input.mode = *ctx_.current_mode;
425 input.left_released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
426 input.left_dragged = io.MouseDragMaxDistanceSqr[ImGuiMouseButton_Left] >=
427 threshold * threshold;
428 input.left_press_owned =
429 left_press_map_.has_value() && map_under_cursor == left_press_map_ &&
430 left_press_time_ == io.MouseClickedTime[ImGuiMouseButton_Left];
431 input.right_clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
432 input.shift = io.KeyShift;
433 input.entity_hovered =
435
436 // Paint modes: the right-click Tile16 sample (and right-drag brush capture)
437 // stays in TilePaintingManager::CheckForSelectRectangle, which runs after
438 // this; sampling here too made every right-click select the tile twice.
439 if (IsMapSelectClick(input)) {
440 (void)SelectMapUnderCursor();
441 }
442 if (input.left_released) {
443 left_press_map_.reset();
444 }
445
447 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) &&
450 }
451
452 // Middle-drag is exclusively navigation. Pinning lives in the toolbar,
453 // Ctrl+L shortcut, and context menu so panning cannot silently freeze tracking.
454}
455
456// =============================================================================
457// Pan and Zoom
458// =============================================================================
459
460ImVec2 StickyWheelPan::Consume(ImVec2 wheel, float dt, float step_px,
461 float snap_px) {
462 if (std::fabs(wheel.x) < kOverworldWheelDeadzone)
463 wheel.x = 0.0f;
464 if (std::fabs(wheel.y) < kOverworldWheelDeadzone)
465 wheel.y = 0.0f;
466 if (wheel.x == 0.0f && wheel.y == 0.0f) {
467 idle_seconds += dt;
469 residual = ImVec2(0.0f, 0.0f);
470 }
471 return ImVec2(0.0f, 0.0f);
472 }
473 idle_seconds = 0.0f;
474
475 const float snap = std::max(snap_px, 1.0f);
476 auto axis = [&](float input, float& rest) -> float {
477 if (input == 0.0f)
478 return 0.0f;
479 // Wheel up/left (positive) scrolls toward the origin.
480 const float delta = -input * step_px;
481 if ((delta > 0.0f) != (rest > 0.0f) && rest != 0.0f) {
482 rest = 0.0f; // Reversal: respond immediately, no leftover travel.
483 }
484 rest += delta;
485 const float out = std::trunc(rest / snap) * snap;
486 rest -= out;
487 return out;
488 };
489 return ImVec2(axis(wheel.x, residual.x), axis(wheel.y, residual.y));
490}
491
492ImVec2 ScrollForZoomAtAnchor(ImVec2 scroll, ImVec2 anchor, float old_scale,
493 float new_scale) {
494 if (old_scale <= 0.0f || new_scale <= 0.0f) {
495 return scroll;
496 }
497 const float ratio = new_scale / old_scale;
498 return ImVec2((scroll.x + anchor.x) * ratio - anchor.x,
499 (scroll.y + anchor.y) * ratio - anchor.y);
500}
501
503 const float scale =
505 constexpr float kWorldSize = kOverworldMapSize * 8.0f; // 4096
506 return ImVec2(kWorldSize * scale, kWorldSize * scale);
507}
508
509ImVec2 CanvasNavigationManager::ClampScroll(ImVec2 scroll) const {
510 const ImVec2 content = ContentSize();
511 const float max_x = std::max(0.0f, content.x - viewport_size_.x);
512 const float max_y = std::max(0.0f, content.y - viewport_size_.y);
513 // Whole pixels: fractional scroll makes pixel art shimmer while moving.
514 return ImVec2(std::round(std::clamp(scroll.x, 0.0f, max_x)),
515 std::round(std::clamp(scroll.y, 0.0f, max_y)));
516}
517
519 ImVec2 anchor) {
520 if (!ctx_.ow_map_canvas)
521 return;
522 const float old_scale = ctx_.ow_map_canvas->global_scale();
523 new_scale = std::clamp(new_scale, kOverworldMinZoom, kOverworldMaxZoom);
524 if (new_scale == old_scale)
525 return;
526 const ImVec2 base = pending_scroll_.value_or(last_scroll_);
528 pending_scroll_ = ScrollForZoomAtAnchor(base, anchor, old_scale, new_scale);
529}
530
531void CanvasNavigationManager::ZoomBySteps(int steps, ImVec2 anchor) {
532 if (!ctx_.ow_map_canvas || steps == 0)
533 return;
536}
537
539 if (!ctx_.ow_map_canvas) {
540 return;
541 }
542 const ImGuiIO& io = ImGui::GetIO();
543 std::optional<ImVec2> target = pending_scroll_;
544 pending_scroll_.reset();
545
546 // ---- Drag pan -------------------------------------------------------------
547 // Only a press that lands on the canvas starts a pan; drags that begin on
548 // scrollbars, other panels or popups never move the map.
549 const bool over_entity =
551 const bool dragging_entity =
554 if (ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
555 pan_button_ = ImGuiMouseButton_Middle;
556 pan_active_ = true; // Middle only pans: follow the cursor at once.
557 } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
559 !over_entity && !dragging_entity) {
560 pan_button_ = ImGuiMouseButton_Left;
561 pan_active_ = false; // Becomes a pan once past the drag threshold.
562 }
563 if (pan_button_ >= 0) {
564 pan_anchor_mouse_ = io.MousePos;
566 }
567 }
568 if (pan_button_ >= 0) {
569 if (!ImGui::IsMouseDown(pan_button_)) {
570 pan_button_ = -1;
571 pan_active_ = false;
572 } else {
573 if (!pan_active_ && ImGui::IsMouseDragging(pan_button_)) {
574 pan_active_ = !dragging_entity;
575 }
576 if (pan_active_) {
577 // Content stays pinned under the grab point: no drift, no inertia.
578 target = ImVec2(
579 pan_anchor_scroll_.x - (io.MousePos.x - pan_anchor_mouse_.x),
580 pan_anchor_scroll_.y - (io.MousePos.y - pan_anchor_mouse_.y));
581 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
582 }
583 }
584 }
585
586 // ---- Wheel: pan in detents, Cmd/Ctrl+wheel zooms at the cursor -------------
587 const bool zoom_modifier =
588 io.KeyCtrl || (gui::IsMacPlatform() && io.KeySuper);
590 ImVec2 wheel(io.MouseWheelH, io.MouseWheel);
591 if (!gui::IsMacPlatform() && io.KeyShift && wheel.x == 0.0f) {
592 wheel = ImVec2(wheel.y, 0.0f); // Shift+wheel scrolls horizontally.
593 }
594 if (zoom_modifier) {
595 if (wheel.y != 0.0f) {
596 zoom_idle_seconds_ = 0.0f;
597 zoom_residual_ += wheel.y;
598 const int steps = static_cast<int>(
600 if (steps != 0) {
602 if (target) {
603 pending_scroll_ = target;
604 }
605 const ImVec2 anchor(io.MousePos.x - viewport_min_.x,
606 io.MousePos.y - viewport_min_.y);
607 ZoomBySteps(steps, anchor);
608 target = pending_scroll_;
609 pending_scroll_.reset();
610 }
611 }
612 } else {
613 const float scale = ctx_.ow_map_canvas->global_scale();
614 const float snap = std::max(1.0f, kOverworldPanSnapMapPx * scale);
615 const float step =
616 std::max(snap, std::round(kOverworldWheelPanPx / snap) * snap);
617 const ImVec2 delta = wheel_pan_.Consume(wheel, io.DeltaTime, step, snap);
618 if (delta.x != 0.0f || delta.y != 0.0f) {
619 const ImVec2 base = target.value_or(last_scroll_);
620 target = ImVec2(base.x + delta.x, base.y + delta.y);
621 }
622 }
623 }
624 if (!zoom_modifier || io.MouseWheel == 0.0f) {
625 zoom_idle_seconds_ += io.DeltaTime;
627 zoom_residual_ = 0.0f;
628 }
629 }
630
631 // Explicit content size keeps ImGui's clamp correct on the frame the scale
632 // changes (the canvas item that would define it is submitted later).
633 ImGui::SetNextWindowContentSize(ContentSize());
634 if (target) {
635 const ImVec2 clamped = viewport_known_ ? ClampScroll(*target)
636 : ImVec2(std::max(0.0f, target->x),
637 std::max(0.0f, target->y));
638 ImGui::SetNextWindowScroll(clamped);
639 last_scroll_ = clamped;
640 }
641}
642
643void CanvasNavigationManager::EndCanvasViewport(bool canvas_item_hovered) {
644 ImGuiWindow* window = ImGui::GetCurrentWindow();
645 if (!window) {
646 return;
647 }
648 const ImVec2 previous_size = viewport_size_;
649 const bool had_viewport = viewport_known_;
650 viewport_min_ = window->InnerRect.Min;
651 viewport_size_ = window->InnerRect.GetSize();
652 last_scroll_ = window->Scroll;
653 canvas_item_hovered_ = canvas_item_hovered;
654 canvas_window_hovered_ = ImGui::IsWindowHovered();
655 viewport_known_ = viewport_size_.x > 0.0f && viewport_size_.y > 0.0f;
657 const int map = *pending_center_map_;
658 // Scrollbars can change InnerRect after the first content frame. Recenter
659 // until the measured size settles, then release this startup request.
660 if (had_viewport && previous_size.x == viewport_size_.x &&
661 previous_size.y == viewport_size_.y) {
662 pending_center_map_.reset();
663 }
664 CenterOnMap(map);
665 }
666}
667
670 ImGui::GetCurrentContext() ? ImGui::GetIO().MousePos : ImVec2(0, 0);
671}
672
674 ZoomBySteps(1, ImVec2(viewport_size_.x * 0.5f, viewport_size_.y * 0.5f));
675}
676
678 ZoomBySteps(-1, ImVec2(viewport_size_.x * 0.5f, viewport_size_.y * 0.5f));
679}
680
683 return;
684 }
685 constexpr float kWorldSize = kOverworldMapSize * 8.0f;
686 const int world = ctx_.current_world ? *ctx_.current_world : 0;
687 const int world_maps =
688 std::clamp(DisplayedMapCount(ctx_, callbacks_) - world * 0x40, 1, 64);
689 const float world_height = ((world_maps + 7) / 8) * kOverworldMapSize;
690 const float scale =
691 std::min(viewport_size_.x / kWorldSize, viewport_size_.y / world_height);
693 std::clamp(scale, kOverworldMinZoom, kOverworldMaxZoom));
694 pending_scroll_ = ImVec2(0.0f, 0.0f);
695}
696
698 pending_center_map_.reset();
699 if (ctx_.ow_map_canvas) {
701 }
702 pending_scroll_ = ImVec2(0.0f, 0.0f);
703}
704
710
712 const int map_count = DisplayedMapCount(ctx_, callbacks_);
713 if (!ctx_.ow_map_canvas || map_id < 0 || map_id >= map_count) {
714 return;
715 }
716 // Startup jumps arrive before the canvas child has a measured viewport.
717 // Wait for that size rather than placing the map's center at the top-left.
718 if (!viewport_known_) {
719 pending_center_map_ = map_id;
720 return;
721 }
722 float scale = ctx_.ow_map_canvas->global_scale();
723 if (scale <= 0.0f)
724 scale = 1.0f;
725 const int map_in_world = map_id % 0x40;
726 const float center_x =
727 ((map_in_world % 8) * kOverworldMapSize + kOverworldMapSize / 2.0f) *
728 scale;
729 const float center_y =
730 ((map_in_world / 8) * kOverworldMapSize + kOverworldMapSize / 2.0f) *
731 scale;
732 pending_scroll_ = ImVec2(center_x - viewport_size_.x * 0.5f,
733 center_y - viewport_size_.y * 0.5f);
734}
735
736void CanvasNavigationManager::CenterOnWorldTile(int world_x, int world_y) {
738 return;
739 }
740 const float scale = std::max(ctx_.ow_map_canvas->global_scale(), 0.01f);
742 ImVec2((world_x * 16.0f + 8.0f) * scale - viewport_size_.x * 0.5f,
743 (world_y * 16.0f + 8.0f) * scale - viewport_size_.y * 0.5f);
744}
745
746// =============================================================================
747// Blockset Selector Synchronization
748// =============================================================================
749
753 (*ctx_.blockset_selector)->ScrollToTile(*ctx_.current_tile16);
754 return;
755 }
756
757 // CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
758 // The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT
759 // window, and when called from CheckForSelectRectangle() during overworld
760 // canvas rendering, it incorrectly scrolls the overworld canvas instead of
761 // the tile16 selector.
762 //
763 // The blockset_selector_ should always be available in modern code paths.
764 // If it's not available, we skip scrolling rather than scroll the wrong
765 // window.
766}
767
777
778// =============================================================================
779// Background Pre-loading
780// =============================================================================
781
783#ifdef __EMSCRIPTEN__
784 // WASM: Skip pre-loading entirely - it blocks the main thread and causes
785 // stuttering. The tileset cache and debouncing provide enough optimization.
786 return;
787#endif
788
789 if (!ctx_.overworld || center_map < 0 ||
790 center_map >= ctx_.overworld->inspected_map_count() ||
791 IsProjectTerrainMap(callbacks_, center_map)) {
792 return;
793 }
794
795 preload_queue_.clear();
796
797 // Calculate grid position (8x8 maps per world)
798 int world_offset = (center_map / 64) * 64;
799 int local_index = center_map % 64;
800 int map_x = local_index % 8;
801 int map_y = local_index / 8;
802 const int max_rows =
803 center_map >= zelda3::kSpecialWorldMapIdStart &&
804 DisplayedMapCount(ctx_, callbacks_) <= zelda3::kNumOverworldMaps
805 ? 4
806 : 8;
807
808 // Add adjacent maps (4-connected neighbors)
809 static const int dx[] = {-1, 1, 0, 0};
810 static const int dy[] = {0, 0, -1, 1};
811
812 for (int i = 0; i < 4; ++i) {
813 int nx = map_x + dx[i];
814 int ny = map_y + dy[i];
815
816 // Check bounds (world grid; standard special world is 4 rows, expanded is 8)
817 if (nx >= 0 && nx < 8 && ny >= 0 && ny < max_rows) {
818 int neighbor_index = world_offset + ny * 8 + nx;
819 // Only queue if not already built
820 if (neighbor_index >= 0 &&
821 neighbor_index < ctx_.overworld->inspected_map_count() &&
822 !IsProjectTerrainMap(callbacks_, neighbor_index) &&
823 ctx_.overworld->overworld_map(neighbor_index) &&
824 !ctx_.overworld->overworld_map(neighbor_index)->is_built()) {
825 preload_queue_.push_back(neighbor_index);
826 }
827 }
828 }
829}
830
832#ifdef __EMSCRIPTEN__
833 // WASM: Pre-loading disabled - each EnsureMapBuilt call blocks for 100-200ms
834 // which causes unacceptable frame drops. Native builds use this for smoother
835 // UX.
836 return;
837#endif
838
839 if (!ctx_.overworld || preload_queue_.empty()) {
840 return;
841 }
842
843 // Process one map per frame to avoid blocking (native only)
844 int map_to_preload = preload_queue_.back();
845 preload_queue_.pop_back();
846 if (IsProjectTerrainMap(callbacks_, map_to_preload) ||
847 !ctx_.overworld->overworld_map(map_to_preload)) {
848 return;
849 }
850
851 // Silent build - don't update UI state
852 auto status = ctx_.overworld->EnsureMapBuilt(map_to_preload);
853 if (!status.ok()) {
854 // Log but don't interrupt - this is background work
855 LOG_DEBUG("CanvasNavigationManager",
856 "Background preload of map %d failed: %s", map_to_preload,
857 status.message().data());
858 }
859}
860
861} // 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:324
bool IsMouseHovering() const
Definition canvas.h:354
void DrawOutline(int x, int y, int w, int h)
Definition canvas.cc:1362
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:845
int inspected_map_count() const
Definition overworld.h:447
auto overworld_map(int i) const
Definition overworld.h:770
void set_current_map(int i)
Definition overworld.h:844
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
std::optional< int > MapFromCanvasPosition(const CanvasNavigationContext &ctx, const CanvasNavigationCallbacks &callbacks, ImVec2 scaled_position)
void SelectMapForEditing(const CanvasNavigationContext &ctx, const CanvasNavigationCallbacks &callbacks, int map_id, bool respect_pin)
void SetHoveredMap(const CanvasNavigationContext &ctx, int map_id)
void SelectMapFallback(const CanvasNavigationContext &ctx, const CanvasNavigationCallbacks &callbacks, int map_id, bool respect_pin)
int DisplayedMapCount(const CanvasNavigationContext &ctx, const CanvasNavigationCallbacks &callbacks)
bool IsProjectTerrainMap(const CanvasNavigationCallbacks &callbacks, int map_id)
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:244
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