yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tile_painting_manager.cc
Go to the documentation of this file.
1// Related header
3
4#include <algorithm>
5#include <cmath>
6#include <vector>
7
14#include "core/features.h"
15#include "imgui/imgui.h"
16#include "util/log.h"
18
19namespace yaze::editor {
20
22 const TilePaintingCallbacks& callbacks)
23 : deps_(deps), callbacks_(callbacks) {}
24
25// ---------------------------------------------------------------------------
26// DrawOverworldEdits - single-tile painting on left-click / drag
27// ---------------------------------------------------------------------------
29 // Determine which overworld map the user is currently editing.
30 // drawn_tile_position() returns scaled coordinates, need to unscale
31 auto scaled_position = deps_.ow_map_canvas->drawn_tile_position();
32 float scale = deps_.ow_map_canvas->global_scale();
33 if (scale <= 0.0f)
34 scale = 1.0f;
35
36 // Convert scaled position to world coordinates
37 ImVec2 mouse_position =
38 ImVec2(scaled_position.x / scale, scaled_position.y / scale);
39
40 int map_x = static_cast<int>(mouse_position.x) / kOverworldMapSize;
41 int map_y = static_cast<int>(mouse_position.y) / kOverworldMapSize;
42 *deps_.current_map = map_x + map_y * 8;
43 if (*deps_.current_world == 1) {
44 *deps_.current_map += 0x40;
45 } else if (*deps_.current_world == 2) {
46 *deps_.current_map += 0x80;
47 }
48
49 // Bounds checking to prevent crashes
50 if (*deps_.current_map < 0 ||
51 *deps_.current_map >= static_cast<int>(deps_.maps_bmp->size())) {
52 return; // Invalid map index, skip drawing
53 }
54
55 // Validate tile16_blockset_ before calling GetTilemapData
57 deps_.tile16_blockset->atlas.vector().empty()) {
58 LOG_ERROR("TilePaintingManager",
59 "Error: tile16_blockset is not properly initialized (active: %s, "
60 "size: %zu)",
61 deps_.tile16_blockset->atlas.is_active() ? "true" : "false",
63 return; // Skip drawing if blockset is invalid
64 }
65
66 // Render the updated map bitmap.
67 auto tile_data =
69 RenderUpdatedMapBitmap(mouse_position, tile_data);
70
71 // Calculate the correct superX and superY values
72 const int world_offset = *deps_.current_world * 0x40;
73 const int local_map = *deps_.current_map - world_offset;
74 const int superY = local_map / 8;
75 const int superX = local_map % 8;
76 int mouse_x_i = static_cast<int>(mouse_position.x);
77 int mouse_y_i = static_cast<int>(mouse_position.y);
78 // Calculate the correct tile16_x and tile16_y positions
79 int tile16_x = (mouse_x_i % kOverworldMapSize) / (kOverworldMapSize / 32);
80 int tile16_y = (mouse_y_i % kOverworldMapSize) / (kOverworldMapSize / 32);
81
82 // Update the overworld map_tiles based on tile16 ID and current world
83 auto& selected_world =
84 (*deps_.current_world == 0)
85 ? deps_.overworld->mutable_map_tiles()->light_world
86 : (*deps_.current_world == 1)
87 ? deps_.overworld->mutable_map_tiles()->dark_world
88 : deps_.overworld->mutable_map_tiles()->special_world;
89
90 int index_x = superX * 32 + tile16_x;
91 int index_y = superY * 32 + tile16_y;
92
93 // Get old tile value for undo tracking
94 int old_tile_id = selected_world[index_x][index_y];
95
96 // Only record undo if tile is actually changing
97 if (old_tile_id != *deps_.current_tile16) {
99 index_x, index_y, old_tile_id);
100 deps_.rom->set_dirty(true);
101 }
102
103 selected_world[index_x][index_y] = *deps_.current_tile16;
104}
105
106// ---------------------------------------------------------------------------
107// RenderUpdatedMapBitmap - update bitmap pixels after tile paint
108// ---------------------------------------------------------------------------
110 const ImVec2& click_position, const std::vector<uint8_t>& tile_data) {
111 // Bounds checking to prevent crashes
112 if (*deps_.current_map < 0 ||
113 *deps_.current_map >= static_cast<int>(deps_.maps_bmp->size())) {
114 LOG_ERROR("TilePaintingManager",
115 "ERROR: RenderUpdatedMapBitmap - Invalid current_map %d "
116 "(maps_bmp size=%zu)",
117 *deps_.current_map, deps_.maps_bmp->size());
118 return; // Invalid map index, skip rendering
119 }
120
121 // Calculate the tile index for x and y based on the click_position
122 int tile_index_x =
123 (static_cast<int>(click_position.x) % kOverworldMapSize) / kTile16Size;
124 int tile_index_y =
125 (static_cast<int>(click_position.y) % kOverworldMapSize) / kTile16Size;
126
127 // Calculate the pixel start position based on tile index and tile size
128 ImVec2 start_position;
129 start_position.x = static_cast<float>(tile_index_x * kTile16Size);
130 start_position.y = static_cast<float>(tile_index_y * kTile16Size);
131
132 // Update the bitmap's pixel data based on the start_position and tile_data
133 gfx::Bitmap& current_bitmap = (*deps_.maps_bmp)[*deps_.current_map];
134
135 // Validate bitmap state before writing
136 if (!current_bitmap.is_active() || current_bitmap.size() == 0) {
137 LOG_ERROR(
138 "TilePaintingManager",
139 "ERROR: RenderUpdatedMapBitmap - Bitmap %d is not active or has no "
140 "data (active=%s, size=%zu)",
141 *deps_.current_map, current_bitmap.is_active() ? "true" : "false",
142 current_bitmap.size());
143 return;
144 }
145
146 for (int y = 0; y < kTile16Size; ++y) {
147 for (int x = 0; x < kTile16Size; ++x) {
148 int pixel_index =
149 (start_position.y + y) * kOverworldMapSize + (start_position.x + x);
150
151 // Bounds check for pixel index
152 if (pixel_index < 0 ||
153 pixel_index >= static_cast<int>(current_bitmap.size())) {
154 LOG_ERROR(
155 "TilePaintingManager",
156 "ERROR: RenderUpdatedMapBitmap - pixel_index %d out of bounds "
157 "(bitmap size=%zu)",
158 pixel_index, current_bitmap.size());
159 continue;
160 }
161
162 // Bounds check for tile data
163 int tile_data_index = y * kTile16Size + x;
164 if (tile_data_index < 0 ||
165 tile_data_index >= static_cast<int>(tile_data.size())) {
166 LOG_ERROR(
167 "TilePaintingManager",
168 "ERROR: RenderUpdatedMapBitmap - tile_data_index %d out of bounds "
169 "(tile_data size=%zu)",
170 tile_data_index, tile_data.size());
171 continue;
172 }
173
174 current_bitmap.WriteToPixel(pixel_index, tile_data[tile_data_index]);
175 }
176 }
177
178 current_bitmap.set_modified(true);
179
180 // Immediately update the texture to reflect changes
182 &current_bitmap);
183}
184
185// ---------------------------------------------------------------------------
186// CheckForOverworldEdits - main painting entry point
187// ---------------------------------------------------------------------------
189 LOG_DEBUG("TilePaintingManager", "CheckForOverworldEdits: Frame %d",
190 ImGui::GetFrameCount());
191
193
194 // User has selected a tile they want to draw from the blockset
195 // and clicked on the canvas.
197 *deps_.current_tile16 >= 0 &&
202 }
203
204 // Fill tool: fill the entire 32x32 tile16 screen under the cursor using the
205 // current selection pattern (if any) or the current tile16.
208 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
209 float scale = deps_.ow_map_canvas->global_scale();
210 if (scale <= 0.0f) {
211 scale = 1.0f;
212 }
213
214 const bool allow_special_tail =
216 const auto scaled_position = deps_.ow_map_canvas->hover_mouse_pos();
217 const int map_x =
218 static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
219 const int map_y =
220 static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
221
222 // Bounds guard.
223 if (map_x >= 0 && map_x < 8 && map_y >= 0 && map_y < 8) {
224 // Special world only renders 4 rows unless tail expansion is enabled.
225 if (allow_special_tail || *deps_.current_world != 2 || map_y < 4) {
226 const int local_map = map_x + (map_y * 8);
227 const int target_map = local_map + (*deps_.current_world * 0x40);
228 if (target_map >= 0 && target_map < zelda3::kNumOverworldMaps) {
229 // Build pattern from active rectangle selection (if present).
230 std::vector<int> pattern_ids;
231 int pattern_w = 1;
232 int pattern_h = 1;
233
235 deps_.ow_map_canvas->selected_points().size() >= 2) {
236 const auto start = deps_.ow_map_canvas->selected_points()[0];
237 const auto end = deps_.ow_map_canvas->selected_points()[1];
238
239 const int start_x =
240 static_cast<int>(std::floor(std::min(start.x, end.x) / 16.0f));
241 const int end_x =
242 static_cast<int>(std::floor(std::max(start.x, end.x) / 16.0f));
243 const int start_y =
244 static_cast<int>(std::floor(std::min(start.y, end.y) / 16.0f));
245 const int end_y =
246 static_cast<int>(std::floor(std::max(start.y, end.y) / 16.0f));
247
248 pattern_w = std::max(1, end_x - start_x + 1);
249 pattern_h = std::max(1, end_y - start_y + 1);
250 pattern_ids.reserve(pattern_w * pattern_h);
251
253 deps_.overworld->set_current_map(target_map);
254 for (int y = start_y; y <= end_y; ++y) {
255 for (int x = start_x; x <= end_x; ++x) {
256 pattern_ids.push_back(deps_.overworld->GetTile(x, y));
257 }
258 }
259 } else {
260 pattern_ids = {*deps_.current_tile16};
261 }
262
263 auto& world_tiles =
264 (*deps_.current_world == 0)
265 ? deps_.overworld->mutable_map_tiles()->light_world
266 : (*deps_.current_world == 1)
267 ? deps_.overworld->mutable_map_tiles()->dark_world
268 : deps_.overworld->mutable_map_tiles()->special_world;
269
270 // Apply the fill (repeat pattern across 32x32).
271 for (int y = 0; y < 32; ++y) {
272 for (int x = 0; x < 32; ++x) {
273 const int pattern_x = x % pattern_w;
274 const int pattern_y = y % pattern_h;
275 const int new_tile_id =
276 pattern_ids[pattern_y * pattern_w + pattern_x];
277
278 const int global_x = map_x * 32 + x;
279 const int global_y = map_y * 32 + y;
280 if (global_x < 0 || global_x >= 256 || global_y < 0 ||
281 global_y >= 256) {
282 continue;
283 }
284
285 const int old_tile_id = world_tiles[global_x][global_y];
286 if (old_tile_id == new_tile_id) {
287 continue;
288 }
289
291 global_x, global_y, old_tile_id);
292 world_tiles[global_x][global_y] = new_tile_id;
293 }
294 }
295
296 deps_.rom->set_dirty(true);
298 *deps_.current_map = target_map;
300 }
301 }
302 }
303 }
304
305 // Rectangle selection stamping (brush mode only).
308 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
309 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
310 LOG_DEBUG("TilePaintingManager",
311 "CheckForOverworldEdits: About to apply rectangle selection");
312
313 auto& selected_world =
314 (*deps_.current_world == 0)
315 ? deps_.overworld->mutable_map_tiles()->light_world
316 : (*deps_.current_world == 1)
317 ? deps_.overworld->mutable_map_tiles()->dark_world
318 : deps_.overworld->mutable_map_tiles()->special_world;
319 // selected_points are now stored in world coordinates
320 auto start = deps_.ow_map_canvas->selected_points()[0];
321 auto end = deps_.ow_map_canvas->selected_points()[1];
322
323 // Calculate the bounds of the rectangle in terms of 16x16 tile indices
324 int start_x = std::floor(start.x / kTile16Size) * kTile16Size;
325 int start_y = std::floor(start.y / kTile16Size) * kTile16Size;
326 int end_x = std::floor(end.x / kTile16Size) * kTile16Size;
327 int end_y = std::floor(end.y / kTile16Size) * kTile16Size;
328
329 if (start_x > end_x)
330 std::swap(start_x, end_x);
331 if (start_y > end_y)
332 std::swap(start_y, end_y);
333
334 constexpr int local_map_size = 512; // Size of each local map
335 // Number of tiles per local map (since each tile is 16x16)
336 constexpr int tiles_per_local_map = local_map_size / kTile16Size;
337
338 LOG_DEBUG("TilePaintingManager",
339 "CheckForOverworldEdits: About to fill rectangle with "
340 "current_tile16=%d",
342
343 // Apply the selected tiles to each position in the rectangle
344 // CRITICAL FIX: Use pre-computed tile16_ids instead of recalculating
345 // from selected_tiles. This prevents wrapping issues when dragging near
346 // boundaries.
347 int i = 0;
348 for (int y = start_y;
349 y <= end_y &&
350 i < static_cast<int>(deps_.selected_tile16_ids->size());
351 y += kTile16Size) {
352 for (int x = start_x;
353 x <= end_x &&
354 i < static_cast<int>(deps_.selected_tile16_ids->size());
355 x += kTile16Size, ++i) {
356 // Determine which local map (512x512) the tile is in
357 int local_map_x = x / local_map_size;
358 int local_map_y = y / local_map_size;
359
360 // Calculate the tile's position within its local map
361 int tile16_x = (x % local_map_size) / kTile16Size;
362 int tile16_y = (y % local_map_size) / kTile16Size;
363
364 // Calculate the index within the overall map structure
365 int index_x = local_map_x * tiles_per_local_map + tile16_x;
366 int index_y = local_map_y * tiles_per_local_map + tile16_y;
367
368 // FIXED: Use pre-computed tile ID from the ORIGINAL selection
369 int tile16_id = (*deps_.selected_tile16_ids)[i];
370 // Bounds check for the selected world array
371 int rect_width = ((end_x - start_x) / kTile16Size) + 1;
372 int rect_height = ((end_y - start_y) / kTile16Size) + 1;
373
374 // Prevent painting from wrapping around at the edges of large maps
375 int start_local_map_x = start_x / local_map_size;
376 int start_local_map_y = start_y / local_map_size;
377 int end_local_map_x = end_x / local_map_size;
378 int end_local_map_y = end_y / local_map_size;
379
380 bool in_same_local_map = (start_local_map_x == end_local_map_x) &&
381 (start_local_map_y == end_local_map_y);
382
383 if (in_same_local_map && index_x >= 0 &&
384 (index_x + rect_width - 1) < 0x200 && index_y >= 0 &&
385 (index_y + rect_height - 1) < 0x200) {
386 // Get old tile value for undo tracking
387 int old_tile_id = selected_world[index_x][index_y];
388 if (old_tile_id != tile16_id) {
390 *deps_.current_world, index_x,
391 index_y, old_tile_id);
392 }
393
394 selected_world[index_x][index_y] = tile16_id;
395
396 // CRITICAL FIX: Also update the bitmap directly like single tile
397 // drawing
398 ImVec2 tile_position(x, y);
399 auto tile_data =
401 if (!tile_data.empty()) {
402 RenderUpdatedMapBitmap(tile_position, tile_data);
403 LOG_DEBUG(
404 "TilePaintingManager",
405 "CheckForOverworldEdits: Updated bitmap at position (%d,%d) "
406 "with tile16_id=%d",
407 x, y, tile16_id);
408 } else {
409 LOG_ERROR("TilePaintingManager",
410 "ERROR: Failed to get tile data for tile16_id=%d",
411 tile16_id);
412 }
413 }
414 }
415 }
416
417 // Finalize the undo batch operation after all tiles are placed
419
420 deps_.rom->set_dirty(true);
422 }
423 }
424}
425
426// ---------------------------------------------------------------------------
427// CheckForSelectRectangle - rectangle drag-to-select tiles
428// ---------------------------------------------------------------------------
430 // Pass the canvas scale for proper zoom handling
431 float scale = deps_.ow_map_canvas->global_scale();
432 if (scale <= 0.0f)
433 scale = 1.0f;
435
436 // Single tile case
437 if (deps_.ow_map_canvas->selected_tile_pos().x != -1) {
441
442 // Scroll blockset canvas to show the selected tile
444 }
445
446 // Rectangle selection case - use member variable instead of static local
448 // Get the tile16 IDs from the selected tile ID positions
449 deps_.selected_tile16_ids->clear();
450
451 if (deps_.ow_map_canvas->selected_tiles().size() > 0) {
452 // Set the current world and map in overworld for proper tile lookup
455 for (auto& each : deps_.ow_map_canvas->selected_tiles()) {
456 deps_.selected_tile16_ids->push_back(
458 }
459 }
460 }
461 // Create a composite image of all the tile16s selected
463 *deps_.tile16_blockset, 0x10,
465}
466
467// ---------------------------------------------------------------------------
468// PickTile16FromHoveredCanvas - eyedropper tool
469// ---------------------------------------------------------------------------
472 return false;
473 }
474
475 const bool allow_special_tail =
477
478 const ImVec2 scaled_position = deps_.ow_map_canvas->hover_mouse_pos();
479 float scale = deps_.ow_map_canvas->global_scale();
480 if (scale <= 0.0f) {
481 scale = 1.0f;
482 }
483
484 const int map_x =
485 static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
486 const int map_y =
487 static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
488 if (map_x < 0 || map_x >= 8 || map_y < 0 || map_y >= 8) {
489 return false;
490 }
491 if (!allow_special_tail && *deps_.current_world == 2 && map_y >= 4) {
492 return false;
493 }
494
495 const int local_tile_x =
496 (static_cast<int>(scaled_position.x / scale) % kOverworldMapSize) /
498 const int local_tile_y =
499 (static_cast<int>(scaled_position.y / scale) % kOverworldMapSize) /
501 if (local_tile_x < 0 || local_tile_x >= 32 || local_tile_y < 0 ||
502 local_tile_y >= 32) {
503 return false;
504 }
505
506 const int world_tile_x = map_x * 32 + local_tile_x;
507 const int world_tile_y = map_y * 32 + local_tile_y;
508 if (world_tile_x < 0 || world_tile_x >= 256 || world_tile_y < 0 ||
509 world_tile_y >= 256) {
510 return false;
511 }
512
513 const auto* map_tiles = deps_.overworld->mutable_map_tiles();
514 if (!map_tiles) {
515 return false;
516 }
517 const auto& world_tiles = (*deps_.current_world == 0) ? map_tiles->light_world
518 : (*deps_.current_world == 1)
519 ? map_tiles->dark_world
520 : map_tiles->special_world;
521 const int tile_id = world_tiles[world_tile_x][world_tile_y];
522 if (tile_id < 0) {
523 return false;
524 }
525
528 } else {
529 *deps_.current_tile16 = tile_id;
530 auto set_tile_status =
532 if (!set_tile_status.ok()) {
533 util::logf("Failed to sync Tile16 editor after eyedropper: %s",
534 set_tile_status.message().data());
535 }
536 }
537
539 return true;
540}
541
542// ---------------------------------------------------------------------------
543// ToggleBrushTool - switch between DRAW_TILE and MOUSE modes
544// ---------------------------------------------------------------------------
558
559// ---------------------------------------------------------------------------
560// ActivateFillTool - toggle FILL_TILE mode on/off
561// ---------------------------------------------------------------------------
572
573} // namespace yaze::editor
void set_dirty(bool dirty)
Definition rom.h:134
static Flags & get()
Definition features.h:118
absl::Status SetCurrentTile(int id)
void CheckForSelectRectangle()
Draw and create the tile16 IDs that are currently selected.
void DrawOverworldEdits()
Handle the actual drawing of a single tile (called by CheckForOverworldEdits when DrawTilemapPainter ...
void CheckForOverworldEdits()
Main entry point: check for tile edits (paint, fill, stamp).
void RenderUpdatedMapBitmap(const ImVec2 &click_position, const std::vector< uint8_t > &tile_data)
Update bitmap pixels after a single tile paint.
bool PickTile16FromHoveredCanvas()
Eyedropper: pick the tile16 under the hovered canvas position.
void ActivateFillTool()
Toggle FILL_TILE mode on/off.
void ToggleBrushTool()
Toggle between DRAW_TILE and MOUSE modes.
TilePaintingManager(const TilePaintingDependencies &deps, const TilePaintingCallbacks &callbacks)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
void WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:581
const std::vector< uint8_t > & vector() const
Definition bitmap.h:381
auto size() const
Definition bitmap.h:376
bool is_active() const
Definition bitmap.h:384
void set_modified(bool modified)
Definition bitmap.h:388
auto selected_tile_pos() const
Definition canvas.h:489
auto global_scale() const
Definition canvas.h:491
auto select_rect_active() const
Definition canvas.h:487
void SetUsageMode(CanvasUsage usage)
Definition canvas.cc:280
auto selected_tiles() const
Definition canvas.h:488
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))
Draw group of bitmaps for multi-tile selection preview.
Definition canvas.cc:1236
auto hover_mouse_pos() const
Definition canvas.h:555
auto drawn_tile_position() const
Definition canvas.h:450
bool DrawTilemapPainter(gfx::Tilemap &tilemap, int current_tile)
Definition canvas.cc:980
void set_selected_tile_pos(ImVec2 pos)
Definition canvas.h:490
void DrawSelectRect(int current_map, int tile_size=0x10, float scale=1.0f)
Definition canvas.cc:1123
bool IsMouseHovering() const
Definition canvas.h:433
auto selected_points() const
Definition canvas.h:553
void set_current_world(int world)
Definition overworld.h:604
int GetTileFromPosition(ImVec2 position) const
Definition overworld.h:505
void set_current_map(int i)
Definition overworld.h:603
uint16_t GetTile(int x, int y) const
Definition overworld.h:605
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
Editors are the view controllers for the application.
constexpr unsigned int kOverworldMapSize
constexpr int kTile16Size
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:268
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115
constexpr int kNumOverworldMaps
Definition common.h:85
struct yaze::core::FeatureFlags::Flags::Overworld overworld
Callbacks for undo integration and map refresh.
std::function< void(int tile_id)> request_tile16_selection
When set, eyedropper routes here (guarded RequestTileSwitch path).
std::function< void(int map_index)> refresh_overworld_map_on_demand
std::function< void()> scroll_blockset_to_current_tile
std::function< void()> finalize_paint_operation
std::function< void(int map_id, int world, int x, int y, int old_tile_id)> create_undo_point
Shared state for the tile painting system.
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > * maps_bmp
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119