Add comprehensive Canvas guide and refactor documentation
- Introduced a new `CANVAS_GUIDE.md` file detailing the Canvas system, including core concepts, usage patterns, and features such as tile painting, selection, and custom overlays. - Created `CANVAS_REFACTORING_STATUS.md` to summarize the current state of refactoring efforts, including completed tasks and outstanding issues. - Enhanced `overworld_editor` functionality by implementing critical fixes for rectangle selection and painting, ensuring proper handling of large map boundaries. - Updated `canvas_utils.h` to include configuration options for rectangle clamping, preventing wrapping issues during tile selection. - Refactored `canvas.cc` and `canvas.h` to improve method signatures and documentation, facilitating better understanding and usage of the Canvas API. - Improved overall documentation structure for clarity and ease of access, consolidating multiple files into focused references.
This commit is contained in:
@@ -946,9 +946,13 @@ void OverworldEditor::CheckForOverworldEdits() {
|
||||
"current_tile16_=%d",
|
||||
current_tile16_);
|
||||
|
||||
// Apply the current selected tile to each position in the rectangle
|
||||
for (int y = start_y, i = 0; y <= end_y; y += kTile16Size) {
|
||||
for (int x = start_x; x <= end_x; x += kTile16Size, ++i) {
|
||||
// Apply the selected tiles to each position in the rectangle
|
||||
// CRITICAL FIX: Use pre-computed tile16_ids_ instead of recalculating from selected_tiles_
|
||||
// This prevents wrapping issues when dragging near boundaries
|
||||
int i = 0;
|
||||
for (int y = start_y; y <= end_y && i < static_cast<int>(selected_tile16_ids_.size()); y += kTile16Size) {
|
||||
for (int x = start_x; x <= end_x && i < static_cast<int>(selected_tile16_ids_.size()); x += kTile16Size, ++i) {
|
||||
|
||||
// Determine which local map (512x512) the tile is in
|
||||
int local_map_x = x / local_map_size;
|
||||
int local_map_y = y / local_map_size;
|
||||
@@ -961,13 +965,25 @@ void OverworldEditor::CheckForOverworldEdits() {
|
||||
int index_x = local_map_x * tiles_per_local_map + tile16_x;
|
||||
int index_y = local_map_y * tiles_per_local_map + tile16_y;
|
||||
|
||||
overworld_.set_current_world(current_world_);
|
||||
overworld_.set_current_map(current_map_);
|
||||
int tile16_id = overworld_.GetTileFromPosition(
|
||||
ow_map_canvas_.selected_tiles()[i]);
|
||||
// Bounds check for the selected world array
|
||||
if (index_x >= 0 && index_x < 0x200 && index_y >= 0 &&
|
||||
index_y < 0x200) {
|
||||
// FIXED: Use pre-computed tile ID from the ORIGINAL selection
|
||||
int tile16_id = selected_tile16_ids_[i];
|
||||
// Bounds check for the selected world array, accounting for rectangle size
|
||||
// Ensure the entire rectangle fits within the world bounds
|
||||
int rect_width = ((end_x - start_x) / kTile16Size) + 1;
|
||||
int rect_height = ((end_y - start_y) / kTile16Size) + 1;
|
||||
|
||||
// Prevent painting from wrapping around at the edges of large maps
|
||||
// Only allow painting if the entire rectangle is within the same 512x512 local map
|
||||
int start_local_map_x = start_x / local_map_size;
|
||||
int start_local_map_y = start_y / local_map_size;
|
||||
int end_local_map_x = end_x / local_map_size;
|
||||
int end_local_map_y = end_y / local_map_size;
|
||||
|
||||
bool in_same_local_map = (start_local_map_x == end_local_map_x) && (start_local_map_y == end_local_map_y);
|
||||
|
||||
if (in_same_local_map &&
|
||||
index_x >= 0 && (index_x + rect_width - 1) < 0x200 &&
|
||||
index_y >= 0 && (index_y + rect_height - 1) < 0x200) {
|
||||
selected_world[index_x][index_y] = tile16_id;
|
||||
|
||||
// CRITICAL FIX: Also update the bitmap directly like single tile drawing
|
||||
@@ -989,6 +1005,7 @@ void OverworldEditor::CheckForOverworldEdits() {
|
||||
|
||||
RefreshOverworldMap();
|
||||
// Clear the rectangle selection after applying
|
||||
// This is commented out for now, will come back to later.
|
||||
// ow_map_canvas_.mutable_selected_tiles()->clear();
|
||||
// ow_map_canvas_.mutable_points()->clear();
|
||||
util::logf(
|
||||
@@ -1005,29 +1022,27 @@ void OverworldEditor::CheckForSelectRectangle() {
|
||||
current_tile16_ =
|
||||
overworld_.GetTileFromPosition(ow_map_canvas_.selected_tile_pos());
|
||||
ow_map_canvas_.set_selected_tile_pos(ImVec2(-1, -1));
|
||||
|
||||
|
||||
// Scroll blockset canvas to show the selected tile
|
||||
ScrollBlocksetCanvasToCurrentTile();
|
||||
}
|
||||
|
||||
static std::vector<int> tile16_ids;
|
||||
// Rectangle selection case - use member variable instead of static local
|
||||
if (ow_map_canvas_.select_rect_active()) {
|
||||
// Get the tile16 IDs from the selected tile ID positions
|
||||
if (tile16_ids.size() != 0) {
|
||||
tile16_ids.clear();
|
||||
}
|
||||
selected_tile16_ids_.clear();
|
||||
|
||||
if (ow_map_canvas_.selected_tiles().size() > 0) {
|
||||
// Set the current world and map in overworld for proper tile lookup
|
||||
overworld_.set_current_world(current_world_);
|
||||
overworld_.set_current_map(current_map_);
|
||||
for (auto& each : ow_map_canvas_.selected_tiles()) {
|
||||
tile16_ids.push_back(overworld_.GetTileFromPosition(each));
|
||||
selected_tile16_ids_.push_back(overworld_.GetTileFromPosition(each));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create a composite image of all the tile16s selected
|
||||
ow_map_canvas_.DrawBitmapGroup(tile16_ids, tile16_blockset_, 0x10,
|
||||
ow_map_canvas_.DrawBitmapGroup(selected_tile16_ids_, tile16_blockset_, 0x10,
|
||||
ow_map_canvas_.global_scale());
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,9 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
|
||||
* @brief Draw and create the tile16 IDs that are currently selected.
|
||||
*/
|
||||
void CheckForSelectRectangle();
|
||||
|
||||
// Selected tile IDs for rectangle operations (moved from local static)
|
||||
std::vector<int> selected_tile16_ids_;
|
||||
|
||||
/**
|
||||
* @brief Check for changes to the overworld map. Calls RefreshOverworldMap
|
||||
|
||||
@@ -35,10 +35,8 @@ using ImGui::OpenPopupOnItemClick;
|
||||
using ImGui::Selectable;
|
||||
using ImGui::Text;
|
||||
|
||||
constexpr uint32_t kBlackColor = IM_COL32(0, 0, 0, 255);
|
||||
constexpr uint32_t kRectangleColor = IM_COL32(32, 32, 32, 255);
|
||||
constexpr uint32_t kWhiteColor = IM_COL32(255, 255, 255, 255);
|
||||
constexpr uint32_t kOutlineRect = IM_COL32(255, 255, 255, 200);
|
||||
|
||||
constexpr ImGuiButtonFlags kMouseFlags =
|
||||
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight;
|
||||
@@ -1153,7 +1151,9 @@ void Canvas::DrawOutlineWithColor(int x, int y, int w, int h, uint32_t color) {
|
||||
}
|
||||
|
||||
void Canvas::DrawBitmapGroup(std::vector<int> &group, gfx::Tilemap &tilemap,
|
||||
int tile_size, float scale) {
|
||||
int tile_size, float scale,
|
||||
int local_map_size,
|
||||
ImVec2 total_map_size) {
|
||||
if (selected_points_.size() != 2) {
|
||||
// points_ should contain exactly two points
|
||||
return;
|
||||
@@ -1166,6 +1166,11 @@ void Canvas::DrawBitmapGroup(std::vector<int> &group, gfx::Tilemap &tilemap,
|
||||
// OPTIMIZATION: Use optimized rendering for large groups to improve performance
|
||||
bool use_optimized_rendering = group.size() > 128; // Optimize for large selections
|
||||
|
||||
// Use provided map sizes for proper boundary handling
|
||||
const int small_map = local_map_size;
|
||||
const float large_map_width = total_map_size.x;
|
||||
const float large_map_height = total_map_size.y;
|
||||
|
||||
// Pre-calculate common values to avoid repeated computation
|
||||
const float tile_scale = tile_size * scale;
|
||||
const int atlas_tiles_per_row = tilemap.atlas.width() / tilemap.tile_size.x;
|
||||
@@ -1256,10 +1261,49 @@ void Canvas::DrawBitmapGroup(std::vector<int> &group, gfx::Tilemap &tilemap,
|
||||
|
||||
// Performance optimization completed - tiles are now rendered with pre-calculated values
|
||||
|
||||
// Reposition rectangle to follow mouse, but clamp to prevent wrapping across map boundaries
|
||||
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);
|
||||
auto new_start_pos = AlignPosToGrid(mouse_pos, tile_size * scale);
|
||||
|
||||
// CRITICAL FIX: Clamp BEFORE grid alignment for smoother dragging behavior
|
||||
// This prevents the rectangle from even attempting to cross boundaries during drag
|
||||
ImVec2 clamped_mouse_pos = mouse_pos;
|
||||
|
||||
if (config_.clamp_rect_to_local_maps) {
|
||||
// Calculate which local map the mouse is in
|
||||
int mouse_local_map_x = static_cast<int>(mouse_pos.x) / small_map;
|
||||
int mouse_local_map_y = static_cast<int>(mouse_pos.y) / small_map;
|
||||
|
||||
// Calculate where the rectangle END would be if we place it at mouse position
|
||||
float potential_end_x = mouse_pos.x + rect_width;
|
||||
float potential_end_y = mouse_pos.y + rect_height;
|
||||
|
||||
// Check if this would cross local map boundary (512x512 blocks)
|
||||
int potential_end_map_x = static_cast<int>(potential_end_x) / small_map;
|
||||
int potential_end_map_y = static_cast<int>(potential_end_y) / small_map;
|
||||
|
||||
// Clamp mouse position to prevent crossing during drag
|
||||
if (potential_end_map_x != mouse_local_map_x) {
|
||||
// Would cross horizontal boundary - clamp mouse to safe zone
|
||||
float max_mouse_x = (mouse_local_map_x + 1) * small_map - rect_width;
|
||||
clamped_mouse_pos.x = std::min(mouse_pos.x, max_mouse_x);
|
||||
}
|
||||
|
||||
if (potential_end_map_y != mouse_local_map_y) {
|
||||
// Would cross vertical boundary - clamp mouse to safe zone
|
||||
float max_mouse_y = (mouse_local_map_y + 1) * small_map - rect_height;
|
||||
clamped_mouse_pos.y = std::min(mouse_pos.y, max_mouse_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Now grid-align the clamped position
|
||||
auto new_start_pos = AlignPosToGrid(clamped_mouse_pos, tile_size * scale);
|
||||
|
||||
// Additional safety: clamp to overall map bounds
|
||||
new_start_pos.x = std::clamp(new_start_pos.x, 0.0f, large_map_width - rect_width);
|
||||
new_start_pos.y = std::clamp(new_start_pos.y, 0.0f, large_map_height - rect_height);
|
||||
|
||||
selected_points_.clear();
|
||||
selected_points_.push_back(new_start_pos);
|
||||
selected_points_.push_back(
|
||||
|
||||
@@ -154,6 +154,30 @@ class Canvas {
|
||||
std::function<void()> callback;
|
||||
std::function<bool()> enabled_condition = []() { return true; };
|
||||
std::vector<ContextMenuItem> subitems;
|
||||
|
||||
// Helper constructor for simple items
|
||||
ContextMenuItem() = default;
|
||||
ContextMenuItem(const std::string& lbl, std::function<void()> cb,
|
||||
const std::string& sc = "")
|
||||
: label(lbl), shortcut(sc), callback(std::move(cb)) {}
|
||||
|
||||
// Helper to create disabled item
|
||||
static ContextMenuItem Disabled(const std::string& lbl) {
|
||||
ContextMenuItem item;
|
||||
item.label = lbl;
|
||||
item.enabled_condition = []() { return false; };
|
||||
return item;
|
||||
}
|
||||
|
||||
// Helper to create conditional item
|
||||
static ContextMenuItem Conditional(const std::string& lbl, std::function<void()> cb,
|
||||
std::function<bool()> condition) {
|
||||
ContextMenuItem item;
|
||||
item.label = lbl;
|
||||
item.callback = std::move(cb);
|
||||
item.enabled_condition = std::move(condition);
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
// BPP format UI components
|
||||
@@ -311,6 +335,10 @@ class Canvas {
|
||||
void SetGlobalScale(float scale) { config_.global_scale = scale; }
|
||||
bool* GetCustomLabelsEnabled() { return &config_.enable_custom_labels; }
|
||||
float GetGridStep() const { return config_.grid_step; }
|
||||
|
||||
// Rectangle selection boundary control (prevents wrapping in large maps)
|
||||
void SetClampRectToLocalMaps(bool clamp) { config_.clamp_rect_to_local_maps = clamp; }
|
||||
bool GetClampRectToLocalMaps() const { return config_.clamp_rect_to_local_maps; }
|
||||
float GetCanvasWidth() const { return config_.canvas_size.x; }
|
||||
float GetCanvasHeight() const { return config_.canvas_size.y; }
|
||||
|
||||
@@ -333,7 +361,19 @@ class Canvas {
|
||||
void DrawBitmap(Bitmap &bitmap, int x_offset, int y_offset, float scale = 1.0f, int alpha = 255);
|
||||
void DrawBitmap(Bitmap &bitmap, ImVec2 dest_pos, ImVec2 dest_size, ImVec2 src_pos, ImVec2 src_size);
|
||||
void DrawBitmapTable(const BitmapTable &gfx_bin);
|
||||
void DrawBitmapGroup(std::vector<int> &group, gfx::Tilemap &tilemap, int tile_size, float scale = 1.0f);
|
||||
/**
|
||||
* @brief Draw group of bitmaps for multi-tile selection preview
|
||||
* @param group Vector of tile IDs to draw
|
||||
* @param tilemap Tilemap containing the tiles
|
||||
* @param tile_size Size of each tile (default 16)
|
||||
* @param scale Rendering scale (default 1.0)
|
||||
* @param local_map_size Size of local map in pixels (default 512 for standard maps)
|
||||
* @param total_map_size Total map size for boundary clamping (default 4096x4096)
|
||||
*/
|
||||
void DrawBitmapGroup(std::vector<int> &group, gfx::Tilemap &tilemap,
|
||||
int tile_size, float scale = 1.0f,
|
||||
int local_map_size = 0x200,
|
||||
ImVec2 total_map_size = ImVec2(0x1000, 0x1000));
|
||||
bool DrawTilemapPainter(gfx::Tilemap &tilemap, int current_tile);
|
||||
void DrawSelectRect(int current_map, int tile_size = 0x10, float scale = 1.0f);
|
||||
bool DrawTileSelector(int size, int size_y = 0);
|
||||
|
||||
@@ -20,6 +20,7 @@ struct CanvasConfig {
|
||||
bool enable_context_menu = true;
|
||||
bool is_draggable = false;
|
||||
bool auto_resize = false;
|
||||
bool clamp_rect_to_local_maps = true; // NEW: Prevent rectangle wrap across 512x512 boundaries
|
||||
float grid_step = 32.0f;
|
||||
float global_scale = 1.0f;
|
||||
ImVec2 canvas_size = ImVec2(0, 0);
|
||||
|
||||
Reference in New Issue
Block a user