402 lines
16 KiB
Markdown
402 lines
16 KiB
Markdown
# Canvas Coordinate Synchronization and Scroll Fix
|
|
|
|
**Date**: October 10, 2025
|
|
**Issues**:
|
|
1. Overworld map highlighting regression after canvas refactoring
|
|
2. Overworld canvas scrolling unexpectedly when selecting tiles
|
|
3. Vanilla Dark/Special World large map outlines not displaying
|
|
**Status**: Fixed
|
|
|
|
## Problem Summary
|
|
|
|
After the canvas refactoring (commits f538775954, 60ddf76331), two critical bugs appeared:
|
|
|
|
1. **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.
|
|
|
|
2. **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)**:
|
|
```cpp
|
|
const auto mouse_position = ImGui::GetIO().MousePos; // ❌ Screen coordinates!
|
|
const auto canvas_zero_point = ow_map_canvas_.zero_point();
|
|
int map_x = (mouse_position.x - canvas_zero_point.x) / kOverworldMapSize;
|
|
```
|
|
|
|
**After (FIXED)**:
|
|
```cpp
|
|
const auto mouse_position = ow_map_canvas_.hover_mouse_pos(); // World coordinates!
|
|
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)**:
|
|
```cpp
|
|
void Canvas::DrawBackground(ImVec2 canvas_size) {
|
|
// ... setup code ...
|
|
ImGui::InvisibleButton(canvas_id_.c_str(), scaled_size, kMouseFlags);
|
|
|
|
// ❌ mouse_pos_in_canvas_ only updated in DrawTilePainter() during painting!
|
|
|
|
if (config_.is_draggable && IsItemHovered()) {
|
|
// ... pan handling ...
|
|
}
|
|
}
|
|
```
|
|
|
|
`mouse_pos_in_canvas_` was only updated inside painting methods:
|
|
- `DrawTilePainter()` at line 741
|
|
- `DrawSolidTilePainter()` at line 860
|
|
- `DrawTileSelector()` at line 929
|
|
|
|
**After (FIXED)**:
|
|
```cpp
|
|
void Canvas::DrawBackground(ImVec2 canvas_size) {
|
|
// ... setup code ...
|
|
ImGui::InvisibleButton(canvas_id_.c_str(), scaled_size, kMouseFlags);
|
|
|
|
// CRITICAL FIX: Always update hover position when hovering
|
|
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; // Updated every frame during hover
|
|
}
|
|
|
|
if (config_.is_draggable && IsItemHovered()) {
|
|
// ... pan handling ...
|
|
}
|
|
}
|
|
```
|
|
|
|
## Technical Details
|
|
|
|
### Coordinate Spaces
|
|
|
|
yaze has three coordinate spaces:
|
|
|
|
1. **Screen Space**: Absolute pixel coordinates on the monitor
|
|
- `ImGui::GetIO().MousePos` returns this
|
|
- Never use this for canvas operations!
|
|
|
|
2. **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.
|
|
|
|
3. **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):
|
|
```cpp
|
|
auto hover_pos = canvas.hover_mouse_pos(); // Updates continuously
|
|
int map_x = hover_pos.x / kOverworldMapSize;
|
|
```
|
|
|
|
**For Active Painting** (DrawOverworldEdits):
|
|
```cpp
|
|
auto paint_pos = canvas.drawn_tile_position(); // Updates only during drag
|
|
int map_x = paint_pos.x / kOverworldMapSize;
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Visual Testing
|
|
|
|
**Map Highlighting Test**:
|
|
1. Open overworld editor
|
|
2. Hover mouse over different maps (without clicking)
|
|
3. Verify current map highlights correctly
|
|
4. Test with different scale levels (0.25x - 4.0x)
|
|
5. Test with scrolled canvas
|
|
|
|
**Scroll Regression Test**:
|
|
1. Open overworld editor
|
|
2. Switch to Dark World (or any world)
|
|
3. Right-click on overworld canvas to select a tile
|
|
4. **Expected**: Tile16 blockset selector scrolls to show the selected tile
|
|
5. **Expected**: Overworld canvas does NOT scroll
|
|
6. ❌ **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:
|
|
```bash
|
|
./build/bin/yaze_test --unit
|
|
```
|
|
|
|
## Impact Analysis
|
|
|
|
### Files Changed
|
|
1. `src/app/editor/overworld/overworld_editor.cc` (line 1041-1049)
|
|
- Changed from screen coordinates to canvas hover coordinates
|
|
- Removed incorrect `canvas_zero_point` subtraction
|
|
|
|
2. `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
|
|
|
|
3. `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
|
|
|
|
4. `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)
|
|
|
|
5. `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:
|
|
- `src/app/editor/overworld/overworld_entity_renderer.cc:68-69` - Uses `hover_mouse_pos()` for entity placement
|
|
- `src/app/editor/overworld/overworld_editor.cc:664` - Uses `drawn_tile_position()` for painting
|
|
|
|
## Multi-Area Map Support
|
|
|
|
The fix properly handles all area sizes:
|
|
|
|
### Standard Maps (512x512)
|
|
```cpp
|
|
int map_x = hover_pos.x / 512; // 0-7 range
|
|
int map_y = hover_pos.y / 512; // 0-7 range
|
|
int map_index = map_x + map_y * 8; // 0-63 (8x8 grid)
|
|
```
|
|
|
|
### ZSCustomOverworld v3 Large Maps (1024x1024)
|
|
```cpp
|
|
int map_x = hover_pos.x / 1024; // Large map X
|
|
int map_y = hover_pos.y / 1024; // Large map Y
|
|
// Parent map calculation handled in lines 1073-1190
|
|
```
|
|
|
|
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)**:
|
|
```cpp
|
|
void OverworldEditor::ScrollBlocksetCanvasToCurrentTile() {
|
|
if (blockset_selector_) {
|
|
blockset_selector_->ScrollToTile(current_tile16_);
|
|
return;
|
|
}
|
|
|
|
// Fallback: maintain legacy behavior when the selector is unavailable.
|
|
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);
|
|
|
|
// ❌ BUG: This scrolls whatever ImGui window is currently active!
|
|
// When called from overworld canvas, it scrolls the overworld instead of tile16 selector!
|
|
ImGui::SetScrollX(std::max(0.0f, scroll_x));
|
|
ImGui::SetScrollY(std::max(0.0f, scroll_y));
|
|
}
|
|
```
|
|
|
|
**After (FIXED)**:
|
|
```cpp
|
|
void OverworldEditor::ScrollBlocksetCanvasToCurrentTile() {
|
|
if (blockset_selector_) {
|
|
blockset_selector_->ScrollToTile(current_tile16_); // Correct: Targets specific canvas
|
|
return;
|
|
}
|
|
|
|
// CRITICAL FIX: Do NOT use fallback scrolling from overworld canvas context!
|
|
// The fallback code uses ImGui::SetScrollX/Y which scrolls the CURRENT window,
|
|
// and when called from CheckForSelectRectangle() during overworld canvas rendering,
|
|
// it incorrectly scrolls the overworld canvas instead of the tile16 selector.
|
|
//
|
|
// The blockset_selector_ should always be available in modern code paths.
|
|
// If it's not available, we skip scrolling rather than scroll the wrong window.
|
|
}
|
|
```
|
|
|
|
**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)**:
|
|
```cpp
|
|
if (current_mode == EditingMode::DRAW_TILE) {
|
|
CheckForOverworldEdits();
|
|
}
|
|
if (IsItemHovered()) // ❌ Checks LAST item (overlay/entity), not canvas!
|
|
status_ = CheckForCurrentMap();
|
|
```
|
|
|
|
**After (FIXED)**:
|
|
```cpp
|
|
if (current_mode == EditingMode::DRAW_TILE) {
|
|
CheckForOverworldEdits();
|
|
}
|
|
// CRITICAL FIX: Use canvas hover state, not ImGui::IsItemHovered()
|
|
// IsItemHovered() checks the LAST drawn item, which could be entities/overlay,
|
|
// not the canvas InvisibleButton. ow_map_canvas_.IsMouseHovering() correctly
|
|
// tracks whether mouse is over the canvas area.
|
|
if (ow_map_canvas_.IsMouseHovering()) // Checks canvas hover state directly
|
|
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)**:
|
|
```cpp
|
|
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; // ❌ Wrong for Dark/Special!
|
|
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)**:
|
|
```cpp
|
|
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();
|
|
|
|
// CRITICAL FIX: Account for world offset when calculating parent coordinates
|
|
int parent_map_x;
|
|
int parent_map_y;
|
|
if (current_world_ == 0) {
|
|
// Light World (0x00-0x3F)
|
|
parent_map_x = highlight_parent % 8;
|
|
parent_map_y = highlight_parent / 8;
|
|
} else if (current_world_ == 1) {
|
|
// Dark World (0x40-0x7F) - subtract 0x40 to get display coordinates
|
|
parent_map_x = (highlight_parent - 0x40) % 8;
|
|
parent_map_y = (highlight_parent - 0x40) / 8;
|
|
} else {
|
|
// Special World (0x80-0x9F) - subtract 0x80 to get display coordinates
|
|
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
|
|
|
|
1. **Canvas Mode System**: Complete the interaction handler modes (tile paint, select, etc.)
|
|
2. **Persistent Context Menus**: Implement mode switching through context menu popups
|
|
3. **Debugging Visualization**: Add canvas coordinate overlay for debugging
|
|
4. **E2E Tests**: Create end-to-end tests for overworld map highlighting workflow
|
|
|
|
## Related Documentation
|
|
- `docs/G1-canvas-guide.md` - Canvas system architecture
|
|
- `docs/E5-debugging-guide.md` - Debugging techniques
|
|
- `docs/debugging-startup-flags.md` - CLI flags for editor testing
|