19 const int world_start = std::clamp(world, 0, 2) * 0x40;
24 return world >= 0 && world <= 2 && map_x >= 0 && map_x < 8 && map_y >= 0 &&
29 return world * 0x40 + map_x + map_y * 8;
36 : deps_(deps), callbacks_(callbacks) {}
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) ||
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)))};
61 if (position.
x < 0 || position.
y < 0 ||
63 position.
y / kTilesPerMap)) {
67 return position.
x <
static_cast<int>(tiles.size()) &&
68 position.
y <
static_cast<int>(tiles[position.
x].size());
83 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
85 }
else if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
88 const bool can_paint =
95 if (can_paint && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
97 const TilePosition screen{anchor.
x / kTilesPerMap * kTilesPerMap,
98 anchor.y / kTilesPerMap * kTilesPerMap};
99 PaintPattern(selected ? *selected : single, screen, kTilesPerMap,
108 if (can_paint && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
109 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
110 PaintPattern(*selected, anchor, selected->width, selected->height,
false);
112 }
else if (single.valid()) {
115 auto* points = canvas.mutable_points();
117 if (canvas.IsMouseHovering() &&
IsValidTile(anchor)) {
119 canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
121 points->push_back(ImVec2(anchor.x * size, anchor.y * size));
122 points->push_back(ImVec2((anchor.x + 1) * size, (anchor.y + 1) * size));
125 if (can_paint && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
126 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
133 if (!brush.
valid()) {
134 return absl::InvalidArgumentError(
"Clipboard tile pattern is invalid");
137 return absl::FailedPreconditionError(
"Hover the overworld canvas to paste");
140 return absl::OkStatus();
145 int height,
bool finalize) {
146 if (!brush.
valid()) {
150 for (
int y = 0; y < height; ++y) {
151 for (
int x = 0; x < width; ++x) {
156 if (std::none_of(changed_maps.begin(), changed_maps.end(),
157 [](
bool changed) { return changed; })) {
168 const int anchor_map = MapIndexForGridPosition(
179 if (changed_maps[map]) {
195 if (old_id == tile_id) {
200 position.
y / kTilesPerMap);
204 changed_maps[map] =
true;
209 if (!pixels.empty()) {
216 int map_id,
int tile_id)
const {
219 if (map && map->is_built() && !map->current_tile16_blockset().empty()) {
222 if (!pixels.empty()) {
233 const std::vector<uint8_t>& tile_data) {
234 if (map_id < 0 || map_id >=
static_cast<int>(
deps_.
maps_bmp->size())) {
241 const int pixel_x = position.
x % kTilesPerMap *
kTile16Size;
242 const int pixel_y = position.
y % kTilesPerMap *
kTile16Size;
246 if (offset >= 0 && offset <
static_cast<int>(bitmap.size())) {
247 bitmap.WriteToPixel(offset, tile_data[y *
kTile16Size + x]);
251 bitmap.set_modified(
true);
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))},
272 if (points.size() != 2) {
275 const int left = std::max(
277 std::floor(std::min(points[0].x, points[1].x) /
kTile16Size)));
278 const int top = std::max(
280 std::floor(std::min(points[0].y, points[1].y) /
kTile16Size)));
282 std::min(8 * kTilesPerMap - 1,
283 static_cast<int>(std::floor(std::max(points[0].x, points[1].x) /
287 static_cast<int>(std::floor(std::max(points[0].y, points[1].y) /
289 TileBrush captured{right - left + 1, bottom - top + 1, {}};
290 if (captured.width <= 0 || captured.height <= 0) {
291 canvas.ClearSelection();
294 for (
int y = top; y <= bottom; ++y) {
295 for (
int x = left; x <= right; ++x) {
297 canvas.ClearSelection();
303 brush_ = std::move(captured);
310 if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
313 ImGui::IsItemActive() &&
314 !ImGui::GetIO().KeyShift;
316 const float scale = canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
318 if (canvas.selected_tile_pos().x != -1) {
322 const int tile_id =
WorldTiles()[source.x][source.y];
330 canvas.set_selected_tile_pos(ImVec2(-1, -1));
332 if (!was_active && canvas.select_rect_active()) {
336 canvas.ClearSelection();
339 if (!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
342 if (!canvas.select_rect_active()) {
353 bool mark_selection,
int alpha) {
355 if (mark_selection) {
365 std::array<MapBrushPreview, zelda3::kNumOverworldMaps> pieces;
366 std::array<bool, zelda3::kNumOverworldMaps> has_piece{};
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))
376 const int map_id = MapIndexForGridPosition(world, mx, my);
382 const auto& source = map->current_tile16_blockset();
385 auto& piece = pieces[map_id];
387 if (piece.pixels.empty())
391 has_piece[map_id] =
true;
394 const float scale = canvas.global_scale() > 0 ? canvas.global_scale() : 1.0f;
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();
401 ImVec2(canvas.zero_point().x + canvas.canvas_size().x * scale,
402 canvas.zero_point().y + canvas.canvas_size().y * scale),
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);
410 const int map_id = MapIndexForGridPosition(
411 world, position.x / kTilesPerMap, position.y / kTilesPerMap);
414 if (!atlas.is_active() || !atlas.texture() || atlas.width() < 16 ||
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
421 const int source_y = has_piece[map_id]
422 ? (position.y - pieces[map_id].tile_y) * 16
424 if (source_y + 16 > atlas.height())
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)));
453 if (scaled_position.x < 0.0f || scaled_position.y < 0.0f) {
465 const int local_tile_x =
468 const int local_tile_y =
471 if (local_tile_x < 0 || local_tile_x >= 32 || local_tile_y < 0 ||
472 local_tile_y >= 32) {
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) {
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())) {
495 const int tile_id = world_tiles[world_tile_x][world_tile_y];
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());
void set_dirty(bool dirty)
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.
TilePaintingCallbacks callbacks_
bool IsValidTile(TilePosition position) 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 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.
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 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)
TilePaintingDependencies deps_
TilePosition HoveredTile() const
void PaintPattern(const TileBrush &brush, TilePosition anchor, int width, int height, bool finalize=true)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
auto global_scale() const
auto select_rect_active() const
void SetUsageMode(CanvasUsage usage)
auto hover_mouse_pos() const
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 AllocatedRowsForWorld(int world)
bool IsValidMapGridPosition(int world, int map_x, int map_y)
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 kNumOverworldMaps
int at(int x, int y) const
std::vector< int > tile_ids
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.
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.