feat(canvas): implement event-driven interaction handling for canvas

- Introduced new files `canvas_interaction.cc` and `canvas_interaction.h` to encapsulate event-driven logic for tile painting, rectangle selection, and entity interactions.
- Refactored `CanvasInteractionHandler` to utilize the new event-based functions, improving code organization and maintainability.
- Added `canvas_events.h` to define event payload structures for various canvas interactions, enhancing clarity and usability.

Benefits:
- Streamlines interaction handling by separating concerns and reducing stateful dependencies.
- Improves testability and readability of the canvas interaction logic, facilitating future enhancements.
This commit is contained in:
scawful
2025-10-16 17:38:08 -04:00
parent bfad2f91fb
commit bc9d8912bf
5 changed files with 843 additions and 96 deletions

View File

@@ -0,0 +1,177 @@
#ifndef YAZE_APP_GUI_CANVAS_CANVAS_EVENTS_H
#define YAZE_APP_GUI_CANVAS_CANVAS_EVENTS_H
#include <vector>
#include "imgui/imgui.h"
namespace yaze {
namespace gui {
/**
* @brief Event payload for tile painting operations
*
* Represents a single tile paint action, either from a click or drag operation.
* Canvas-space coordinates are provided for positioning.
*/
struct TilePaintEvent {
ImVec2 position; ///< Canvas-space pixel coordinates
ImVec2 grid_position; ///< Grid-aligned tile position
int tile_id = -1; ///< Tile ID being painted (-1 if none)
bool is_drag = false; ///< True for continuous drag painting
bool is_complete = false; ///< True when paint action finishes
void Reset() {
position = ImVec2(-1, -1);
grid_position = ImVec2(-1, -1);
tile_id = -1;
is_drag = false;
is_complete = false;
}
};
/**
* @brief Event payload for rectangle selection operations
*
* Represents a multi-tile rectangular selection, typically from right-click drag.
* Provides both the rectangle bounds and the individual selected tile positions.
*/
struct RectSelectionEvent {
std::vector<ImVec2> selected_tiles; ///< Individual tile positions (grid coords)
ImVec2 start_pos; ///< Rectangle start (canvas coords)
ImVec2 end_pos; ///< Rectangle end (canvas coords)
int current_map = -1; ///< Map ID for coordinate calculation
bool is_complete = false; ///< True when selection finishes
bool is_active = false; ///< True while dragging
void Reset() {
selected_tiles.clear();
start_pos = ImVec2(-1, -1);
end_pos = ImVec2(-1, -1);
current_map = -1;
is_complete = false;
is_active = false;
}
/** @brief Get number of selected tiles */
size_t Count() const { return selected_tiles.size(); }
/** @brief Check if selection is empty */
bool IsEmpty() const { return selected_tiles.empty(); }
};
/**
* @brief Event payload for single tile selection
*
* Represents selecting a single tile, typically from a right-click.
*/
struct TileSelectionEvent {
ImVec2 tile_position; ///< Selected tile position (grid coords)
int tile_id = -1; ///< Selected tile ID
bool is_valid = false; ///< True if selection is valid
void Reset() {
tile_position = ImVec2(-1, -1);
tile_id = -1;
is_valid = false;
}
};
/**
* @brief Event payload for entity interactions
*
* Represents various entity interaction events (hover, click, drag).
* Used for exits, entrances, sprites, items, etc.
*/
struct EntityInteractionEvent {
enum class Type {
kNone, ///< No interaction
kHover, ///< Mouse hovering over entity
kClick, ///< Single click on entity
kDoubleClick, ///< Double click on entity
kDragStart, ///< Started dragging entity
kDragMove, ///< Dragging entity (continuous)
kDragEnd ///< Finished dragging entity
};
Type type = Type::kNone; ///< Type of interaction
int entity_id = -1; ///< Entity being interacted with
ImVec2 position; ///< Current entity position (canvas coords)
ImVec2 delta; ///< Movement delta (for drag events)
ImVec2 grid_position; ///< Grid-aligned position
bool is_valid = false; ///< True if event is valid
void Reset() {
type = Type::kNone;
entity_id = -1;
position = ImVec2(-1, -1);
delta = ImVec2(0, 0);
grid_position = ImVec2(-1, -1);
is_valid = false;
}
/** @brief Check if this is a drag event */
bool IsDragEvent() const {
return type == Type::kDragStart || type == Type::kDragMove ||
type == Type::kDragEnd;
}
/** @brief Check if this is a click event */
bool IsClickEvent() const {
return type == Type::kClick || type == Type::kDoubleClick;
}
};
/**
* @brief Event payload for hover preview
*
* Represents hover state for overlay rendering.
*/
struct HoverEvent {
ImVec2 position; ///< Canvas-space hover position
ImVec2 grid_position; ///< Grid-aligned hover position
bool is_valid = false; ///< True if hovering over canvas
void Reset() {
position = ImVec2(-1, -1);
grid_position = ImVec2(-1, -1);
is_valid = false;
}
};
/**
* @brief Combined interaction result for a frame
*
* Aggregates all possible interaction events for a single frame update.
* Handlers populate relevant events, consumers check which events occurred.
*/
struct CanvasInteractionEvents {
TilePaintEvent tile_paint;
RectSelectionEvent rect_selection;
TileSelectionEvent tile_selection;
EntityInteractionEvent entity_interaction;
HoverEvent hover;
/** @brief Reset all events */
void Reset() {
tile_paint.Reset();
rect_selection.Reset();
tile_selection.Reset();
entity_interaction.Reset();
hover.Reset();
}
/** @brief Check if any event occurred */
bool HasAnyEvent() const {
return tile_paint.is_complete ||
rect_selection.is_complete ||
tile_selection.is_valid ||
entity_interaction.is_valid ||
hover.is_valid;
}
};
} // namespace gui
} // namespace yaze
#endif // YAZE_APP_GUI_CANVAS_CANVAS_EVENTS_H

View File

@@ -0,0 +1,403 @@
#include "canvas_interaction.h"
#include <algorithm>
#include <cmath>
#include "imgui/imgui.h"
namespace yaze {
namespace gui {
namespace {
// Static state for rectangle selection (temporary until we have proper state management)
struct SelectRectState {
ImVec2 drag_start_pos = ImVec2(-1, -1);
bool is_dragging = false;
};
// Per-canvas state (keyed by canvas geometry pointer for simplicity)
// TODO(scawful): Replace with proper state management in Phase 2.5
thread_local SelectRectState g_select_rect_state;
} // namespace
// ============================================================================
// Helper Functions
// ============================================================================
ImVec2 AlignToGrid(ImVec2 pos, float grid_step) {
return ImVec2(std::floor(pos.x / grid_step) * grid_step,
std::floor(pos.y / grid_step) * grid_step);
}
ImVec2 GetMouseInCanvasSpace(const CanvasGeometry& geometry) {
const ImGuiIO& imgui_io = ImGui::GetIO();
const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x,
geometry.canvas_p0.y + geometry.scrolling.y);
return ImVec2(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y);
}
bool IsMouseInCanvas(const CanvasGeometry& geometry) {
const ImGuiIO& imgui_io = ImGui::GetIO();
return imgui_io.MousePos.x >= geometry.canvas_p0.x &&
imgui_io.MousePos.x <= geometry.canvas_p1.x &&
imgui_io.MousePos.y >= geometry.canvas_p0.y &&
imgui_io.MousePos.y <= geometry.canvas_p1.y;
}
ImVec2 CanvasToTileGrid(ImVec2 canvas_pos, float tile_size, float global_scale) {
const float scaled_size = tile_size * global_scale;
return ImVec2(std::floor(canvas_pos.x / scaled_size),
std::floor(canvas_pos.y / scaled_size));
}
// ============================================================================
// Rectangle Selection Implementation
// ============================================================================
RectSelectionEvent HandleRectangleSelection(
const CanvasGeometry& geometry,
int current_map,
float tile_size,
ImDrawList* draw_list,
ImGuiMouseButton mouse_button) {
RectSelectionEvent event;
event.current_map = current_map;
if (!IsMouseInCanvas(geometry)) {
return event;
}
const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry);
const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x;
constexpr int kSmallMapSize = 0x200; // 512 pixels
// Calculate super X/Y accounting for world offset
int super_y = 0;
int super_x = 0;
if (current_map < 0x40) {
// Light World (0x00-0x3F)
super_y = current_map / 8;
super_x = current_map % 8;
} else if (current_map < 0x80) {
// Dark World (0x40-0x7F)
super_y = (current_map - 0x40) / 8;
super_x = (current_map - 0x40) % 8;
} else {
// Special World (0x80+)
super_y = (current_map - 0x80) / 8;
super_x = (current_map - 0x80) % 8;
}
// Handle mouse button press - start selection
if (ImGui::IsMouseClicked(mouse_button)) {
g_select_rect_state.drag_start_pos = AlignToGrid(mouse_pos, scaled_size);
g_select_rect_state.is_dragging = false;
// Single tile selection on click
ImVec2 painter_pos = AlignToGrid(mouse_pos, scaled_size);
int painter_x = static_cast<int>(painter_pos.x);
int painter_y = static_cast<int>(painter_pos.y);
auto tile16_x = (painter_x % kSmallMapSize) / (kSmallMapSize / 0x20);
auto tile16_y = (painter_y % kSmallMapSize) / (kSmallMapSize / 0x20);
int index_x = super_x * 0x20 + tile16_x;
int index_y = super_y * 0x20 + tile16_y;
event.start_pos = painter_pos;
event.selected_tiles.push_back(ImVec2(static_cast<float>(index_x),
static_cast<float>(index_y)));
}
// Handle dragging - draw preview rectangle
ImVec2 drag_end_pos = AlignToGrid(mouse_pos, scaled_size);
if (ImGui::IsMouseDragging(mouse_button) && draw_list) {
g_select_rect_state.is_dragging = true;
event.is_active = true;
event.start_pos = g_select_rect_state.drag_start_pos;
event.end_pos = drag_end_pos;
// Draw preview rectangle
auto start = ImVec2(geometry.canvas_p0.x + g_select_rect_state.drag_start_pos.x,
geometry.canvas_p0.y + g_select_rect_state.drag_start_pos.y);
auto end = ImVec2(geometry.canvas_p0.x + drag_end_pos.x + tile_size,
geometry.canvas_p0.y + drag_end_pos.y + tile_size);
draw_list->AddRect(start, end, IM_COL32(255, 255, 255, 255));
}
// Handle mouse release - complete selection
if (g_select_rect_state.is_dragging && !ImGui::IsMouseDown(mouse_button)) {
g_select_rect_state.is_dragging = false;
event.is_complete = true;
event.is_active = false;
event.start_pos = g_select_rect_state.drag_start_pos;
event.end_pos = drag_end_pos;
// Calculate selected tiles
constexpr int kTile16Size = 16;
int start_x = static_cast<int>(std::floor(g_select_rect_state.drag_start_pos.x / scaled_size)) * kTile16Size;
int start_y = static_cast<int>(std::floor(g_select_rect_state.drag_start_pos.y / scaled_size)) * kTile16Size;
int end_x = static_cast<int>(std::floor(drag_end_pos.x / scaled_size)) * kTile16Size;
int end_y = static_cast<int>(std::floor(drag_end_pos.y / scaled_size)) * kTile16Size;
if (start_x > end_x) std::swap(start_x, end_x);
if (start_y > end_y) std::swap(start_y, end_y);
constexpr int kTilesPerLocalMap = kSmallMapSize / 16;
for (int tile_y = start_y; tile_y <= end_y; tile_y += kTile16Size) {
for (int tile_x = start_x; tile_x <= end_x; tile_x += kTile16Size) {
int local_map_x = tile_x / kSmallMapSize;
int local_map_y = tile_y / kSmallMapSize;
int tile16_x = (tile_x % kSmallMapSize) / kTile16Size;
int tile16_y = (tile_y % kSmallMapSize) / kTile16Size;
int index_x = local_map_x * kTilesPerLocalMap + tile16_x;
int index_y = local_map_y * kTilesPerLocalMap + tile16_y;
event.selected_tiles.emplace_back(static_cast<float>(index_x),
static_cast<float>(index_y));
}
}
}
return event;
}
TileSelectionEvent HandleTileSelection(
const CanvasGeometry& geometry,
int current_map,
float tile_size,
ImGuiMouseButton mouse_button) {
TileSelectionEvent event;
if (!IsMouseInCanvas(geometry) || !ImGui::IsMouseClicked(mouse_button)) {
return event;
}
const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry);
const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x;
constexpr int kSmallMapSize = 0x200;
// Calculate super X/Y
int super_y = 0;
int super_x = 0;
if (current_map < 0x40) {
super_y = current_map / 8;
super_x = current_map % 8;
} else if (current_map < 0x80) {
super_y = (current_map - 0x40) / 8;
super_x = (current_map - 0x40) % 8;
} else {
super_y = (current_map - 0x80) / 8;
super_x = (current_map - 0x80) % 8;
}
ImVec2 painter_pos = AlignToGrid(mouse_pos, scaled_size);
int painter_x = static_cast<int>(painter_pos.x);
int painter_y = static_cast<int>(painter_pos.y);
auto tile16_x = (painter_x % kSmallMapSize) / (kSmallMapSize / 0x20);
auto tile16_y = (painter_y % kSmallMapSize) / (kSmallMapSize / 0x20);
int index_x = super_x * 0x20 + tile16_x;
int index_y = super_y * 0x20 + tile16_y;
event.tile_position = ImVec2(static_cast<float>(index_x),
static_cast<float>(index_y));
event.is_valid = true;
return event;
}
// ============================================================================
// Tile Painting Implementation
// ============================================================================
TilePaintEvent HandleTilePaint(
const CanvasGeometry& geometry,
int tile_id,
float tile_size,
ImGuiMouseButton mouse_button) {
TilePaintEvent event;
event.tile_id = tile_id;
if (!IsMouseInCanvas(geometry)) {
return event;
}
const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry);
const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x;
ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size);
event.position = mouse_pos;
event.grid_position = paint_pos;
// Check for paint action
if (ImGui::IsMouseClicked(mouse_button)) {
event.is_complete = true;
event.is_drag = false;
} else if (ImGui::IsMouseDragging(mouse_button)) {
event.is_complete = true;
event.is_drag = true;
}
return event;
}
TilePaintEvent HandleTilePaintWithPreview(
const CanvasGeometry& geometry,
const gfx::Bitmap& bitmap,
float tile_size,
ImDrawList* draw_list,
ImGuiMouseButton mouse_button) {
TilePaintEvent event;
if (!IsMouseInCanvas(geometry)) {
return event;
}
const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry);
const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x;
// Calculate grid-aligned paint position
ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size);
event.position = mouse_pos;
event.grid_position = paint_pos;
auto paint_pos_end = ImVec2(paint_pos.x + scaled_size, paint_pos.y + scaled_size);
// Draw preview of tile at hover position
if (bitmap.is_active() && draw_list) {
const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x,
geometry.canvas_p0.y + geometry.scrolling.y);
draw_list->AddImage(
reinterpret_cast<ImTextureID>(bitmap.texture()),
ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y),
ImVec2(origin.x + paint_pos_end.x, origin.y + paint_pos_end.y));
}
// Check for paint action
if (ImGui::IsMouseClicked(mouse_button) &&
ImGui::IsMouseDragging(mouse_button)) {
event.is_complete = true;
event.is_drag = true;
}
return event;
}
TilePaintEvent HandleTilemapPaint(
const CanvasGeometry& geometry,
const gfx::Tilemap& tilemap,
int current_tile,
ImDrawList* draw_list,
ImGuiMouseButton mouse_button) {
TilePaintEvent event;
event.tile_id = current_tile;
if (!IsMouseInCanvas(geometry)) {
return event;
}
const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry);
const float scaled_size = 16.0f * geometry.scaled_size.x / geometry.canvas_sz.x;
ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size);
event.position = mouse_pos;
event.grid_position = paint_pos;
// Draw preview if tilemap has texture
if (tilemap.atlas.is_active() && draw_list) {
const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x,
geometry.canvas_p0.y + geometry.scrolling.y);
// TODO(scawful): Render tilemap preview
(void)origin; // Suppress unused warning
}
// Check for paint action
if (ImGui::IsMouseDown(mouse_button)) {
event.is_complete = true;
event.is_drag = ImGui::IsMouseDragging(mouse_button);
}
return event;
}
// ============================================================================
// Hover and Preview Implementation
// ============================================================================
HoverEvent HandleHover(const CanvasGeometry& geometry, float tile_size) {
HoverEvent event;
if (!IsMouseInCanvas(geometry)) {
return event;
}
const ImVec2 mouse_pos = GetMouseInCanvasSpace(geometry);
const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x;
event.position = mouse_pos;
event.grid_position = AlignToGrid(mouse_pos, scaled_size);
event.is_valid = true;
return event;
}
void RenderHoverPreview(
const CanvasGeometry& geometry,
const HoverEvent& hover,
float tile_size,
ImDrawList* draw_list,
ImU32 color) {
if (!hover.is_valid || !draw_list) {
return;
}
const float scaled_size = tile_size * geometry.scaled_size.x / geometry.canvas_sz.x;
const ImVec2 origin(geometry.canvas_p0.x + geometry.scrolling.x,
geometry.canvas_p0.y + geometry.scrolling.y);
ImVec2 preview_start = ImVec2(origin.x + hover.grid_position.x,
origin.y + hover.grid_position.y);
ImVec2 preview_end = ImVec2(preview_start.x + scaled_size,
preview_start.y + scaled_size);
draw_list->AddRectFilled(preview_start, preview_end, color);
}
// ============================================================================
// Entity Interaction Implementation (Stub for Phase 2.4)
// ============================================================================
EntityInteractionEvent HandleEntityInteraction(
const CanvasGeometry& geometry,
int entity_id,
ImVec2 entity_position) {
EntityInteractionEvent event;
event.entity_id = entity_id;
event.position = entity_position;
if (!IsMouseInCanvas(geometry)) {
return event;
}
// TODO(scawful): Implement entity interaction logic in Phase 2.4
// For now, just return empty event
return event;
}
} // namespace gui
} // namespace yaze

View File

@@ -0,0 +1,228 @@
#ifndef YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_H
#define YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_H
#include "app/gui/canvas/canvas_events.h"
#include "app/gui/canvas/canvas_state.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/render/tilemap.h"
#include "imgui/imgui.h"
namespace yaze {
namespace gui {
/**
* @file canvas_interaction.h
* @brief Free functions for canvas interaction handling
*
* Phase 2 of Canvas refactoring: Extract interaction logic into event-driven
* free functions. These functions replace the stateful CanvasInteractionHandler
* methods with pure functions that return event payloads.
*
* Design Pattern:
* - Input: Canvas geometry, mouse state, interaction parameters
* - Output: Event payload struct (TilePaintEvent, RectSelectionEvent, etc.)
* - No hidden state, fully testable
* - Editors consume events and respond accordingly
*/
// ============================================================================
// Rectangle Selection (Phase 2.1)
// ============================================================================
/**
* @brief Handle rectangle selection interaction
*
* Processes right-click drag to select multiple tiles in a rectangular region.
* Returns event when selection completes.
*
* @param geometry Canvas geometry (position, size, scale)
* @param current_map Current map ID for coordinate calculation
* @param tile_size Logical tile size (before scaling)
* @param draw_list ImGui draw list for preview rendering
* @param mouse_button Mouse button for selection (default: right)
* @return RectSelectionEvent with selection results
*/
RectSelectionEvent HandleRectangleSelection(
const CanvasGeometry& geometry,
int current_map,
float tile_size,
ImDrawList* draw_list,
ImGuiMouseButton mouse_button = ImGuiMouseButton_Right);
/**
* @brief Handle single tile selection (right-click)
*
* Processes single right-click to select one tile.
*
* @param geometry Canvas geometry
* @param current_map Current map ID
* @param tile_size Logical tile size
* @param mouse_button Mouse button for selection
* @return TileSelectionEvent with selected tile
*/
TileSelectionEvent HandleTileSelection(
const CanvasGeometry& geometry,
int current_map,
float tile_size,
ImGuiMouseButton mouse_button = ImGuiMouseButton_Right);
// ============================================================================
// Tile Painting (Phase 2.2)
// ============================================================================
/**
* @brief Handle tile painting interaction
*
* Processes left-click/drag to paint tiles on tilemap.
* Returns event when paint action occurs.
*
* @param geometry Canvas geometry
* @param tile_id Current tile ID to paint
* @param tile_size Logical tile size
* @param mouse_button Mouse button for painting
* @return TilePaintEvent with paint results
*/
TilePaintEvent HandleTilePaint(
const CanvasGeometry& geometry,
int tile_id,
float tile_size,
ImGuiMouseButton mouse_button = ImGuiMouseButton_Left);
/**
* @brief Handle tile painter with bitmap preview
*
* Renders preview of tile at hover position and handles paint interaction.
*
* @param geometry Canvas geometry
* @param bitmap Tile bitmap to paint
* @param tile_size Logical tile size
* @param draw_list ImGui draw list for preview
* @param mouse_button Mouse button for painting
* @return TilePaintEvent with paint results
*/
TilePaintEvent HandleTilePaintWithPreview(
const CanvasGeometry& geometry,
const gfx::Bitmap& bitmap,
float tile_size,
ImDrawList* draw_list,
ImGuiMouseButton mouse_button = ImGuiMouseButton_Left);
/**
* @brief Handle tilemap painting interaction
*
* Processes painting with tilemap data (multiple tiles).
*
* @param geometry Canvas geometry
* @param tilemap Tilemap containing tile data
* @param current_tile Current tile index in tilemap
* @param draw_list ImGui draw list for preview
* @param mouse_button Mouse button for painting
* @return TilePaintEvent with paint results
*/
TilePaintEvent HandleTilemapPaint(
const CanvasGeometry& geometry,
const gfx::Tilemap& tilemap,
int current_tile,
ImDrawList* draw_list,
ImGuiMouseButton mouse_button = ImGuiMouseButton_Left);
// ============================================================================
// Hover and Preview (Phase 2.3)
// ============================================================================
/**
* @brief Update hover state for canvas
*
* Calculates hover position and grid-aligned preview position.
*
* @param geometry Canvas geometry
* @param tile_size Logical tile size
* @return HoverEvent with hover state
*/
HoverEvent HandleHover(const CanvasGeometry& geometry, float tile_size);
/**
* @brief Render hover preview overlay
*
* Draws preview rectangle at hover position.
*
* @param geometry Canvas geometry
* @param hover Hover event from HandleHover
* @param tile_size Logical tile size
* @param draw_list ImGui draw list
* @param color Preview color (default: white with alpha)
*/
void RenderHoverPreview(
const CanvasGeometry& geometry,
const HoverEvent& hover,
float tile_size,
ImDrawList* draw_list,
ImU32 color = IM_COL32(255, 255, 255, 80));
// ============================================================================
// Entity Interaction (Phase 2.4 - Future)
// ============================================================================
/**
* @brief Handle entity interaction (hover, click, drag)
*
* Processes entity manipulation events.
*
* @param geometry Canvas geometry
* @param entity_id Entity being interacted with
* @param entity_position Current entity position
* @return EntityInteractionEvent with interaction results
*/
EntityInteractionEvent HandleEntityInteraction(
const CanvasGeometry& geometry,
int entity_id,
ImVec2 entity_position);
// ============================================================================
// Helper Functions
// ============================================================================
/**
* @brief Align position to grid
*
* Snaps canvas position to nearest grid cell.
*
* @param pos Canvas position
* @param grid_step Grid cell size
* @return Grid-aligned position
*/
ImVec2 AlignToGrid(ImVec2 pos, float grid_step);
/**
* @brief Get mouse position in canvas space
*
* Converts screen-space mouse position to canvas-space coordinates.
*
* @param geometry Canvas geometry (includes origin)
* @return Mouse position in canvas space
*/
ImVec2 GetMouseInCanvasSpace(const CanvasGeometry& geometry);
/**
* @brief Check if mouse is in canvas bounds
*
* @param geometry Canvas geometry
* @return True if mouse is within canvas
*/
bool IsMouseInCanvas(const CanvasGeometry& geometry);
/**
* @brief Calculate tile grid indices from canvas position
*
* @param canvas_pos Canvas-space position
* @param tile_size Logical tile size
* @param global_scale Canvas scale factor
* @return Tile grid position (x, y)
*/
ImVec2 CanvasToTileGrid(ImVec2 canvas_pos, float tile_size, float global_scale);
} // namespace gui
} // namespace yaze
#endif // YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_H

View File

@@ -1,7 +1,7 @@
#include "canvas_interaction_handler.h" #include "canvas_interaction_handler.h"
#include <algorithm>
#include <cmath> #include <cmath>
#include "app/gui/canvas/canvas_interaction.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
namespace yaze { namespace yaze {
@@ -9,8 +9,8 @@ namespace gui {
namespace { namespace {
// Helper function to align position to grid // Helper function to align position to grid (local version)
ImVec2 AlignToGrid(ImVec2 pos, float grid_step) { ImVec2 AlignToGridLocal(ImVec2 pos, float grid_step) {
return ImVec2(std::floor(pos.x / grid_step) * grid_step, return ImVec2(std::floor(pos.x / grid_step) * grid_step,
std::floor(pos.y / grid_step) * grid_step); std::floor(pos.y / grid_step) * grid_step);
} }
@@ -73,7 +73,7 @@ bool CanvasInteractionHandler::DrawTilePainter(
hover_points_.clear(); hover_points_.clear();
// Calculate grid-aligned paint position // Calculate grid-aligned paint position
ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size); ImVec2 paint_pos = AlignToGridLocal(mouse_pos, scaled_size);
mouse_pos_in_canvas_ = paint_pos; mouse_pos_in_canvas_ = paint_pos;
auto paint_pos_end = ImVec2(paint_pos.x + scaled_size, paint_pos.y + scaled_size); auto paint_pos_end = ImVec2(paint_pos.x + scaled_size, paint_pos.y + scaled_size);
@@ -83,7 +83,7 @@ bool CanvasInteractionHandler::DrawTilePainter(
// Draw preview of tile at hover position // Draw preview of tile at hover position
if (bitmap.is_active() && draw_list) { if (bitmap.is_active() && draw_list) {
draw_list->AddImage( draw_list->AddImage(
(ImTextureID)(intptr_t)bitmap.texture(), reinterpret_cast<ImTextureID>(bitmap.texture()),
ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y), ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y),
ImVec2(origin.x + paint_pos.x + scaled_size, origin.y + paint_pos.y + scaled_size)); ImVec2(origin.x + paint_pos.x + scaled_size, origin.y + paint_pos.y + scaled_size));
} }
@@ -142,7 +142,7 @@ bool CanvasInteractionHandler::DrawTilemapPainter(
static_cast<float>(tile_y + tilemap.tile_size.y) / tilemap.atlas.height()); static_cast<float>(tile_y + tilemap.tile_size.y) / tilemap.atlas.height());
draw_list->AddImage( draw_list->AddImage(
(ImTextureID)(intptr_t)tilemap.atlas.texture(), reinterpret_cast<ImTextureID>(tilemap.atlas.texture()),
ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y), ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y),
ImVec2(origin.x + paint_pos.x + scaled_size, origin.y + paint_pos.y + scaled_size), ImVec2(origin.x + paint_pos.x + scaled_size, origin.y + paint_pos.y + scaled_size),
uv0, uv1); uv0, uv1);
@@ -218,7 +218,7 @@ bool CanvasInteractionHandler::DrawTileSelector(
if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
hover_points_.clear(); hover_points_.clear();
ImVec2 painter_pos = AlignToGrid(mouse_pos, tile_size); ImVec2 painter_pos = AlignToGridLocal(mouse_pos, tile_size);
hover_points_.push_back(painter_pos); hover_points_.push_back(painter_pos);
hover_points_.push_back(ImVec2(painter_pos.x + tile_size, painter_pos.y + tile_size)); hover_points_.push_back(ImVec2(painter_pos.x + tile_size, painter_pos.y + tile_size));
@@ -236,109 +236,47 @@ bool CanvasInteractionHandler::DrawSelectRect(
int current_map, ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, int current_map, ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling,
float global_scale, float tile_size, bool is_hovered) { float global_scale, float tile_size, bool is_hovered) {
const ImGuiIO& imgui_io = ImGui::GetIO();
const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y);
const ImVec2 mouse_pos(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y);
const float scaled_size = tile_size * global_scale;
static ImVec2 drag_start_pos;
static bool dragging = false;
constexpr int small_map_size = 0x200;
if (!is_hovered) { if (!is_hovered) {
return false; return false;
} }
// Calculate superX and superY accounting for world offset // Create CanvasGeometry from parameters
int super_y = 0; CanvasGeometry geometry;
int super_x = 0; geometry.canvas_p0 = canvas_p0;
if (current_map < 0x40) { geometry.scrolling = scrolling;
super_y = current_map / 8; geometry.scaled_size = ImVec2(tile_size * global_scale, tile_size * global_scale);
super_x = current_map % 8; geometry.canvas_sz = ImVec2(tile_size, tile_size); // Will be updated if needed
} else if (current_map < 0x80) {
super_y = (current_map - 0x40) / 8; // Call new event-based function
super_x = (current_map - 0x40) % 8; RectSelectionEvent event = HandleRectangleSelection(
} else { geometry, current_map, tile_size, draw_list, ImGuiMouseButton_Right);
super_y = (current_map - 0x80) / 8;
super_x = (current_map - 0x80) % 8; // Update internal state for backward compatibility
} if (event.is_complete) {
selected_tiles_ = event.selected_tiles;
// Handle right click for single tile selection
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
ImVec2 painter_pos = AlignToGrid(mouse_pos, scaled_size);
int painter_x = painter_pos.x;
int painter_y = painter_pos.y;
auto tile16_x = (painter_x % small_map_size) / (small_map_size / 0x20);
auto tile16_y = (painter_y % small_map_size) / (small_map_size / 0x20);
int index_x = super_x * 0x20 + tile16_x;
int index_y = super_y * 0x20 + tile16_y;
selected_tile_pos_ = ImVec2(index_x, index_y);
selected_points_.clear(); selected_points_.clear();
rect_select_active_ = false; selected_points_.push_back(event.start_pos);
selected_points_.push_back(event.end_pos);
drag_start_pos = AlignToGrid(mouse_pos, scaled_size);
}
// Draw rectangle while dragging
ImVec2 drag_end_pos = AlignToGrid(mouse_pos, scaled_size);
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right) && draw_list) {
auto start = ImVec2(canvas_p0.x + drag_start_pos.x,
canvas_p0.y + drag_start_pos.y);
auto end = ImVec2(canvas_p0.x + drag_end_pos.x + tile_size,
canvas_p0.y + drag_end_pos.y + tile_size);
draw_list->AddRect(start, end, IM_COL32(255, 255, 255, 255));
dragging = true;
}
// Complete selection on release
if (dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
dragging = false;
constexpr int tile16_size = 16;
int start_x = std::floor(drag_start_pos.x / scaled_size) * tile16_size;
int start_y = std::floor(drag_start_pos.y / scaled_size) * tile16_size;
int end_x = std::floor(drag_end_pos.x / scaled_size) * tile16_size;
int end_y = std::floor(drag_end_pos.y / scaled_size) * tile16_size;
if (start_x > end_x) std::swap(start_x, end_x);
if (start_y > end_y) std::swap(start_y, end_y);
selected_tiles_.clear();
selected_tiles_.reserve(((end_x - start_x) / tile16_size + 1) *
((end_y - start_y) / tile16_size + 1));
constexpr int tiles_per_local_map = small_map_size / 16;
for (int tile_y = start_y; tile_y <= end_y; tile_y += tile16_size) {
for (int tile_x = start_x; tile_x <= end_x; tile_x += tile16_size) {
int local_map_x = tile_x / small_map_size;
int local_map_y = tile_y / small_map_size;
int tile16_x = (tile_x % small_map_size) / tile16_size;
int tile16_y = (tile_y % small_map_size) / tile16_size;
int index_x = local_map_x * tiles_per_local_map + tile16_x;
int index_y = local_map_y * tiles_per_local_map + tile16_y;
selected_tiles_.emplace_back(index_x, index_y);
}
}
selected_points_.clear();
selected_points_.push_back(drag_start_pos);
selected_points_.push_back(drag_end_pos);
rect_select_active_ = true; rect_select_active_ = true;
return true; return true;
} }
if (!event.selected_tiles.empty() && !event.is_complete) {
// Single tile selection
selected_tile_pos_ = event.selected_tiles[0];
selected_points_.clear();
rect_select_active_ = false;
}
rect_select_active_ = event.is_active;
return false; return false;
} }
// Helper methods - these are thin wrappers that could be static but kept as instance // Helper methods - these are thin wrappers that could be static but kept as instance
// methods for potential future state access // methods for potential future state access
ImVec2 CanvasInteractionHandler::AlignPosToGrid(ImVec2 pos, float grid_step) { ImVec2 CanvasInteractionHandler::AlignPosToGrid(ImVec2 pos, float grid_step) {
return AlignToGrid(pos, grid_step); return AlignToGridLocal(pos, grid_step);
} }
ImVec2 CanvasInteractionHandler::GetMousePosition(ImVec2 canvas_p0, ImVec2 scrolling) { ImVec2 CanvasInteractionHandler::GetMousePosition(ImVec2 canvas_p0, ImVec2 scrolling) {

View File

@@ -30,6 +30,7 @@ set(CANVAS_SRC
app/gui/canvas/canvas_automation_api.cc app/gui/canvas/canvas_automation_api.cc
app/gui/canvas/canvas_context_menu.cc app/gui/canvas/canvas_context_menu.cc
app/gui/canvas/canvas_geometry.cc app/gui/canvas/canvas_geometry.cc
app/gui/canvas/canvas_interaction.cc
app/gui/canvas/canvas_interaction_handler.cc app/gui/canvas/canvas_interaction_handler.cc
app/gui/canvas/canvas_modals.cc app/gui/canvas/canvas_modals.cc
app/gui/canvas/canvas_performance_integration.cc app/gui/canvas/canvas_performance_integration.cc