8#include "absl/strings/str_format.h"
21 const int world_start = std::clamp(world, 0, 2) * 0x40;
26 return world * 0x40 + map_x + map_y * 8;
33 : deps_(deps), callbacks_(callbacks) {}
41 const float scale = canvas.
global_scale() > 0 ? canvas.global_scale() : 1.0f;
42 const ImVec2 origin(canvas.zero_point().x + canvas.scrolling().x,
43 canvas.zero_point().y + canvas.scrolling().y);
44 const ImVec2 mouse = ImGui::GetIO().MousePos;
45 const float x = (mouse.x - origin.x) / scale /
kTile16Size;
46 const float y = (mouse.y - origin.y) / scale /
kTile16Size;
47 if (!ImGui::IsMousePosValid(&mouse) || !std::isfinite(x) ||
53 return {
static_cast<int>(std::floor(std::clamp(x, -256.0f, 512.0f))),
54 static_cast<int>(std::floor(std::clamp(y, -256.0f, 512.0f)))};
60 : LegacyAllocatedRowsForWorld(world);
65 return world >= 0 && world <= 2 && map_x >= 0 && map_x < 8 && map_y >= 0 &&
77 if (position.
x < 0 || position.
y < 0 ||
79 position.
y / kTilesPerMap)) {
80 return absl::OutOfRangeError(
"Tile is outside the displayed world");
84 return absl::FailedPreconditionError(
"Project tile reader unavailable");
89 position.
y / kTilesPerMap) >=
91 return absl::OutOfRangeError(
"Tile is outside the ROM map model");
94 if (position.
x >=
static_cast<int>(tiles.size()) ||
95 position.
y >=
static_cast<int>(tiles[position.
x].size())) {
96 return absl::OutOfRangeError(
"Tile is outside the loaded world");
98 return tiles[position.
x][position.
y];
114 brush_ = std::move(brush);
153 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
156 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
161 }
else if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
168 (!canvas.IsMouseHovering() || !
IsValidTile(anchor))) ||
169 (canvas.IsMouseHovering() &&
IsValidTile(anchor) &&
176 const bool can_paint =
182 if (can_paint && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
184 const TilePosition screen{anchor.
x / kTilesPerMap * kTilesPerMap,
185 anchor.y / kTilesPerMap * kTilesPerMap};
186 (void)
PaintPattern(selected ? *selected : single, screen, kTilesPerMap,
195 if (can_paint && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
196 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
197 (void)
PaintPattern(*selected, anchor, selected->width, selected->height,
200 }
else if (single.valid()) {
203 auto* points = canvas.mutable_points();
205 if (canvas.IsMouseHovering() &&
IsValidTile(anchor)) {
207 canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
209 points->push_back(ImVec2(anchor.x * size, anchor.y * size));
210 points->push_back(ImVec2((anchor.x + 1) * size, (anchor.y + 1) * size));
213 if (can_paint && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
214 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
221 if (!brush.
valid()) {
222 return absl::InvalidArgumentError(
"Clipboard tile pattern is invalid");
225 return absl::FailedPreconditionError(
"Hover the overworld canvas to paste");
233 int height,
bool finalize) {
234 if (!brush.
valid()) {
235 return absl::InvalidArgumentError(
"Tile pattern is invalid");
239 return absl::FailedPreconditionError(
"Project tile writer unavailable");
241 width, height, finalize);
251 anchor.
y / kTilesPerMap));
253 return absl::OkStatus();
256 for (
int y = 0; y < height; ++y) {
257 for (
int x = 0; x < width; ++x) {
262 if (std::none_of(changed_maps.begin(), changed_maps.end(),
263 [](
bool changed) { return changed; })) {
264 return absl::OkStatus();
274 const int anchor_map = MapIndexForGridPosition(
285 return absl::OkStatus();
289 int map_id,
const std::vector<GuardedTileCell>& cells) {
291 return absl::OutOfRangeError(
"Stencil map is outside the ROM map model");
294 return absl::FailedPreconditionError(
295 "Stencil map is not in the displayed world");
298 return absl::InvalidArgumentError(
"Stencil has no cells");
300 const int origin_x = (map_id % 0x40) % 8 * kTilesPerMap;
301 const int origin_y = (map_id % 0x40) / 8 * kTilesPerMap;
302 std::vector<bool> seen(kTilesPerMap * kTilesPerMap,
false);
304 std::string first_mismatch;
305 for (
const auto& cell : cells) {
306 if (cell.x < 0 || cell.x >= kTilesPerMap || cell.y < 0 ||
307 cell.y >= kTilesPerMap) {
308 return absl::InvalidArgumentError(absl::StrFormat(
309 "Stencil cell (%d,%d) is outside the map", cell.x, cell.y));
312 return absl::InvalidArgumentError(
313 absl::StrFormat(
"Stencil cell (%d,%d) proposes invalid Tile16 0x%X",
314 cell.x, cell.y, cell.proposed));
316 const int index = cell.y * kTilesPerMap + cell.x;
318 return absl::InvalidArgumentError(
319 absl::StrFormat(
"Stencil repeats cell (%d,%d)", cell.x, cell.y));
322 const TilePosition position{origin_x + cell.
x, origin_y + cell.y};
324 return absl::FailedPreconditionError(
325 "Stencil cells must be ROM map tiles, not project terrain");
327 const auto current =
ReadTile(position);
329 return current.status();
331 if (*current != cell.expected && mismatches++ == 0) {
332 first_mismatch = absl::StrFormat(
"(%d,%d) expected 0x%03X, found 0x%03X",
333 cell.x, cell.y, cell.expected, *current);
336 if (mismatches > 0) {
337 return absl::FailedPreconditionError(
338 absl::StrFormat(
"Stencil guard failed at %d cell(s); first %s",
339 mismatches, first_mismatch));
345 for (
const auto& cell : cells) {
346 if (
PaintTile({origin_x + cell.x, origin_y + cell.y}, cell.proposed,
362 if (changed_maps[map]) {
378 if (old_id == tile_id) {
383 position.
y / kTilesPerMap);
387 changed_maps[map] =
true;
392 if (!pixels.empty()) {
399 int map_id,
int tile_id)
const {
402 if (map && map->is_built() && !map->current_tile16_blockset().empty()) {
405 if (!pixels.empty()) {
416 const std::vector<uint8_t>& tile_data) {
417 if (map_id < 0 || map_id >=
static_cast<int>(
deps_.
maps_bmp->size())) {
424 const int pixel_x = position.
x % kTilesPerMap *
kTile16Size;
425 const int pixel_y = position.
y % kTilesPerMap *
kTile16Size;
429 if (offset >= 0 && offset <
static_cast<int>(bitmap.size())) {
430 bitmap.WriteToPixel(offset, tile_data[y *
kTile16Size + x]);
434 bitmap.set_modified(
true);
438 const ImVec2& position,
const std::vector<uint8_t>& tile_data) {
440 {static_cast<int>(std::floor(position.x / kTile16Size)),
441 static_cast<int>(std::floor(position.y / kTile16Size))},
455 if (points.size() != 2) {
458 const int left = std::max(
460 std::floor(std::min(points[0].x, points[1].x) /
kTile16Size)));
461 const int top = std::max(
463 std::floor(std::min(points[0].y, points[1].y) /
kTile16Size)));
465 std::min(8 * kTilesPerMap - 1,
466 static_cast<int>(std::floor(std::max(points[0].x, points[1].x) /
470 static_cast<int>(std::floor(std::max(points[0].y, points[1].y) /
472 TileBrush captured{right - left + 1, bottom - top + 1, {}};
473 if (captured.width <= 0 || captured.height <= 0) {
474 canvas.ClearSelection();
477 for (
int y = top; y <= bottom; ++y) {
478 for (
int x = left; x <= right; ++x) {
481 canvas.ClearSelection();
487 brush_ = std::move(captured);
494 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
497 ImGui::IsItemActive() &&
498 !ImGui::GetIO().KeyShift;
500 const float scale = canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
502 if (canvas.selected_tile_pos().x != -1) {
507 const int tile_id = *tile;
515 canvas.set_selected_tile_pos(ImVec2(-1, -1));
517 if (!was_active && canvas.select_rect_active()) {
522 canvas.ClearSelection();
525 if (!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
539 bool mark_selection,
int alpha) {
541 if (mark_selection) {
551 std::array<MapBrushPreview, zelda3::kNumOverworldMaps> pieces;
552 std::array<bool, zelda3::kNumOverworldMaps> has_piece{};
554 const int first_x = std::max(0, anchor.
x) / kTilesPerMap;
555 const int first_y = std::max(0, anchor.
y) / kTilesPerMap;
556 const int last_x = std::min(255, anchor.
x + brush.
width - 1) / kTilesPerMap;
557 const int last_y = std::min(255, anchor.
y + brush.
height - 1) / kTilesPerMap;
558 for (
int my = first_y; my <= last_y; ++my) {
559 for (
int mx = first_x; mx <= last_x; ++mx) {
562 const int map_id = MapIndexForGridPosition(world, mx, my);
571 const auto& source = map->current_tile16_blockset();
574 auto& piece = pieces[map_id];
576 if (piece.pixels.empty())
580 has_piece[map_id] =
true;
583 const float scale = canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
585 const ImVec2 origin(canvas.zero_point().x + canvas.scrolling().x,
586 canvas.zero_point().y + canvas.scrolling().y);
587 auto* draw = canvas.draw_list();
590 ImVec2(canvas.zero_point().x + canvas.canvas_size().x * scale,
591 canvas.zero_point().y + canvas.canvas_size().y * scale),
593 for (
int y = 0; y < brush.
height; ++y) {
594 for (
int x = 0; x < brush.
width; ++x) {
595 const int id = brush.
at(x, y);
599 const int map_id = MapIndexForGridPosition(
600 world, position.x / kTilesPerMap, position.y / kTilesPerMap);
602 const bool use_piece = !project_tile && has_piece[map_id];
611 const auto& atlas = *source_atlas;
612 if (!atlas.is_active() || !atlas.texture() || atlas.width() < 16 ||
615 const int columns = atlas.
width() / 16;
616 const int source_x = use_piece ? (position.x - pieces[map_id].tile_x) * 16
618 const int source_y = use_piece ? (position.y - pieces[map_id].tile_y) * 16
620 if (source_y + 16 > atlas.height())
622 const ImVec2 start(origin.x + position.x * size,
623 origin.y + position.y * size);
624 const ImVec2 uv0(
static_cast<float>(source_x) / atlas.width(),
625 static_cast<float>(source_y) / atlas.height());
626 const ImVec2 uv1(uv0.x + 16.0f / atlas.width(),
627 uv0.y + 16.0f / atlas.height());
628 draw->AddImage((ImTextureID)(intptr_t)atlas.texture(), start,
629 ImVec2(start.x + size, start.y + size), uv0, uv1,
630 ImGui::GetColorU32(IM_COL32(255, 255, 255, alpha)));
648 const int tile_id = *tile;
654 auto set_tile_status =
656 if (!set_tile_status.ok()) {
657 util::logf(
"Failed to sync Tile16 editor after eyedropper: %s",
658 set_tile_status.message().data());
void set_dirty(bool dirty)
absl::Status SetCurrentTile(int id)
std::vector< uint8_t > Tile16PixelsForMap(int map_id, int tile_id) const
bool IsValidMapGridPosition(int world, int map_x, int map_y) const
bool PaintTile(TilePosition position, int tile_id, ChangedMaps &changed_maps)
bool project_paint_pending_
void CheckForSelectRectangle()
Draw and create the tile16 IDs that are currently selected.
EditingMode gesture_mode_
TilePaintingCallbacks callbacks_
bool external_brush_active_
bool IsValidTile(TilePosition position) const
int AllocatedRowsForWorld(int world) const
bool paint_gesture_owned_
std::array< gfx::Bitmap, zelda3::kNumOverworldMaps > map_brush_previews_
void CheckForOverworldEdits()
Main entry point: check for tile edits (paint, fill, stamp).
void SuspendCanvasGestures()
void FinalizePendingPaint()
bool IsProjectTile(TilePosition position) const
void RenderUpdatedMapBitmap(const ImVec2 &click_position, const std::vector< uint8_t > &tile_data)
Update bitmap pixels after a single tile paint.
bool selection_gesture_owned_
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.
void SetSelectionBrush(TileBrush brush)
absl::StatusOr< int > ApplyGuardedCells(int map_id, const std::vector< GuardedTileCell > &cells)
absl::Status PasteBrush(const TileBrush &brush)
bool single_paint_pending_
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 ClearSelectionBrush()
void RefreshChangedMaps(const ChangedMaps &changed_maps)
absl::StatusOr< uint16_t > ReadTile(TilePosition position) const
void DrawBrushPreview(const TileBrush &brush, TilePosition anchor, bool mark_selection, int alpha)
absl::Status PaintPattern(const TileBrush &brush, TilePosition anchor, int width, int height, bool finalize=true)
std::vector< std::vector< uint16_t > > & WorldTiles() const
std::array< bool, zelda3::kNumOverworldMaps > ChangedMaps
TilePaintingManager(const TilePaintingDependencies &deps, const TilePaintingCallbacks &callbacks)
TilePaintingDependencies deps_
TilePosition HoveredTile() const
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Represents a bitmap image optimized for SNES ROM hacking.
auto global_scale() const
auto select_rect_active() const
void SetUsageMode(CanvasUsage usage)
auto mutable_selected_points()
bool IsMouseHovering() const
auto selected_points() const
auto overworld_map(int i) const
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
OverworldBlockset & GetMapTiles(int world_type)
int LegacyAllocatedRowsForWorld(int world)
int MapIndexForGridPosition(int world, int map_x, int map_y)
constexpr int kTilesPerMap
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)
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
constexpr int kNumTile16Individual
constexpr int kNumOverworldMaps
int at(int x, int y) const
std::vector< int > tile_ids
Callbacks for undo integration and map refresh.
std::function< absl::Status(const TileBrush &, int x, int y, int width, int height, bool finalize) paint_project_pattern)
std::function< void(int tile_id)> request_tile16_selection
When set, eyedropper routes here (guarded RequestTileSwitch path).
std::function< absl::StatusOr< uint16_t >(int world, int x, int y)> read_project_tile
std::function< void(int map_index)> refresh_overworld_map_on_demand
std::function< void()> scroll_blockset_to_current_tile
std::function< int(int world)> allocated_rows
std::function< void()> finalize_paint_operation
std::function< void(int map_id)> select_map_for_editing
std::function< bool(int world, int x, int y)> is_project_tile
std::function< void()> finalize_project_paint
std::function< const gfx::Bitmap *(int map_id)> project_tile_atlas
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.
Tile16Editor * tile16_editor
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > * maps_bmp
gui::Canvas * ow_map_canvas
zelda3::Overworld * overworld
std::vector< int > * selected_tile16_ids
gfx::Tilemap * tile16_blockset
EditingMode * current_mode
Bitmap atlas
Master bitmap containing all tiles.