Date: October 10, 2025 Issues:
- Overworld map highlighting regression after canvas refactoring
- Overworld canvas scrolling unexpectedly when selecting tiles
- Vanilla Dark/Special World large map outlines not displaying Status: ✅ Fixed
Problem Summary
After the canvas refactoring (commits f538775954, 60ddf76331), two critical bugs appeared:
- Map highlighting broken: The overworld editor stopped properly highlighting the current map when hovering. The map highlighting only worked during active tile painting, not during normal mouse hover.
- Wrong canvas scrolling: When right-clicking to select tiles (especially on Dark World), the overworld canvas would scroll unexpectedly instead of the tile16 blockset selector.
Root Cause
The regression had FIVE issues:
Issue 1: Wrong Coordinate System (Line 1041)
File: src/app/editor/overworld/overworld_editor.cc:1041
Before (BROKEN):
const auto mouse_position = ImGui::GetIO().MousePos;
const auto canvas_zero_point = ow_map_canvas_.zero_point();
int map_x = (mouse_position.x - canvas_zero_point.x) / kOverworldMapSize;
After (FIXED):
const auto mouse_position = ow_map_canvas_.hover_mouse_pos();
int map_x = mouse_position.x / kOverworldMapSize;
Why This Was Wrong:
ImGui::GetIO().MousePos
returns screen space coordinates (absolute position on screen)
- The canvas may be scrolled, scaled, or positioned anywhere on screen
- Screen coordinates don't account for canvas scrolling/offset
hover_mouse_pos()
returns canvas/world space coordinates (relative to canvas content)
Issue 2: Hover Position Not Updated (Line 416)
File: src/app/gui/canvas.cc:416
Before (BROKEN):
void Canvas::DrawBackground(ImVec2 canvas_size) {
ImGui::InvisibleButton(canvas_id_.c_str(), scaled_size, kMouseFlags);
if (config_.is_draggable && IsItemHovered()) {
}
}
mouse_pos_in_canvas_
was only updated inside painting methods:
DrawTilePainter()
at line 741
DrawSolidTilePainter()
at line 860
DrawTileSelector()
at line 929
After (FIXED):
void Canvas::DrawBackground(ImVec2 canvas_size) {
ImGui::InvisibleButton(canvas_id_.c_str(), scaled_size, kMouseFlags);
if (IsItemHovered()) {
const ImGuiIO& io = GetIO();
const ImVec2 origin(canvas_p0_.x + scrolling_.x, canvas_p0_.y + scrolling_.y);
const ImVec2 mouse_pos(io.MousePos.x - origin.x, io.MousePos.y - origin.y);
mouse_pos_in_canvas_ = mouse_pos;
}
if (config_.is_draggable && IsItemHovered()) {
}
}
Technical Details
Coordinate Spaces
yaze has three coordinate spaces:
- Screen Space: Absolute pixel coordinates on the monitor
ImGui::GetIO().MousePos
returns this
- Never use this for canvas operations!
- Canvas/World Space: Coordinates relative to canvas content
- Accounts for canvas scrolling and offset
Canvas::hover_mouse_pos()
returns this
- Use this for map calculations, entity positioning, etc.
- Tile/Grid Space: Coordinates in tile units (not pixels)
Canvas::CanvasToTile()
converts from canvas to grid space
- Used by automation API
Usage Patterns
For Hover/Highlighting (CheckForCurrentMap):
auto hover_pos = canvas.hover_mouse_pos();
int map_x = hover_pos.x / kOverworldMapSize;
For Active Painting (DrawOverworldEdits):
auto paint_pos = canvas.drawn_tile_position();
int map_x = paint_pos.x / kOverworldMapSize;
Testing
Visual Testing
Map Highlighting Test:
- Open overworld editor
- Hover mouse over different maps (without clicking)
- Verify current map highlights correctly
- Test with different scale levels (0.25x - 4.0x)
- Test with scrolled canvas
Scroll Regression Test:
- Open overworld editor
- Switch to Dark World (or any world)
- Right-click on overworld canvas to select a tile
- ✅ Expected: Tile16 blockset selector scrolls to show the selected tile
- ✅ Expected: Overworld canvas does NOT scroll
- ❌ Before fix: Overworld canvas would scroll unexpectedly
Unit Tests
Created test/unit/gui/canvas_coordinate_sync_test.cc
with regression tests:
HoverMousePos_IndependentFromDrawnPos
: Verifies hover vs paint separation
CoordinateSpace_WorldNotScreen
: Ensures world coordinates used
MapCalculation_SmallMaps
: Tests 512x512 map boundaries
MapCalculation_LargeMaps
: Tests 1024x1024 v3 ASM maps
OverworldMapHighlight_UsesHoverNotDrawn
: Critical regression test
OverworldMapIndex_From8x8Grid
: Tests all three worlds (Light/Dark/Special)
Run tests:
./build/bin/yaze_test --unit
Impact Analysis
Files Changed
src/app/editor/overworld/overworld_editor.cc
(line 1041-1049)
- Changed from screen coordinates to canvas hover coordinates
- Removed incorrect
canvas_zero_point
subtraction
src/app/gui/canvas.cc
(line 414-421)
- Added continuous hover position tracking in
DrawBackground()
- Now updates
mouse_pos_in_canvas_
every frame when hovering
src/app/editor/overworld/overworld_editor.cc
(line 2344-2360)
- Removed fallback scroll code that scrolled the wrong canvas
- Now only uses
blockset_selector_->ScrollToTile()
which targets the correct canvas
src/app/editor/overworld/overworld_editor.cc
(line 1403-1408)
- Changed from
ImGui::IsItemHovered()
(checks last drawn item)
- To
ow_map_canvas_.IsMouseHovering()
(checks canvas hover state directly)
src/app/editor/overworld/overworld_editor.cc
(line 1133-1151)
- Added world offset subtraction for vanilla large map parent coordinates
- Now properly accounts for Dark World (0x40-0x7F) and Special World (0x80-0x9F)
Affected Functionality
- ✅ Fixed: Overworld map highlighting during hover (all worlds, all ROM types)
- ✅ Fixed: Vanilla Dark World large map highlighting (was drawing off-screen)
- ✅ Fixed: Vanilla Special World large map highlighting (was drawing off-screen)
- ✅ Fixed: Overworld canvas no longer scrolls when selecting tiles
- ✅ Fixed: Tile16 selector properly scrolls to show selected tile (via blockset_selector_)
- ✅ Fixed: Entity renderer using
hover_mouse_pos()
(already worked correctly)
- ✅ Preserved: Tile painting using
drawn_tile_position()
(unchanged)
- ✅ Preserved: Multi-area map support (512x512, 1024x1024)
- ✅ Preserved: All three worlds (Light 0x00-0x3F, Dark 0x40-0x7F, Special 0x80+)
- ✅ Preserved: ZSCustomOverworld v3 large maps (already worked correctly)
Related Code That Works Correctly
These files already use the correct pattern:
Multi-Area Map Support
The fix properly handles all area sizes:
Standard Maps (512x512)
int map_x = hover_pos.x / 512;
int map_y = hover_pos.y / 512;
int map_index = map_x + map_y * 8;
ZSCustomOverworld v3 Large Maps (1024x1024)
int map_x = hover_pos.x / 1024;
int map_y = hover_pos.y / 1024;
The existing multi-area logic (lines 1068-1190) remains unchanged and works correctly with the fix.
Issue 3: Wrong Canvas Being Scrolled (Line 2344-2366)
File: src/app/editor/overworld/overworld_editor.cc:2344
Problem: When selecting tiles with right-click on the overworld canvas, ScrollBlocksetCanvasToCurrentTile()
was calling ImGui::SetScrollX/Y()
which scrolls the current ImGui window, not a specific canvas.
Call Stack:
DrawOverworldCanvas() // Overworld canvas is current window
└─ CheckForOverworldEdits() (line 1401)
└─ CheckForSelectRectangle() (line 793)
└─ ScrollBlocksetCanvasToCurrentTile() (line 916)
└─ ImGui::SetScrollX/Y() (lines 2364-2365) // ❌ Scrolls CURRENT window!
Before (BROKEN):
void OverworldEditor::ScrollBlocksetCanvasToCurrentTile() {
if (blockset_selector_) {
blockset_selector_->ScrollToTile(current_tile16_);
return;
}
constexpr int kTilesPerRow = 8;
constexpr int kTileDisplaySize = 32;
int tile_col = current_tile16_ % kTilesPerRow;
int tile_row = current_tile16_ / kTilesPerRow;
float tile_x = static_cast<float>(tile_col * kTileDisplaySize);
float tile_y = static_cast<float>(tile_row * kTileDisplaySize);
const ImVec2 window_size = ImGui::GetWindowSize();
float scroll_x = tile_x - (window_size.x / 2.0F) + (kTileDisplaySize / 2.0F);
float scroll_y = tile_y - (window_size.y / 2.0F) + (kTileDisplaySize / 2.0F);
ImGui::SetScrollX(std::max(0.0f, scroll_x));
ImGui::SetScrollY(std::max(0.0f, scroll_y));
}
After (FIXED):
void OverworldEditor::ScrollBlocksetCanvasToCurrentTile() {
if (blockset_selector_) {
blockset_selector_->ScrollToTile(current_tile16_);
return;
}
}
Why This Fixes It:
- The modern
blockset_selector_->ScrollToTile()
targets the specific tile16 selector canvas
- The fallback
ImGui::SetScrollX/Y()
has no context - it just scrolls the active window
- By removing the fallback, we prevent scrolling the wrong canvas
- If
blockset_selector_
is null (shouldn't happen in modern builds), we safely do nothing instead of breaking user interaction
Issue 4: Wrong Hover Check (Line 1403)
File: src/app/editor/overworld/overworld_editor.cc:1403
Problem: The code was using ImGui::IsItemHovered()
to check if the mouse was over the canvas, but this checks the last drawn ImGui item, which could be entities, overlays, or anything drawn after the canvas's InvisibleButton. This meant hover detection was completely broken.
Call Stack:
DrawOverworldCanvas()
└─ DrawBackground() at line 1350 // Creates InvisibleButton (item A)
└─ DrawExits/Entrances/Items/Sprites() // Draws entities (items B, C, D...)
└─ DrawOverlayPreviewOnMap() // Draws overlay (item E)
└─ IsItemHovered() at line 1403 // ❌ Checks item E, not item A!
Before (BROKEN):
if (current_mode == EditingMode::DRAW_TILE) {
CheckForOverworldEdits();
}
if (IsItemHovered())
status_ = CheckForCurrentMap();
After (FIXED):
if (current_mode == EditingMode::DRAW_TILE) {
CheckForOverworldEdits();
}
if (ow_map_canvas_.IsMouseHovering())
status_ = CheckForCurrentMap();
Why This Fixes It:
IsItemHovered()
is context-sensitive - it checks whatever the last ImGui::*()
call was
- After drawing entities and overlays, the "last item" is NOT the canvas
Canvas::IsMouseHovering()
tracks the hover state from the InvisibleButton in DrawBackground()
- This state is set correctly when the InvisibleButton is hovered (line 416 in canvas.cc)
Issue 5: Vanilla Large Map World Offset (Line 1132-1136)
File: src/app/editor/overworld/overworld_editor.cc:1132-1136
Problem: For vanilla ROMs, the large map highlighting logic wasn't accounting for world offsets when calculating parent map coordinates. Dark World maps (0x40-0x7F) and Special World maps (0x80-0x9F) use map IDs with offsets, but the display grid coordinates are 0-7.
Before (BROKEN):
if (overworld_.overworld_map(current_map_)->is_large_map() ||
overworld_.overworld_map(current_map_)->large_index() != 0) {
const int highlight_parent =
overworld_.overworld_map(current_highlighted_map)->parent();
const int parent_map_x = highlight_parent % 8;
const int parent_map_y = highlight_parent / 8;
ow_map_canvas_.DrawOutline(parent_map_x * kOverworldMapSize,
parent_map_y * kOverworldMapSize,
large_map_size, large_map_size);
}
Example Bug:
- Dark World map 0x42 (parent) →
0x42 % 8 = 2
, 0x42 / 8 = 8
- This draws the outline at grid position (2, 8) which is off the screen!
- Correct position should be (2, 0) in the Dark World display grid
After (FIXED):
if (overworld_.overworld_map(current_map_)->is_large_map() ||
overworld_.overworld_map(current_map_)->large_index() != 0) {
const int highlight_parent =
overworld_.overworld_map(current_highlighted_map)->parent();
int parent_map_x;
int parent_map_y;
if (current_world_ == 0) {
parent_map_x = highlight_parent % 8;
parent_map_y = highlight_parent / 8;
} else if (current_world_ == 1) {
parent_map_x = (highlight_parent - 0x40) % 8;
parent_map_y = (highlight_parent - 0x40) / 8;
} else {
parent_map_x = (highlight_parent - 0x80) % 8;
parent_map_y = (highlight_parent - 0x80) / 8;
}
ow_map_canvas_.DrawOutline(parent_map_x * kOverworldMapSize,
parent_map_y * kOverworldMapSize,
large_map_size, large_map_size);
}
Why This Fixes It:
- Map IDs are absolute: Light World 0x00-0x3F, Dark World 0x40-0x7F, Special 0x80-0x9F
- Display coordinates are relative: Each world displays in an 8x8 grid (0-7, 0-7)
- Without subtracting the world offset, coordinates overflow the display grid
- This matches the same logic used for v3 large maps (lines 1084-1096) and small maps (lines 1141-1172)
Commit Reference
Canvas Refactoring Commits:
f538775954
- Organize Canvas Utilities and BPP Format Management
60ddf76331
- Integrate Canvas Automation API and Simplify Overworld Editor Controls
These commits moved canvas utilities to modular components but introduced the regression by not maintaining hover position tracking.
Future Improvements
- Canvas Mode System: Complete the interaction handler modes (tile paint, select, etc.)
- Persistent Context Menus: Implement mode switching through context menu popups
- Debugging Visualization: Add canvas coordinate overlay for debugging
- E2E Tests: Create end-to-end tests for overworld map highlighting workflow
Related Documentation