yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
scratch_space.cc
Go to the documentation of this file.
1#include <algorithm>
2#include <cmath>
3#include <cstdint>
4#include <cstring>
5#include <filesystem>
6#include <functional>
7#include <memory>
8#include <set>
9#include <string>
10#include <utility>
11#include <vector>
12
13#include "absl/status/status.h"
14#include "absl/status/statusor.h"
21#include "app/gui/core/icons.h"
22#include "app/gui/core/style.h"
23#include "imgui/imgui.h"
24#include "util/i18n/tr.h"
25#include "util/macro.h"
26#include "util/platform_paths.h"
27
28namespace yaze::editor {
29namespace {
30
31absl::StatusOr<std::filesystem::path> GetScratchPadPath() {
33 return config_dir / "ScratchPad.dat";
34}
35
36// DrawBitmap offsets are screen pixels; scrolling is already in screen space.
37// Canvas's old drawn_tile_position is owned by its bitmap painter, which this
38// Tile16 workspace does not invoke.
39ImVec2 ScratchMousePosition(const gui::Canvas& canvas) {
40 const float scale = std::max(0.01f, canvas.GetGlobalScale());
41 const auto mouse = ImGui::GetIO().MousePos;
42 const auto origin = canvas.zero_point();
43 const auto scroll = canvas.scrolling();
44 return ImVec2((mouse.x - origin.x - scroll.x - 2) / scale,
45 (mouse.y - origin.y - scroll.y - 2) / scale);
46}
47
48uint64_t ScratchPreviewRevision(const ScratchPage& page, gfx::Tilemap& atlas,
49 bool loaded) {
50 uint64_t hash = 1469598103934665603ULL;
51 const auto add = [&hash](uint64_t value) {
52 hash = (hash ^ value) * 1099511628211ULL;
53 };
54 add(loaded);
55 add(atlas.atlas.width());
56 add(atlas.atlas.height());
57 for (size_t i = 0; i < atlas.atlas.palette().size(); ++i)
58 add(atlas.atlas.palette()[i].snes());
59 if (loaded && page.in_use) {
60 std::set<int> ids;
61 for (int y = 0; y < page.height; ++y)
62 for (int x = 0; x < page.width; ++x)
63 ids.insert(page.tile_data[x][y]);
64 for (const int id : ids) {
65 add(id);
66 for (const auto pixel : gfx::GetTilemapData(atlas, id))
67 add(pixel);
68 }
69 }
70 return hash;
71}
72
74 public:
75 using Apply = std::function<absl::Status(const ScratchWorkspaceState&)>;
77 ScratchWorkspaceState after, Apply apply,
78 std::string description)
79 : before_(std::move(before)),
80 after_(std::move(after)),
81 apply_(std::move(apply)),
82 description_(std::move(description)) {}
83
84 absl::Status Undo() override { return apply_(before_); }
85 absl::Status Redo() override { return apply_(after_); }
86 std::string Description() const override { return description_; }
87 size_t MemoryUsage() const override {
88 return sizeof(*this) +
89 (before_.pages.size() + after_.pages.size()) * sizeof(ScratchPage);
90 }
91
92 private:
96 std::string description_;
97};
98
99} // namespace
100
104 const auto path = GetScratchPadPath();
105 if (!path.ok()) {
106 scratch_load_status_ = path.status();
108 }
110 if (!scratch_load_status_.ok())
113 scratch_path_ = *path;
114 scratch_save_status_ = absl::OkStatus();
115 scratch_space_dirty_ = false;
116 scratch_edit_before_.reset();
118}
119
121 if (scratch_path_.empty())
122 return absl::FailedPreconditionError("Scratch workspace is not loaded");
124}
125
128 return absl::OkStatus();
131 scratch_space_dirty_ = false;
132 return absl::OkStatus();
133}
134
139
146
147void OverworldEditor::FinishScratchEdit(const std::string& description) {
150 return;
153 undo_manager_.Push(std::make_unique<ScratchWorkspaceUndoAction>(
155 [this](const ScratchWorkspaceState& state) {
156 return RestoreScratchState(state);
157 },
158 description));
159 }
160 scratch_edit_before_.reset();
161}
162
164 const ScratchWorkspaceState& state) {
168 // Keep the Undo operation usable after a save failure; dirty state protects
169 // the recoverable snapshot and normal Save retries persistence.
171 return absl::OkStatus();
172}
173
175 const auto& page = scratch_workspace_.current();
176 const int width = page.width * 16;
177 const int height = page.height * 16;
178 std::vector<uint8_t> pixels(width * height, 0);
179 if (map_blockset_loaded_ && page.in_use) {
180 for (int ty = 0; ty < page.height; ++ty) {
181 for (int tx = 0; tx < page.width; ++tx) {
182 const auto tile =
183 gfx::GetTilemapData(tile16_blockset_, page.tile_data[tx][ty]);
184 if (tile.size() != 16 * 16)
185 continue;
186 for (int y = 0; y < 16; ++y)
187 for (int x = 0; x < 16; ++x)
188 pixels[(ty * 16 + y) * width + tx * 16 + x] = tile[y * 16 + x];
189 }
190 }
191 }
192 scratch_bitmap_.Create(width, height, 8, pixels);
193 // The Tile16 atlas already carries the currently selected area's palette.
194 // Watch its actual colors, since in-place palette/Tile16 edits do not create
195 // a new Bitmap resource generation.
197 if (all_gfx_loaded_) {
200 }
203 ScratchPreviewRevision(page, tile16_blockset_, map_blockset_loaded_);
204 return absl::OkStatus();
205}
206
208 using namespace ImGui;
209 if (is_lyra_preview()) {
210 TextDisabled("Lyra preview: scratch editing unavailable");
211 return absl::OkStatus();
212 }
213 if (!scratch_load_status_.ok()) {
214 TextWrapped("Scratch workspace retained on disk: %s",
215 std::string(scratch_load_status_.message()).c_str());
216 if (Button(tr("Retry loading scratch workspace")))
217 return LoadScratchPad();
218 return absl::OkStatus();
219 }
220
221 const auto report = [this](absl::Status status) {
222 scratch_action_status_ = std::move(status);
223 };
224
225 // Escape clears any active scratch selection or tool brush
226 if (IsKeyPressed(ImGuiKey_Escape)) {
227 if (scratch_select_active_ || selection_brush() != nullptr) {
230 }
231 }
232
233 const auto& pages = scratch_workspace_.state().pages;
234 const auto& page = scratch_workspace_.current();
235 int current_page_idx = 0;
236 for (size_t i = 0; i < pages.size(); ++i) {
237 if (pages[i].id == scratch_workspace_.state().selected_id) {
238 current_page_idx = static_cast<int>(i);
239 break;
240 }
241 }
242
243 // --- Compact 26px Toolbar Header ---
244 BeginDisabled(current_page_idx <= 0);
245 if (Button(ICON_MD_CHEVRON_LEFT "##ScratchPrev")) {
246 FinishScratchEdit("Switch scratch layout");
247 scratch_workspace_.Select(pages[current_page_idx - 1].id);
250 }
251 EndDisabled();
252 if (IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
253 SetTooltip("Previous layout");
254 }
255
256 SameLine();
257
258 SetNextItemWidth(std::max(100.0f, GetContentRegionAvail().x - 220.0f));
259 if (BeginCombo("##ScratchLayout", page.name.c_str())) {
260 for (const auto& p : pages) {
261 PushID(std::to_string(p.id).c_str());
262 if (Selectable(p.name.c_str(),
264 FinishScratchEdit("Switch scratch layout");
268 }
269 PopID();
270 }
271 EndCombo();
272 }
273
274 SameLine();
275
276 BeginDisabled(current_page_idx >= static_cast<int>(pages.size()) - 1);
277 if (Button(ICON_MD_CHEVRON_RIGHT "##ScratchNext")) {
278 FinishScratchEdit("Switch scratch layout");
279 scratch_workspace_.Select(pages[current_page_idx + 1].id);
282 }
283 EndDisabled();
284 if (IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
285 SetTooltip("Next layout");
286 }
287
288 SameLine();
289
290 BeginDisabled(pages.size() >= ScratchWorkspace::kMaxPages);
291 if (Button(ICON_MD_ADD "##ScratchNew")) {
293 const auto status = scratch_workspace_.AddPage();
294 FinishScratchEdit("New scratch layout");
295 report(status.ok() ? RebuildScratchBitmapFromTileData() : status);
296 }
297 EndDisabled();
298 if (IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
299 SetTooltip("New layout");
300 }
301
302 SameLine();
303
304 const auto* active_brush = selection_brush();
305 const bool can_capture = (active_brush && active_brush->valid());
306 BeginDisabled(pages.size() >= ScratchWorkspace::kMaxPages || !can_capture);
307 if (Button(ICON_MD_FILE_DOWNLOAD "##ScratchCapture")) {
309 }
310 EndDisabled();
311 if (IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
312 SetTooltip("Capture selection from map to scratch");
313 }
314
315 SameLine();
316
317 DrawSheetScaleControl("##ScratchScale", &scratch_scale_mode_);
318
319 SameLine();
320
321 if (Button(ICON_MD_MORE_VERT "##ScratchMore")) {
322 OpenPopup("ScratchMoreMenu");
323 }
324 if (IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
325 SetTooltip("Layout options");
326 }
327
328 if (BeginPopup("ScratchMoreMenu")) {
329 if (BeginMenu("Rename layout")) {
330 if (scratch_name_id_ != page.id || !IsAnyItemActive()) {
331 scratch_name_buffer_.fill('\0');
332 std::memcpy(
333 scratch_name_buffer_.data(), page.name.data(),
334 std::min(scratch_name_buffer_.size() - 1, page.name.size()));
335 scratch_name_id_ = page.id;
336 }
337 SetNextItemWidth(140.0f);
338 if (InputText("##ScratchRenameInput", scratch_name_buffer_.data(),
340 ImGuiInputTextFlags_EnterReturnsTrue)) {
343 FinishScratchEdit("Rename scratch layout");
344 CloseCurrentPopup();
345 }
346 EndMenu();
347 }
348
349 if (BeginMenu("Resize layout")) {
350 if (scratch_resize_id_ != page.id || !IsAnyItemActive()) {
351 scratch_resize_dimensions_[0] = page.width;
352 scratch_resize_dimensions_[1] = page.height;
353 scratch_resize_id_ = page.id;
354 }
355 SetNextItemWidth(120.0f);
356 const bool enter_pressed = InputInt2(
357 "Tiles (W x H)##ScratchResize", scratch_resize_dimensions_.data(),
358 ImGuiInputTextFlags_EnterReturnsTrue);
359 SameLine();
360 const bool apply_clicked = Button("Apply##ScratchResize");
361 if (enter_pressed || apply_clicked) {
363 const auto resize = scratch_workspace_.Resize(
365 FinishScratchEdit("Resize scratch layout");
366 if (resize.ok())
368 else
369 report(resize);
370 CloseCurrentPopup();
371 }
372 EndMenu();
373 }
374
375 if (MenuItem("Duplicate layout", nullptr, false,
376 pages.size() < ScratchWorkspace::kMaxPages)) {
378 const auto status = scratch_workspace_.AddPage(true);
379 FinishScratchEdit("Duplicate scratch layout");
380 report(status.ok() ? RebuildScratchBitmapFromTileData() : status);
381 }
382
383 if (MenuItem("Clear layout")) {
384 report(ClearScratchSpace());
385 }
386
387 if (MenuItem("Delete layout", nullptr, false, pages.size() > 1)) {
389 const auto status = scratch_workspace_.DeletePage();
390 FinishScratchEdit("Delete scratch layout");
391 report(status.ok() ? RebuildScratchBitmapFromTileData() : status);
392 }
393
394 Separator();
395
396 if (MenuItem("Copy layout as brush", nullptr, false, page.in_use)) {
397 report(LoadScratchToSelection());
398 }
399
400 if (MenuItem("Refresh preview")) {
402 }
403
404 EndPopup();
405 }
406
409 ScratchPreviewRevision(page, tile16_blockset_, map_blockset_loaded_))
411
412 const float available_width = GetContentRegionAvail().x;
413 const gui::AdaptiveSheetLayout scratch_layout =
414 gui::ResolveAdaptiveSheetLayout(available_width, page.width * 16,
415 page.height * 16, scratch_scale_mode_,
416 0.5f, 4.0f, 24.0f);
418
420 options.canvas_size = ImVec2(page.width * 16 + 4, page.height * 16 + 4);
421 options.draw_grid = false;
422 options.draw_context_menu = false;
423 options.draw_overlay = false;
424 options.render_popups = false;
425 options.use_child_window = false;
426
428 "##ScratchSpaceScrollRegion",
429 ImVec2(0, std::max(150.0f, GetContentRegionAvail().y - 28)));
430 auto canvas = gui::BeginCanvas(scratch_canvas_, options);
431 scratch_canvas_.DrawBitmap(scratch_bitmap_, 2, 2, canvas.scale);
432
433 const ImVec2 origin = scratch_canvas_.zero_point();
434 const ImVec2 scroll = scratch_canvas_.scrolling();
435 const float scale = canvas.scale;
436 const float cell_size = 16.0f * scale;
437 const ImVec2 grid_min(origin.x + scroll.x + 2.0f, origin.y + scroll.y + 2.0f);
438 const ImVec2 grid_max(grid_min.x + page.width * cell_size,
439 grid_min.y + page.height * cell_size);
440
441 // Draw grid lines
443 constexpr ImU32 kGridColor = IM_COL32(200, 200, 200, 45);
444 for (int col = 0; col <= page.width; ++col) {
445 const float x = grid_min.x + col * cell_size;
446 canvas.draw_list->AddLine(ImVec2(x, grid_min.y), ImVec2(x, grid_max.y),
447 kGridColor);
448 }
449 for (int row = 0; row <= page.height; ++row) {
450 const float y = grid_min.y + row * cell_size;
451 canvas.draw_list->AddLine(ImVec2(grid_min.x, y), ImVec2(grid_max.x, y),
452 kGridColor);
453 }
454 canvas.draw_list->AddRect(grid_min, grid_max, IM_COL32(255, 255, 255, 60));
455 }
456
457 // Render existing selection rectangle if active
459 const float x0 = grid_min.x + scratch_select_min_.x * cell_size;
460 const float y0 = grid_min.y + scratch_select_min_.y * cell_size;
461 const float x1 = grid_min.x + (scratch_select_max_.x + 1.0f) * cell_size;
462 const float y1 = grid_min.y + (scratch_select_max_.y + 1.0f) * cell_size;
463 canvas.draw_list->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1),
464 IM_COL32(80, 140, 240, 50));
465 canvas.draw_list->AddRect(ImVec2(x0, y0), ImVec2(x1, y1),
466 IM_COL32(255, 255, 255, 220), 0.0f, 0, 2.0f);
467 }
468
469 if (scratch_selecting_ || (canvas.hovered && map_blockset_loaded_)) {
470 const auto cursor = ScratchMousePosition(scratch_canvas_);
471 const int x = static_cast<int>(std::floor(cursor.x / 16.0f));
472 const int y = static_cast<int>(std::floor(cursor.y / 16.0f));
473
474 // Right mouse drag-selection (Tile16 Select Rect)
475 if (canvas.hovered && map_blockset_loaded_ &&
476 IsMouseClicked(ImGuiMouseButton_Right) && x >= 0 && y >= 0 &&
477 x < page.width && y < page.height) {
478 scratch_selecting_ = true;
479 scratch_drag_start_ = ImVec2(x, y);
482 }
483
484 if (scratch_selecting_ && IsMouseDragging(ImGuiMouseButton_Right)) {
485 int cur_x = std::clamp(x, 0, page.width - 1);
486 int cur_y = std::clamp(y, 0, page.height - 1);
487 int min_x = std::min(static_cast<int>(scratch_drag_start_.x), cur_x);
488 int min_y = std::min(static_cast<int>(scratch_drag_start_.y), cur_y);
489 int max_x = std::max(static_cast<int>(scratch_drag_start_.x), cur_x);
490 int max_y = std::max(static_cast<int>(scratch_drag_start_.y), cur_y);
491 scratch_select_min_ = ImVec2(min_x, min_y);
492 scratch_select_max_ = ImVec2(max_x, max_y);
493
494 // Draw active dragging rect
495 const float x0 = grid_min.x + min_x * cell_size;
496 const float y0 = grid_min.y + min_y * cell_size;
497 const float x1 = grid_min.x + (max_x + 1.0f) * cell_size;
498 const float y1 = grid_min.y + (max_y + 1.0f) * cell_size;
499 canvas.draw_list->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1),
500 IM_COL32(100, 160, 255, 70));
501 canvas.draw_list->AddRect(ImVec2(x0, y0), ImVec2(x1, y1),
502 IM_COL32(255, 255, 255, 255), 0.0f, 0, 2.0f);
503 }
504
505 if (scratch_selecting_ && !IsMouseDown(ImGuiMouseButton_Right)) {
506 scratch_selecting_ = false;
507 int start_x = static_cast<int>(scratch_drag_start_.x);
508 int start_y = static_cast<int>(scratch_drag_start_.y);
509 int end_x = std::clamp(x, 0, page.width - 1);
510 int end_y = std::clamp(y, 0, page.height - 1);
511
512 if (start_x == end_x && start_y == end_y) {
513 // Single right click: sample Tile16
514 RequestTile16Selection(page.tile_data[start_x][start_y]);
516 } else {
517 int min_x = std::min(start_x, end_x);
518 int max_x = std::max(start_x, end_x);
519 int min_y = std::min(start_y, end_y);
520 int max_y = std::max(start_y, end_y);
521 TileBrush brush;
522 brush.width = max_x - min_x + 1;
523 brush.height = max_y - min_y + 1;
524 brush.tile_ids.reserve(brush.width * brush.height);
525 for (int row = min_y; row <= max_y; ++row) {
526 for (int col = min_x; col <= max_x; ++col) {
527 brush.tile_ids.push_back(page.tile_data[col][row]);
528 }
529 }
530 SetSelectionBrush(std::move(brush));
532 scratch_select_min_ = ImVec2(min_x, min_y);
533 scratch_select_max_ = ImVec2(max_x, max_y);
534 }
535 }
536
537 if (canvas.hovered && map_blockset_loaded_) {
538 // Brush stamp or single-tile paint
539 if (x >= 0 && y >= 0 && x < page.width && y < page.height) {
540 if (active_brush && active_brush->valid()) {
541 // Draw brush ghost preview with atlas textures
542 const float bx0 = grid_min.x + x * cell_size;
543 const float by0 = grid_min.y + y * cell_size;
544 const float bx1 = grid_min.x + (x + active_brush->width) * cell_size;
545 const float by1 = grid_min.y + (y + active_brush->height) * cell_size;
546
548 const int cols = std::max(1, tile16_blockset_.atlas.width() / 16);
549 const float atlas_w =
550 static_cast<float>(tile16_blockset_.atlas.width());
551 const float atlas_h =
552 static_cast<float>(tile16_blockset_.atlas.height());
553 if (atlas_w > 0.0f && atlas_h > 0.0f) {
554 for (int by = 0; by < active_brush->height; ++by) {
555 for (int bx = 0; bx < active_brush->width; ++bx) {
556 if (x + bx < 0 || x + bx >= page.width || y + by < 0 ||
557 y + by >= page.height)
558 continue;
559 const int brush_idx = by * active_brush->width + bx;
560 if (brush_idx >=
561 static_cast<int>(active_brush->tile_ids.size()))
562 continue;
563 const int tile_id = active_brush->tile_ids[brush_idx];
564 const int tile_col = tile_id % cols;
565 const int tile_row = tile_id / cols;
566 const float uv0_x = (tile_col * 16.0f) / atlas_w;
567 const float uv0_y = (tile_row * 16.0f) / atlas_h;
568 const float uv1_x = ((tile_col + 1) * 16.0f) / atlas_w;
569 const float uv1_y = ((tile_row + 1) * 16.0f) / atlas_h;
570 const float tx0 = grid_min.x + (x + bx) * cell_size;
571 const float ty0 = grid_min.y + (y + by) * cell_size;
572 canvas.draw_list->AddImage(
573 (ImTextureID)(intptr_t)tile16_blockset_.atlas.texture(),
574 ImVec2(tx0, ty0),
575 ImVec2(tx0 + cell_size, ty0 + cell_size),
576 ImVec2(uv0_x, uv0_y), ImVec2(uv1_x, uv1_y),
577 IM_COL32(255, 255, 255, 170));
578 }
579 }
580 }
581 }
582 canvas.draw_list->AddRect(ImVec2(bx0, by0), ImVec2(bx1, by1),
583 IM_COL32(255, 220, 50, 220), 0.0f, 0, 2.0f);
584
585 if (IsMouseClicked(ImGuiMouseButton_Left)) {
587 const auto status = scratch_workspace_.Stamp(*active_brush, x, y);
588 FinishScratchEdit("Stamp scratch layout");
589 if (!status.ok())
590 report(status);
591 else
593 }
594 } else {
595 // Hover cursor box on single tile
596 const float hx0 = grid_min.x + x * cell_size;
597 const float hy0 = grid_min.y + y * cell_size;
598 canvas.draw_list->AddRect(
599 ImVec2(hx0, hy0), ImVec2(hx0 + cell_size, hy0 + cell_size),
600 IM_COL32(255, 255, 255, 180), 0.0f, 0, 1.5f);
601
602 if (IsMouseClicked(ImGuiMouseButton_Left)) {
605 }
607 IsMouseDown(ImGuiMouseButton_Left))
609 }
610
611 // Hover to View tooltip: show tile info, usage count, coordinates, zoomed preview
612 if (!scratch_selecting_ && !IsMouseDown(ImGuiMouseButton_Left) &&
613 !IsMouseDown(ImGuiMouseButton_Right) && page.in_use) {
614 const int tile_id = page.tile_data[x][y];
615 if (ImGui::BeginTooltip()) {
616 ImGui::Text(tr("Tile %d (0x%03X)"), tile_id, tile_id);
617 ImGui::TextDisabled("Scratch: (%d, %d)", x, y);
618 const int usage = GetTile16UsageCount(tile_id);
619 if (usage > 0) {
620 ImGui::Text(tr("Placed %d times (%s)"), usage,
621 GetTile16ScopeLabel().c_str());
622 }
624 const int cols = std::max(1, tile16_blockset_.atlas.width() / 16);
625 const int tile_col = tile_id % cols;
626 const int tile_row = tile_id / cols;
627 const float atlas_w =
628 static_cast<float>(tile16_blockset_.atlas.width());
629 const float atlas_h =
630 static_cast<float>(tile16_blockset_.atlas.height());
631 if (atlas_w > 0 && atlas_h > 0) {
632 const float uv0_x = (tile_col * 16.0f) / atlas_w;
633 const float uv0_y = (tile_row * 16.0f) / atlas_h;
634 const float uv1_x = ((tile_col + 1) * 16.0f) / atlas_w;
635 const float uv1_y = ((tile_row + 1) * 16.0f) / atlas_h;
636 ImGui::Image(
637 (ImTextureID)(intptr_t)tile16_blockset_.atlas.texture(),
638 ImVec2(64.0f, 64.0f), ImVec2(uv0_x, uv0_y),
639 ImVec2(uv1_x, uv1_y));
640 }
641 }
642 ImGui::EndTooltip();
643 }
644 }
645 }
646 }
647 }
648 gui::EndCanvas(scratch_canvas_, canvas, options);
649 EndChild();
650 if (!IsMouseDown(ImGuiMouseButton_Left)) {
651 FinishScratchEdit("Paint scratch layout");
652 if (scratch_save_status_.ok())
654 }
655 if (!scratch_action_status_.ok())
656 TextWrapped("Scratch action: %s",
657 std::string(scratch_action_status_.message()).c_str());
658 if (!scratch_save_status_.ok()) {
659 TextWrapped("Unsaved scratch layouts: %s",
660 std::string(scratch_save_status_.message()).c_str());
661 if (Button(tr("Retry scratch save")))
663 }
664 return absl::OkStatus();
665}
666
668 const auto cursor = ScratchMousePosition(scratch_canvas_);
669 const int x = static_cast<int>(std::floor(cursor.x / 16.0f));
670 const int y = static_cast<int>(std::floor(cursor.y / 16.0f));
671 auto& page = scratch_workspace_.current();
672 if (x < 0 || y < 0 || x >= page.width || y >= page.height ||
673 current_tile16_ < 0 || current_tile16_ > 0xFFFF)
674 return;
675 if (page.in_use && page.tile_data[x][y] == current_tile16_)
676 return;
677 if (scratch_workspace_.Stamp({1, 1, {current_tile16_}}, x, y).ok()) {
678 MarkScratchSpaceDirty();
679 UpdateScratchBitmapTile(x, y, current_tile16_);
680 }
681}
682
683void OverworldEditor::DrawScratchSpacePattern() {
684 if (!dependencies_.shared_clipboard ||
685 !dependencies_.shared_clipboard->has_overworld_tile16)
686 return;
687 const auto& clipboard = *dependencies_.shared_clipboard;
688 const auto cursor = ScratchMousePosition(scratch_canvas_);
689 BeginScratchEdit();
690 const auto status = scratch_workspace_.Stamp(
691 {clipboard.overworld_width, clipboard.overworld_height,
692 clipboard.overworld_tile16_ids},
693 static_cast<int>(std::floor(cursor.x / 16.0f)),
694 static_cast<int>(std::floor(cursor.y / 16.0f)));
695 FinishScratchEdit("Stamp scratch layout");
696 if (status.ok())
697 (void)RebuildScratchBitmapFromTileData();
698 else
699 status_ = status;
700}
701
702void OverworldEditor::UpdateScratchBitmapTile(int tile_x, int tile_y,
703 int tile_id) {
704 const auto tile = gfx::GetTilemapData(tile16_blockset_, tile_id);
705 if (tile.size() != 16 * 16)
706 return;
707 for (int y = 0; y < 16; ++y)
708 for (int x = 0; x < 16; ++x)
709 scratch_bitmap_.WriteToPixel(
710 (tile_y * 16 + y) * scratch_bitmap_.width() + tile_x * 16 + x,
711 tile[y * 16 + x]);
712 scratch_bitmap_.set_modified(true);
714 &scratch_bitmap_);
715}
716
717absl::Status OverworldEditor::SaveCurrentSelectionToScratch() {
718 const auto* brush =
719 tile_painting_ ? tile_painting_->selection_brush() : nullptr;
720 if (!brush)
721 return absl::FailedPreconditionError("Select an overworld rectangle first");
722 BeginScratchEdit();
723 const auto status = scratch_workspace_.Capture(*brush, current_map_);
724 FinishScratchEdit("Capture map selection in scratch layout");
725 RETURN_IF_ERROR(status);
726 RETURN_IF_ERROR(RebuildScratchBitmapFromTileData());
727 return FlushScratchPadIfDirty();
728}
729
730absl::Status OverworldEditor::LoadScratchToSelection() {
731 const auto& page = scratch_workspace_.current();
732 if (!page.in_use)
733 return absl::FailedPreconditionError("Scratch layout is empty");
734 const auto brush = page.AsBrush();
735 if (overworld_.is_loaded() &&
736 std::any_of(brush.tile_ids.begin(), brush.tile_ids.end(), [this](int id) {
737 return id >= static_cast<int>(overworld_.tiles16().size());
738 }))
739 return absl::FailedPreconditionError(
740 "Scratch layout references Tile16 IDs absent from this ROM");
741 SetSelectionBrush(brush);
742 return absl::OkStatus();
743}
744
745absl::Status OverworldEditor::ClearScratchSpace() {
746 BeginScratchEdit();
747 scratch_workspace_.Clear();
748 FinishScratchEdit("Clear scratch layout");
749 scratch_select_active_ = false;
750 ClearSelectionBrush();
751 RETURN_IF_ERROR(RebuildScratchBitmapFromTileData());
752 return FlushScratchPadIfDirty();
753}
754
755} // namespace yaze::editor
UndoManager undo_manager_
Definition editor.h:383
std::array< char, 129 > scratch_name_buffer_
void SetSelectionBrush(TileBrush brush)
std::optional< ScratchWorkspaceState > scratch_edit_before_
int GetTile16UsageCount(int tile_id) const
const TileBrush * selection_brush() const
void FinishScratchEdit(const std::string &description)
absl::Status SaveCurrentSelectionToScratch()
absl::Status RestoreScratchState(const ScratchWorkspaceState &state)
std::string GetTile16ScopeLabel() const
absl::Status RebuildScratchBitmapFromTileData()
std::filesystem::path scratch_path_
gui::AdaptiveSheetScaleMode scratch_scale_mode_
std::array< int, 2 > scratch_resize_dimensions_
absl::Status Load(const std::filesystem::path &path)
absl::Status Resize(int width, int height)
const ScratchWorkspaceState & state() const
absl::Status Stamp(const TileBrush &brush, int x, int y)
static constexpr size_t kMaxPages
void Restore(const ScratchWorkspaceState &state)
absl::Status Rename(absl::string_view name)
absl::Status AddPage(bool duplicate=false)
absl::Status Save(const std::filesystem::path &path)
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
void Push(std::unique_ptr< UndoAction > action)
ScratchWorkspaceUndoAction(ScratchWorkspaceState before, ScratchWorkspaceState after, Apply apply, std::string description)
std::function< absl::Status(const ScratchWorkspaceState &)> Apply
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
const SnesPalette & palette() const
Definition bitmap.h:393
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
TextureHandle texture() const
Definition bitmap.h:410
bool is_active() const
Definition bitmap.h:414
int height() const
Definition bitmap.h:399
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
int width() const
Definition bitmap.h:398
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1295
float GetGlobalScale() const
Definition canvas.h:392
auto zero_point() const
Definition canvas.h:364
auto scrolling() const
Definition canvas.h:366
void SetGlobalScale(float scale)
Definition canvas.h:394
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
#define ICON_MD_MORE_VERT
Definition icons.h:1243
#define ICON_MD_FILE_DOWNLOAD
Definition icons.h:744
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_CHEVRON_LEFT
Definition icons.h:405
#define ICON_MD_CHEVRON_RIGHT
Definition icons.h:406
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
Definition input.cc:30
ImVec2 ScratchMousePosition(const gui::Canvas &canvas)
uint64_t ScratchPreviewRevision(const ScratchPage &page, gfx::Tilemap &atlas, bool loaded)
absl::StatusOr< std::filesystem::path > GetScratchPadPath()
Editors are the view controllers for the application.
void DrawSheetScaleControl(const char *id, gui::AdaptiveSheetScaleMode *mode)
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:268
void EndCanvas(Canvas &canvas)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
AdaptiveSheetLayout ResolveAdaptiveSheetLayout(float available_width, int source_width, int source_height, AdaptiveSheetScaleMode mode=AdaptiveSheetScaleMode::kFit, float min_scale=0.35f, float max_scale=4.0f, float horizontal_chrome=24.0f)
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:315
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::array< std::array< int, 32 >, 32 > tile_data
std::vector< ScratchPage > pages
std::vector< int > tile_ids
Definition tile_brush.h:15
Tilemap structure for SNES tile-based graphics management.
Definition tilemap.h:118
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119