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.
2
3#include <algorithm>
4#include <cmath>
5#include <vector>
6
11#include "util/log.h"
12
13namespace yaze::editor {
14namespace {
15
17
18int AllocatedRowsForWorld(int world) {
19 const int world_start = std::clamp(world, 0, 2) * 0x40;
20 return (std::clamp(zelda3::kNumOverworldMaps - world_start, 0, 0x40) + 7) / 8;
21}
22
23bool IsValidMapGridPosition(int world, int map_x, int map_y) {
24 return world >= 0 && world <= 2 && map_x >= 0 && map_x < 8 && map_y >= 0 &&
25 map_y < AllocatedRowsForWorld(world);
26}
27
28int MapIndexForGridPosition(int world, int map_x, int map_y) {
29 return world * 0x40 + map_x + map_y * 8;
30}
31
32} // namespace
33
35 const TilePaintingCallbacks& callbacks)
36 : deps_(deps), callbacks_(callbacks) {}
37
38std::vector<std::vector<uint16_t>>& TilePaintingManager::WorldTiles() const {
40}
41
43 const auto& canvas = *deps_.ow_map_canvas;
44 const float scale = canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
45 const ImVec2 origin(canvas.zero_point().x + canvas.scrolling().x,
46 canvas.zero_point().y + canvas.scrolling().y);
47 const ImVec2 mouse = ImGui::GetIO().MousePos;
48 const float x = (mouse.x - origin.x) / scale / kTile16Size;
49 const float y = (mouse.y - origin.y) / scale / kTile16Size;
50 if (!ImGui::IsMousePosValid(&mouse) || !std::isfinite(x) ||
51 !std::isfinite(y)) {
52 return {-256, -256}; // Outside even the largest valid brush footprint.
53 }
54 // Bound before converting to int; focus loss or extreme scrolling can supply
55 // coordinates outside the integer range. Neither bound reaches a valid tile.
56 return {static_cast<int>(std::floor(std::clamp(x, -256.0f, 512.0f))),
57 static_cast<int>(std::floor(std::clamp(y, -256.0f, 512.0f)))};
58}
59
61 if (position.x < 0 || position.y < 0 ||
62 !IsValidMapGridPosition(*deps_.current_world, position.x / kTilesPerMap,
63 position.y / kTilesPerMap)) {
64 return false;
65 }
66 const auto& tiles = WorldTiles();
67 return position.x < static_cast<int>(tiles.size()) &&
68 position.y < static_cast<int>(tiles[position.x].size());
69}
70
75
78 auto& canvas = *deps_.ow_map_canvas;
79 if (single_paint_pending_ && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
82 }
83 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
84 paint_gesture_owned_ = canvas.IsMouseHovering() && ImGui::IsItemActive();
85 } else if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
87 }
88 const bool can_paint =
89 paint_gesture_owned_ && canvas.IsMouseHovering() && ImGui::IsItemActive();
90 const auto anchor = HoveredTile();
91 const auto* selected = selection_brush();
92 const TileBrush single{1, 1, {*deps_.current_tile16}};
93
95 if (can_paint && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
96 IsValidTile(anchor)) {
97 const TilePosition screen{anchor.x / kTilesPerMap * kTilesPerMap,
98 anchor.y / kTilesPerMap * kTilesPerMap};
99 PaintPattern(selected ? *selected : single, screen, kTilesPerMap,
100 kTilesPerMap);
101 }
102 return;
103 }
105 return;
106 }
107 if (selected) {
108 if (can_paint && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
109 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
110 PaintPattern(*selected, anchor, selected->width, selected->height, false);
111 }
112 } else if (single.valid()) {
113 // The hovered tile shows the selected Tile16 as the map under the cursor
114 // draws it (its own graphics and palette), not as the current map does.
115 auto* points = canvas.mutable_points();
116 points->clear();
117 if (canvas.IsMouseHovering() && IsValidTile(anchor)) {
118 const float scale =
119 canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
120 const float size = kTile16Size * scale;
121 points->push_back(ImVec2(anchor.x * size, anchor.y * size));
122 points->push_back(ImVec2((anchor.x + 1) * size, (anchor.y + 1) * size));
123 DrawBrushPreview(single, anchor, /*mark_selection=*/false, 255);
124 }
125 if (can_paint && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
126 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
127 PaintPattern(single, anchor, 1, 1, false);
128 }
129 }
130}
131
132absl::Status TilePaintingManager::PasteBrush(const TileBrush& brush) {
133 if (!brush.valid()) {
134 return absl::InvalidArgumentError("Clipboard tile pattern is invalid");
135 }
137 return absl::FailedPreconditionError("Hover the overworld canvas to paste");
138 }
139 PaintPattern(brush, HoveredTile(), brush.width, brush.height);
140 return absl::OkStatus();
141}
142
144 TilePosition anchor, int width,
145 int height, bool finalize) {
146 if (!brush.valid()) {
147 return;
148 }
149 ChangedMaps changed_maps{};
150 for (int y = 0; y < height; ++y) {
151 for (int x = 0; x < width; ++x) {
152 PaintTile({anchor.x + x, anchor.y + y},
153 brush.at(x % brush.width, y % brush.height), changed_maps);
154 }
155 }
156 if (std::none_of(changed_maps.begin(), changed_maps.end(),
157 [](bool changed) { return changed; })) {
158 return;
159 }
160 deps_.rom->set_dirty(true);
161 if (finalize) {
163 single_paint_pending_ = false;
164 } else {
166 }
167 if (IsValidTile(anchor)) {
168 const int anchor_map = MapIndexForGridPosition(
169 *deps_.current_world, anchor.x / kTilesPerMap, anchor.y / kTilesPerMap);
170 // A direct write changes the selected map without refreshing its Tile16
171 // palette and ignores the pin. The editor callback does both.
174 } else {
175 *deps_.current_map = anchor_map;
176 }
177 }
178 for (int map = 0; map < zelda3::kNumOverworldMaps; ++map) {
179 if (changed_maps[map]) {
180 if ((*deps_.maps_bmp)[map].is_active()) {
183 }
185 }
186 }
187}
188
190 ChangedMaps& changed_maps) {
191 if (!IsValidTile(position)) {
192 return false;
193 }
194 auto& old_id = WorldTiles()[position.x][position.y];
195 if (old_id == tile_id) {
196 return false;
197 }
198 const int map =
199 MapIndexForGridPosition(*deps_.current_world, position.x / kTilesPerMap,
200 position.y / kTilesPerMap);
202 position.y, old_id);
203 old_id = tile_id;
204 changed_maps[map] = true;
205 (*deps_.maps_bmp)[map].set_modified(true);
206 // Draw with the destination map's graphics: a stroke can cross into an
207 // area whose sheets differ from the current map's blockset.
208 const auto pixels = Tile16PixelsForMap(map, tile_id);
209 if (!pixels.empty()) {
210 RenderMapTile(map, position, pixels);
211 }
212 return true;
213}
214
216 int map_id, int tile_id) const {
217 const auto* map =
218 deps_.overworld ? deps_.overworld->overworld_map(map_id) : nullptr;
219 if (map && map->is_built() && !map->current_tile16_blockset().empty()) {
220 auto pixels =
221 Tile16PixelsFromBlockset(map->current_tile16_blockset(), tile_id);
222 if (!pixels.empty()) {
223 return pixels;
224 }
225 }
227 return gfx::GetTilemapData(*deps_.tile16_blockset, tile_id);
228 }
229 return {};
230}
231
233 const std::vector<uint8_t>& tile_data) {
234 if (map_id < 0 || map_id >= static_cast<int>(deps_.maps_bmp->size())) {
235 return;
236 }
237 auto& bitmap = (*deps_.maps_bmp)[map_id];
238 if (!bitmap.is_active() || tile_data.size() < kTile16Size * kTile16Size) {
239 return;
240 }
241 const int pixel_x = position.x % kTilesPerMap * kTile16Size;
242 const int pixel_y = position.y % kTilesPerMap * kTile16Size;
243 for (int y = 0; y < kTile16Size; ++y) {
244 for (int x = 0; x < kTile16Size; ++x) {
245 const int offset = (pixel_y + y) * kOverworldMapSize + pixel_x + x;
246 if (offset >= 0 && offset < static_cast<int>(bitmap.size())) {
247 bitmap.WriteToPixel(offset, tile_data[y * kTile16Size + x]);
248 }
249 }
250 }
251 bitmap.set_modified(true);
252}
253
255 const ImVec2& position, const std::vector<uint8_t>& tile_data) {
257 {static_cast<int>(std::floor(position.x / kTile16Size)),
258 static_cast<int>(std::floor(position.y / kTile16Size))},
259 tile_data);
260 if (*deps_.current_map >= 0 &&
262 (*deps_.maps_bmp)[*deps_.current_map].is_active()) {
266 }
267}
268
270 auto& canvas = *deps_.ow_map_canvas;
271 const auto points = canvas.selected_points();
272 if (points.size() != 2) {
273 return;
274 }
275 const int left = std::max(
276 0, static_cast<int>(
277 std::floor(std::min(points[0].x, points[1].x) / kTile16Size)));
278 const int top = std::max(
279 0, static_cast<int>(
280 std::floor(std::min(points[0].y, points[1].y) / kTile16Size)));
281 const int right =
282 std::min(8 * kTilesPerMap - 1,
283 static_cast<int>(std::floor(std::max(points[0].x, points[1].x) /
284 kTile16Size)));
285 const int bottom =
286 std::min(AllocatedRowsForWorld(*deps_.current_world) * kTilesPerMap - 1,
287 static_cast<int>(std::floor(std::max(points[0].y, points[1].y) /
288 kTile16Size)));
289 TileBrush captured{right - left + 1, bottom - top + 1, {}};
290 if (captured.width <= 0 || captured.height <= 0) {
291 canvas.ClearSelection();
292 return;
293 }
294 for (int y = top; y <= bottom; ++y) {
295 for (int x = left; x <= right; ++x) {
296 if (!IsValidTile({x, y})) {
297 canvas.ClearSelection();
298 return;
299 }
300 captured.tile_ids.push_back(WorldTiles()[x][y]);
301 }
302 }
303 brush_ = std::move(captured);
305}
306
308 auto& canvas = *deps_.ow_map_canvas;
309 const bool was_active = canvas.select_rect_active();
310 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
311 // Shift+right-click opens the map context menu instead of sampling.
312 selection_gesture_owned_ = canvas.IsMouseHovering() &&
313 ImGui::IsItemActive() &&
314 !ImGui::GetIO().KeyShift;
315 }
316 const float scale = canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
317 canvas.DrawSelectRect(*deps_.current_map, kTile16Size, scale);
318 if (canvas.selected_tile_pos().x != -1) {
319 const auto source = HoveredTile();
320 if (selection_gesture_owned_ && IsValidTile(source)) {
321 // Same path as the I key: keeps staged Tile16 edits guarded.
322 const int tile_id = WorldTiles()[source.x][source.y];
325 } else {
326 *deps_.current_tile16 = tile_id;
327 }
329 }
330 canvas.set_selected_tile_pos(ImVec2(-1, -1));
331 }
332 if (!was_active && canvas.select_rect_active()) {
333 if (selection_gesture_owned_ && canvas.IsMouseHovering()) {
335 } else {
336 canvas.ClearSelection();
337 }
338 }
339 if (!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
341 }
342 if (!canvas.select_rect_active()) {
343 brush_ = {};
344 deps_.selected_tile16_ids->clear();
345 }
346 if (selection_brush() && canvas.IsMouseHovering()) {
347 DrawBrushPreview(brush_, HoveredTile(), /*mark_selection=*/true, 180);
348 }
349}
350
352 TilePosition anchor,
353 bool mark_selection, int alpha) {
354 auto& canvas = *deps_.ow_map_canvas;
355 if (mark_selection) {
356 auto* points = canvas.mutable_selected_points();
357 points->clear();
358 points->push_back(ImVec2(anchor.x * kTile16Size, anchor.y * kTile16Size));
359 points->push_back(ImVec2((anchor.x + brush.width - 1) * kTile16Size,
360 (anchor.y + brush.height - 1) * kTile16Size));
361 }
362
363 // Build each destination piece once. The selected/pinned map's atlas is
364 // only a fallback for synthetic or unavailable map data.
365 std::array<MapBrushPreview, zelda3::kNumOverworldMaps> pieces;
366 std::array<bool, zelda3::kNumOverworldMaps> has_piece{};
367 const int world = *deps_.current_world;
368 const int first_x = std::max(0, anchor.x) / kTilesPerMap;
369 const int first_y = std::max(0, anchor.y) / kTilesPerMap;
370 const int last_x = std::min(255, anchor.x + brush.width - 1) / kTilesPerMap;
371 const int last_y = std::min(255, anchor.y + brush.height - 1) / kTilesPerMap;
372 for (int my = first_y; my <= last_y; ++my) {
373 for (int mx = first_x; mx <= last_x; ++mx) {
374 if (!IsValidMapGridPosition(world, mx, my))
375 continue;
376 const int map_id = MapIndexForGridPosition(world, mx, my);
377 const auto* map = deps_.overworld->overworld_map(map_id);
378 if (!map)
379 continue;
380 if (!map->is_built() && !deps_.overworld->EnsureMapBuilt(map_id).ok())
381 continue;
382 const auto& source = map->current_tile16_blockset();
383 if (source.empty())
384 continue;
385 auto& piece = pieces[map_id];
386 piece = BuildMapBrushPreview(brush, anchor.x, anchor.y, mx, my, source);
387 if (piece.pixels.empty())
388 continue;
389 auto& preview = map_brush_previews_[map_id];
390 SyncMapBrushPreview(piece, map->current_palette(), preview);
391 has_piece[map_id] = true;
392 }
393 }
394 const float scale = canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
395 const float size = kTile16Size * scale;
396 const ImVec2 origin(canvas.zero_point().x + canvas.scrolling().x,
397 canvas.zero_point().y + canvas.scrolling().y);
398 auto* draw = canvas.draw_list();
399 draw->PushClipRect(
400 canvas.zero_point(),
401 ImVec2(canvas.zero_point().x + canvas.canvas_size().x * scale,
402 canvas.zero_point().y + canvas.canvas_size().y * scale),
403 true);
404 for (int y = 0; y < brush.height; ++y) {
405 for (int x = 0; x < brush.width; ++x) {
406 const int id = brush.at(x, y);
407 const TilePosition position{anchor.x + x, anchor.y + y};
408 if (!IsValidTile(position))
409 continue;
410 const int map_id = MapIndexForGridPosition(
411 world, position.x / kTilesPerMap, position.y / kTilesPerMap);
412 const auto& atlas = has_piece[map_id] ? map_brush_previews_[map_id]
414 if (!atlas.is_active() || !atlas.texture() || atlas.width() < 16 ||
415 atlas.height() < 16)
416 continue;
417 const int columns = atlas.width() / 16;
418 const int source_x = has_piece[map_id]
419 ? (position.x - pieces[map_id].tile_x) * 16
420 : id % columns * 16;
421 const int source_y = has_piece[map_id]
422 ? (position.y - pieces[map_id].tile_y) * 16
423 : id / columns * 16;
424 if (source_y + 16 > atlas.height())
425 continue;
426 const ImVec2 start(origin.x + position.x * size,
427 origin.y + position.y * size);
428 const ImVec2 uv0(static_cast<float>(source_x) / atlas.width(),
429 static_cast<float>(source_y) / atlas.height());
430 const ImVec2 uv1(uv0.x + 16.0f / atlas.width(),
431 uv0.y + 16.0f / atlas.height());
432 draw->AddImage((ImTextureID)(intptr_t)atlas.texture(), start,
433 ImVec2(start.x + size, start.y + size), uv0, uv1,
434 ImGui::GetColorU32(IM_COL32(255, 255, 255, alpha)));
435 }
436 }
437 draw->PopClipRect();
438}
439
440// ---------------------------------------------------------------------------
441// PickTile16FromHoveredCanvas - eyedropper tool
442// ---------------------------------------------------------------------------
445 return false;
446 }
447
448 const ImVec2 scaled_position = deps_.ow_map_canvas->hover_mouse_pos();
449 float scale = deps_.ow_map_canvas->global_scale();
450 if (scale <= 0.0f) {
451 scale = 1.0f;
452 }
453 if (scaled_position.x < 0.0f || scaled_position.y < 0.0f) {
454 return false;
455 }
456
457 const int map_x =
458 static_cast<int>(scaled_position.x / scale) / kOverworldMapSize;
459 const int map_y =
460 static_cast<int>(scaled_position.y / scale) / kOverworldMapSize;
461 if (!IsValidMapGridPosition(*deps_.current_world, map_x, map_y)) {
462 return false;
463 }
464
465 const int local_tile_x =
466 (static_cast<int>(scaled_position.x / scale) % kOverworldMapSize) /
468 const int local_tile_y =
469 (static_cast<int>(scaled_position.y / scale) % kOverworldMapSize) /
471 if (local_tile_x < 0 || local_tile_x >= 32 || local_tile_y < 0 ||
472 local_tile_y >= 32) {
473 return false;
474 }
475
476 const int world_tile_x = map_x * 32 + local_tile_x;
477 const int world_tile_y = map_y * 32 + local_tile_y;
478 if (world_tile_x < 0 || world_tile_x >= 256 || world_tile_y < 0 ||
479 world_tile_y >= 256) {
480 return false;
481 }
482
483 const auto* map_tiles = deps_.overworld->mutable_map_tiles();
484 if (!map_tiles) {
485 return false;
486 }
487 const auto& world_tiles = (*deps_.current_world == 0) ? map_tiles->light_world
488 : (*deps_.current_world == 1)
489 ? map_tiles->dark_world
490 : map_tiles->special_world;
491 if (world_tile_x >= static_cast<int>(world_tiles.size()) ||
492 world_tile_y >= static_cast<int>(world_tiles[world_tile_x].size())) {
493 return false;
494 }
495 const int tile_id = world_tiles[world_tile_x][world_tile_y];
496 if (tile_id < 0) {
497 return false;
498 }
499
502 } else {
503 *deps_.current_tile16 = tile_id;
504 auto set_tile_status =
506 if (!set_tile_status.ok()) {
507 util::logf("Failed to sync Tile16 editor after eyedropper: %s",
508 set_tile_status.message().data());
509 }
510 }
511
513 return true;
514}
515
516// ---------------------------------------------------------------------------
517// ToggleBrushTool - switch between DRAW_TILE and MOUSE modes
518// ---------------------------------------------------------------------------
532
533// ---------------------------------------------------------------------------
534// ActivateFillTool - toggle FILL_TILE mode on/off
535// ---------------------------------------------------------------------------
546
547} // namespace yaze::editor
void set_dirty(bool dirty)
Definition rom.h:189
absl::Status SetCurrentTile(int id)
std::vector< uint8_t > Tile16PixelsForMap(int map_id, int tile_id) const
bool PaintTile(TilePosition position, int tile_id, ChangedMaps &changed_maps)
void CheckForSelectRectangle()
Draw and create the tile16 IDs that are currently selected.
bool IsValidTile(TilePosition position) const
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > map_brush_previews_
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 RenderMapTile(int map_id, TilePosition position, const std::vector< uint8_t > &tile_data)
void ActivateFillTool()
Toggle FILL_TILE mode on/off.
absl::Status PasteBrush(const TileBrush &brush)
const TileBrush * selection_brush() const
The captured selection, independent of its source or preview position.
void ToggleBrushTool()
Toggle between DRAW_TILE and MOUSE modes.
void DrawBrushPreview(const TileBrush &brush, TilePosition anchor, bool mark_selection, int alpha)
std::vector< std::vector< uint16_t > > & WorldTiles() const
std::array< bool, zelda3::kNumOverworldMaps > ChangedMaps
TilePaintingManager(const TilePaintingDependencies &deps, const TilePaintingCallbacks &callbacks)
void PaintPattern(const TileBrush &brush, TilePosition anchor, int width, int height, bool finalize=true)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
bool is_active() const
Definition bitmap.h:414
int width() const
Definition bitmap.h:398
auto global_scale() const
Definition canvas.h:413
auto select_rect_active() const
Definition canvas.h:409
void SetUsageMode(CanvasUsage usage)
Definition canvas.cc:385
auto hover_mouse_pos() const
Definition canvas.h:476
auto mutable_selected_points()
Definition canvas.h:473
bool IsMouseHovering() const
Definition canvas.h:354
auto selected_points() const
Definition canvas.h:474
auto overworld_map(int i) const
Definition overworld.h:737
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:721
Editors are the view controllers for the application.
MapBrushPreview BuildMapBrushPreview(const TileBrush &brush, int anchor_x, int anchor_y, int map_x, int map_y, std::span< const uint8_t > atlas, int atlas_width=128)
void SyncMapBrushPreview(const MapBrushPreview &piece, const gfx::SnesPalette &palette, gfx::Bitmap &preview)
constexpr unsigned int kOverworldMapSize
constexpr int kTile16Size
std::vector< uint8_t > Tile16PixelsFromBlockset(std::span< const uint8_t > atlas, int tile_id, int atlas_width=128)
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:146
constexpr int kNumOverworldMaps
Definition common.h:85
int at(int x, int y) const
Definition tile_brush.h:24
std::vector< int > tile_ids
Definition tile_brush.h:15
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)> select_map_for_editing
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::kExpandedMapCount > * maps_bmp
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119