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 <filesystem>
4#include <fstream>
5#include <vector>
6#include "util/i18n/tr.h"
7
8#include "absl/status/status.h"
9#include "absl/status/statusor.h"
10#include "absl/strings/str_format.h"
12#include "app/gfx/core/bitmap.h"
17#include "app/gui/core/icons.h"
18#include "app/gui/core/style.h"
19#include "imgui/imgui.h"
20#include "nlohmann/json.hpp"
21#include "util/log.h"
22#include "util/macro.h"
23#include "util/platform_paths.h"
25
26namespace yaze::editor {
27
28using namespace ImGui;
29
30namespace {
31
32constexpr int kScratchPadVersion = 1;
33constexpr const char* kScratchPadFileName = "ScratchPad.dat";
34
35absl::StatusOr<std::filesystem::path> GetScratchPadPath() {
37 return config_dir / kScratchPadFileName;
38}
39
40} // namespace
41
42// =============================================================================
43// Scratch Space - Unified Single Workspace
44// =============================================================================
45
47 ASSIGN_OR_RETURN(auto path, GetScratchPadPath());
48 if (!std::filesystem::exists(path)) {
49 return absl::OkStatus();
50 }
51
52 std::ifstream input(path);
53 if (!input.is_open()) {
54 return absl::InternalError(
55 absl::StrFormat("Could not open %s", path.string()));
56 }
57
58 auto root = nlohmann::json::parse(input, nullptr, false);
59 if (root.is_discarded() || !root.is_object()) {
60 return absl::DataLossError(
61 absl::StrFormat("Invalid scratch pad JSON in %s", path.string()));
62 }
63
64 scratch_space_.width = std::clamp(root.value("width", 16), 1, 32);
65 scratch_space_.height = std::clamp(root.value("height", 16), 1, 32);
66 scratch_space_.name = root.value("name", std::string("Scratch Space"));
67 scratch_space_.in_use = root.value("in_use", false);
68
69 for (auto& row : scratch_space_.tile_data) {
70 row.fill(0);
71 }
72
73 if (root.contains("tiles") && root["tiles"].is_array()) {
74 const auto& rows = root["tiles"];
75 for (size_t y = 0; y < rows.size() && y < 32; ++y) {
76 if (!rows[y].is_array()) {
77 continue;
78 }
79 const auto& row = rows[y];
80 for (size_t x = 0; x < row.size() && x < 32; ++x) {
81 scratch_space_.tile_data[x][y] = row[x].get<int>();
82 }
83 }
84 }
85
88}
89
90absl::Status OverworldEditor::SaveScratchPad() const {
91 ASSIGN_OR_RETURN(auto path, GetScratchPadPath());
92
93 nlohmann::json root;
94 root["version"] = kScratchPadVersion;
95 root["name"] = scratch_space_.name;
96 root["in_use"] = scratch_space_.in_use;
97 root["width"] = std::clamp(scratch_space_.width, 1, 32);
98 root["height"] = std::clamp(scratch_space_.height, 1, 32);
99 root["tiles"] = nlohmann::json::array();
100
101 for (int y = 0; y < root["height"].get<int>(); ++y) {
102 nlohmann::json row = nlohmann::json::array();
103 for (int x = 0; x < root["width"].get<int>(); ++x) {
104 row.push_back(scratch_space_.tile_data[x][y]);
105 }
106 root["tiles"].push_back(std::move(row));
107 }
108
109 std::ofstream output(path, std::ios::trunc);
110 if (!output.is_open()) {
111 return absl::InternalError(
112 absl::StrFormat("Could not write %s", path.string()));
113 }
114 output << root.dump(2) << '\n';
115 return absl::OkStatus();
116}
117
120 return absl::OkStatus();
121 }
123 scratch_space_dirty_ = false;
124 return absl::OkStatus();
125}
126
130
132 const int bitmap_width = scratch_space_.width * 16;
133 const int bitmap_height = scratch_space_.height * 16;
134 std::vector<uint8_t> empty_data(bitmap_width * bitmap_height, 0);
135 const bool was_in_use = scratch_space_.in_use;
136
137 scratch_space_.scratch_bitmap.Create(bitmap_width, bitmap_height, 8,
138 empty_data);
139 if (all_gfx_loaded_) {
144 }
145
146 if (map_blockset_loaded_ && was_in_use) {
147 for (int y = 0; y < scratch_space_.height && y < 32; ++y) {
148 for (int x = 0; x < scratch_space_.width && x < 32; ++x) {
150 }
151 }
152 }
153 scratch_space_.in_use = was_in_use;
154 return absl::OkStatus();
155}
156
158 if (is_lyra_preview()) {
159 ImGui::TextDisabled("Lyra preview: scratch editing unavailable");
160 return absl::OkStatus();
161 }
162 // Header with clear button
163 Text(ICON_MD_BRUSH " Scratch Workspace");
164 SameLine();
165 if (Button(tr("Clear"))) {
167 }
168 HOVER_HINT("Clear scratch workspace");
169
170 // Status info
171 Text(tr("%s (%dx%d)"), scratch_space_.name.c_str(), scratch_space_.width,
173
174 // Interaction hints
176 !ow_map_canvas_.selected_tiles().empty()) {
177 TextColored(
178 ImVec4(0.4f, 1.0f, 0.4f, 1.0f), ICON_MD_CONTENT_PASTE
179 " Overworld selection active! Click in scratch space to stamp.");
180 } else {
181 Text(tr("Left-click to paint with current tile."));
182 Text(tr("Right-click to select tiles."));
183 }
184
185 // Initialize scratch bitmap if needed
187 int bitmap_width = scratch_space_.width * 16;
188 int bitmap_height = scratch_space_.height * 16;
189 std::vector<uint8_t> empty_data(bitmap_width * bitmap_height, 0);
190 scratch_space_.scratch_bitmap.Create(bitmap_width, bitmap_height, 8,
191 empty_data);
192 if (all_gfx_loaded_) {
198 }
199 }
200
201 // Draw the scratch space canvas using modern BeginCanvas/EndCanvas pattern
203 ImGui::BeginGroup();
204
205 ImVec2 scratch_content_size(scratch_space_.width * 16 + 4,
206 scratch_space_.height * 16 + 4);
207
208 // Configure canvas frame options
209 gui::CanvasFrameOptions frame_opts;
210 frame_opts.canvas_size = scratch_content_size;
211 frame_opts.draw_grid = true;
212 frame_opts.grid_step = 32.0f; // Tile16 grid (32px = 2x tile scale)
213 frame_opts.draw_context_menu = false; // No context menu for scratch
214 frame_opts.draw_overlay = true;
215 frame_opts.render_popups = false;
216 frame_opts.use_child_window = false;
217
218 gui::BeginChildWithScrollbar("##ScratchSpaceScrollRegion",
219 scratch_content_size);
220
221 auto canvas_rt = gui::BeginCanvas(scratch_canvas_, frame_opts);
223
226 }
227
230 }
231
232 // Handle Interactions using runtime hover state
233 if (canvas_rt.hovered) {
235 !ow_map_canvas_.selected_tiles().empty()) {
236 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
238 }
239 } else if (current_mode == EditingMode::DRAW_TILE ||
240 ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
242 }
243 }
244
245 gui::EndCanvas(scratch_canvas_, canvas_rt, frame_opts);
246 EndChild();
247 ImGui::EndGroup();
248
249 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
251 }
252
253 return absl::OkStatus();
254}
255
257 auto mouse_position = scratch_canvas_.drawn_tile_position();
258 int grid_size = 32;
259
260 int tile_x = static_cast<int>(mouse_position.x) / grid_size;
261 int tile_y = static_cast<int>(mouse_position.y) / grid_size;
262
263 int max_width = scratch_space_.width > 0 ? scratch_space_.width : 20;
264 int max_height = scratch_space_.height > 0 ? scratch_space_.height : 30;
265
266 if (tile_x >= 0 && tile_x < max_width && tile_y >= 0 && tile_y < max_height) {
267 bool changed = false;
268 if (tile_x < 32 && tile_y < 32) {
269 changed = scratch_space_.tile_data[tile_x][tile_y] != current_tile16_;
270 scratch_space_.tile_data[tile_x][tile_y] = current_tile16_;
271 }
272
273 if (changed) {
275 }
276
277 if (!scratch_space_.in_use) {
278 scratch_space_.in_use = true;
279 scratch_space_.name = "Modified Layout";
280 changed = true;
282 }
283 if (changed) {
285 }
286 }
287}
288
290 auto mouse_position = scratch_canvas_.drawn_tile_position();
291
292 int start_tile_x = static_cast<int>(mouse_position.x) / 32;
293 int start_tile_y = static_cast<int>(mouse_position.y) / 32;
294
297 return;
298 }
299
302 int pattern_height = dependencies_.shared_clipboard->overworld_height;
303
304 if (tile_ids.empty())
305 return;
306
307 int max_width = scratch_space_.width > 0 ? scratch_space_.width : 20;
308 int max_height = scratch_space_.height > 0 ? scratch_space_.height : 30;
309
310 int idx = 0;
311 for (int py = 0; py < pattern_height && (start_tile_y + py) < max_height;
312 ++py) {
313 for (int px = 0; px < pattern_width && (start_tile_x + px) < max_width;
314 ++px) {
315 if (idx < static_cast<int>(tile_ids.size())) {
316 int tile_id = tile_ids[idx];
317 int scratch_x = start_tile_x + px;
318 int scratch_y = start_tile_y + py;
319
320 if (scratch_x >= 0 && scratch_x < 32 && scratch_y >= 0 &&
321 scratch_y < 32) {
322 scratch_space_.tile_data[scratch_x][scratch_y] = tile_id;
323 UpdateScratchBitmapTile(scratch_x, scratch_y, tile_id);
324 }
325 idx++;
326 }
327 }
328 }
329
330 scratch_space_.in_use = true;
331 if (scratch_space_.name == "Scratch Space") {
333 absl::StrFormat("Pattern %dx%d", pattern_width, pattern_height);
334 }
336}
337
339 int tile_id) {
340 gfx::ScopedTimer timer("overworld_update_scratch_tile");
341
342 auto tile_data = gfx::GetTilemapData(tile16_blockset_, tile_id);
343 if (tile_data.empty())
344 return;
345
346 const int grid_size = 32;
347 int scratch_bitmap_width = scratch_space_.scratch_bitmap.width();
348 int scratch_bitmap_height = scratch_space_.scratch_bitmap.height();
349
350 for (int y = 0; y < 16; ++y) {
351 for (int x = 0; x < 16; ++x) {
352 int src_index = y * 16 + x;
353
354 int dst_x = tile_x * grid_size + x + x;
355 int dst_y = tile_y * grid_size + y + y;
356
357 if (dst_x >= 0 && dst_x < scratch_bitmap_width && dst_y >= 0 &&
358 dst_y < scratch_bitmap_height &&
359 src_index < static_cast<int>(tile_data.size())) {
360 for (int py = 0; py < 2 && (dst_y + py) < scratch_bitmap_height; ++py) {
361 for (int px = 0; px < 2 && (dst_x + px) < scratch_bitmap_width;
362 ++px) {
363 int dst_index = (dst_y + py) * scratch_bitmap_width + (dst_x + px);
365 tile_data[src_index]);
366 }
367 }
368 }
369 }
370 }
371
375 scratch_space_.in_use = true;
376}
377
379 gfx::ScopedTimer timer("overworld_save_selection_to_scratch");
380
381 const auto* brush =
382 tile_painting_ ? tile_painting_->selection_brush() : nullptr;
383 if (brush) {
384 scratch_space_.width = std::min(brush->width, 32);
385 scratch_space_.height = std::min(brush->height, 32);
386 scratch_space_.in_use = true;
387 scratch_space_.name = absl::StrFormat(
388 "Selection %dx%d", scratch_space_.width, scratch_space_.height);
389
390 const int bitmap_width = scratch_space_.width * 16;
391 const int bitmap_height = scratch_space_.height * 16;
393 bitmap_width, bitmap_height, 8,
394 std::vector<uint8_t>(bitmap_width * bitmap_height, 0));
395 if (all_gfx_loaded_) {
401 }
402 for (int y = 0; y < scratch_space_.height; ++y) {
403 for (int x = 0; x < scratch_space_.width; ++x) {
404 const int tile_id = brush->at(x, y);
405 scratch_space_.tile_data[x][y] = tile_id;
406 UpdateScratchBitmapTile(x, y, tile_id);
407 }
408 }
409 } else {
412 scratch_space_.name = absl::StrFormat("Map %d Area", current_map_);
413 scratch_space_.in_use = true;
414 }
415
418 return absl::OkStatus();
419}
420
422 if (!scratch_space_.in_use) {
423 return absl::FailedPreconditionError("Scratch space is empty");
424 }
425
426 util::logf("Loading scratch space: %s", scratch_space_.name.c_str());
427
428 return absl::OkStatus();
429}
430
432 scratch_space_.in_use = false;
433 scratch_space_.name = "Scratch Space";
434
435 // Clear tile data
436 for (auto& row : scratch_space_.tile_data) {
437 row.fill(0);
438 }
439
440 // Clear the bitmap
443 std::fill(data.begin(), data.end(), 0);
447 }
448
450 return FlushScratchPadIfDirty();
451}
452
453} // namespace yaze::editor
EditorDependencies dependencies_
Definition editor.h:370
void UpdateScratchBitmapTile(int tile_x, int tile_y, int tile_id)
absl::Status SaveCurrentSelectionToScratch()
absl::Status RebuildScratchBitmapFromTileData()
std::unique_ptr< TilePaintingManager > tile_painting_
absl::Status SaveScratchPad() const
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
void WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:649
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
bool is_active() const
Definition bitmap.h:414
void set_modified(bool modified)
Definition bitmap.h:418
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
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:408
RAII timer for automatic timing management.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1293
auto select_rect_active() const
Definition canvas.h:409
auto selected_tiles() const
Definition canvas.h:410
auto drawn_tile_position() const
Definition canvas.h:372
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1229
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
const gfx::SnesPalette & current_area_palette() const
Definition overworld.h:780
#define ICON_MD_BRUSH
Definition icons.h:325
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
#define HOVER_HINT(string)
Definition macro.h:24
Definition input.cc:30
absl::StatusOr< std::filesystem::path > GetScratchPadPath()
Editors are the view controllers for the application.
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:268
void EndCanvas(Canvas &canvas)
void BeginPadding(int i)
Definition style.cc:296
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
void EndPadding()
Definition style.cc:300
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:312
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:146
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
SharedClipboard * shared_clipboard
Definition editor.h:187
std::array< std::array< int, 32 >, 32 > tile_data
std::vector< int > overworld_tile16_ids
Definition editor.h:120
std::optional< float > grid_step