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 // Header with clear button
159 Text(ICON_MD_BRUSH " Scratch Workspace");
160 SameLine();
161 if (Button(tr("Clear"))) {
163 }
164 HOVER_HINT("Clear scratch workspace");
165
166 // Status info
167 Text(tr("%s (%dx%d)"), scratch_space_.name.c_str(), scratch_space_.width,
169
170 // Interaction hints
172 !ow_map_canvas_.selected_tiles().empty()) {
173 TextColored(
174 ImVec4(0.4f, 1.0f, 0.4f, 1.0f), ICON_MD_CONTENT_PASTE
175 " Overworld selection active! Click in scratch space to stamp.");
176 } else {
177 Text(tr("Left-click to paint with current tile."));
178 Text(tr("Right-click to select tiles."));
179 }
180
181 // Initialize scratch bitmap if needed
183 int bitmap_width = scratch_space_.width * 16;
184 int bitmap_height = scratch_space_.height * 16;
185 std::vector<uint8_t> empty_data(bitmap_width * bitmap_height, 0);
186 scratch_space_.scratch_bitmap.Create(bitmap_width, bitmap_height, 8,
187 empty_data);
188 if (all_gfx_loaded_) {
194 }
195 }
196
197 // Draw the scratch space canvas using modern BeginCanvas/EndCanvas pattern
199 ImGui::BeginGroup();
200
201 ImVec2 scratch_content_size(scratch_space_.width * 16 + 4,
202 scratch_space_.height * 16 + 4);
203
204 // Configure canvas frame options
205 gui::CanvasFrameOptions frame_opts;
206 frame_opts.canvas_size = scratch_content_size;
207 frame_opts.draw_grid = true;
208 frame_opts.grid_step = 32.0f; // Tile16 grid (32px = 2x tile scale)
209 frame_opts.draw_context_menu = false; // No context menu for scratch
210 frame_opts.draw_overlay = true;
211 frame_opts.render_popups = false;
212 frame_opts.use_child_window = false;
213
214 gui::BeginChildWithScrollbar("##ScratchSpaceScrollRegion",
215 scratch_content_size);
216
217 auto canvas_rt = gui::BeginCanvas(scratch_canvas_, frame_opts);
219
222 }
223
226 }
227
228 // Handle Interactions using runtime hover state
229 if (canvas_rt.hovered) {
231 !ow_map_canvas_.selected_tiles().empty()) {
232 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
234 }
235 } else if (current_mode == EditingMode::DRAW_TILE ||
236 ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
238 }
239 }
240
241 gui::EndCanvas(scratch_canvas_, canvas_rt, frame_opts);
242 EndChild();
243 ImGui::EndGroup();
244
245 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
247 }
248
249 return absl::OkStatus();
250}
251
253 auto mouse_position = scratch_canvas_.drawn_tile_position();
254 int grid_size = 32;
255
256 int tile_x = static_cast<int>(mouse_position.x) / grid_size;
257 int tile_y = static_cast<int>(mouse_position.y) / grid_size;
258
259 int max_width = scratch_space_.width > 0 ? scratch_space_.width : 20;
260 int max_height = scratch_space_.height > 0 ? scratch_space_.height : 30;
261
262 if (tile_x >= 0 && tile_x < max_width && tile_y >= 0 && tile_y < max_height) {
263 bool changed = false;
264 if (tile_x < 32 && tile_y < 32) {
265 changed = scratch_space_.tile_data[tile_x][tile_y] != current_tile16_;
266 scratch_space_.tile_data[tile_x][tile_y] = current_tile16_;
267 }
268
269 if (changed) {
271 }
272
273 if (!scratch_space_.in_use) {
274 scratch_space_.in_use = true;
275 scratch_space_.name = "Modified Layout";
276 changed = true;
278 }
279 if (changed) {
281 }
282 }
283}
284
286 auto mouse_position = scratch_canvas_.drawn_tile_position();
287
288 int start_tile_x = static_cast<int>(mouse_position.x) / 32;
289 int start_tile_y = static_cast<int>(mouse_position.y) / 32;
290
293 return;
294 }
295
298 int pattern_height = dependencies_.shared_clipboard->overworld_height;
299
300 if (tile_ids.empty())
301 return;
302
303 int max_width = scratch_space_.width > 0 ? scratch_space_.width : 20;
304 int max_height = scratch_space_.height > 0 ? scratch_space_.height : 30;
305
306 int idx = 0;
307 for (int py = 0; py < pattern_height && (start_tile_y + py) < max_height;
308 ++py) {
309 for (int px = 0; px < pattern_width && (start_tile_x + px) < max_width;
310 ++px) {
311 if (idx < static_cast<int>(tile_ids.size())) {
312 int tile_id = tile_ids[idx];
313 int scratch_x = start_tile_x + px;
314 int scratch_y = start_tile_y + py;
315
316 if (scratch_x >= 0 && scratch_x < 32 && scratch_y >= 0 &&
317 scratch_y < 32) {
318 scratch_space_.tile_data[scratch_x][scratch_y] = tile_id;
319 UpdateScratchBitmapTile(scratch_x, scratch_y, tile_id);
320 }
321 idx++;
322 }
323 }
324 }
325
326 scratch_space_.in_use = true;
327 if (scratch_space_.name == "Scratch Space") {
329 absl::StrFormat("Pattern %dx%d", pattern_width, pattern_height);
330 }
332}
333
335 int tile_id) {
336 gfx::ScopedTimer timer("overworld_update_scratch_tile");
337
338 auto tile_data = gfx::GetTilemapData(tile16_blockset_, tile_id);
339 if (tile_data.empty())
340 return;
341
342 const int grid_size = 32;
343 int scratch_bitmap_width = scratch_space_.scratch_bitmap.width();
344 int scratch_bitmap_height = scratch_space_.scratch_bitmap.height();
345
346 for (int y = 0; y < 16; ++y) {
347 for (int x = 0; x < 16; ++x) {
348 int src_index = y * 16 + x;
349
350 int dst_x = tile_x * grid_size + x + x;
351 int dst_y = tile_y * grid_size + y + y;
352
353 if (dst_x >= 0 && dst_x < scratch_bitmap_width && dst_y >= 0 &&
354 dst_y < scratch_bitmap_height &&
355 src_index < static_cast<int>(tile_data.size())) {
356 for (int py = 0; py < 2 && (dst_y + py) < scratch_bitmap_height; ++py) {
357 for (int px = 0; px < 2 && (dst_x + px) < scratch_bitmap_width;
358 ++px) {
359 int dst_index = (dst_y + py) * scratch_bitmap_width + (dst_x + px);
361 tile_data[src_index]);
362 }
363 }
364 }
365 }
366 }
367
371 scratch_space_.in_use = true;
372}
373
375 gfx::ScopedTimer timer("overworld_save_selection_to_scratch");
376
378 !ow_map_canvas_.selected_tiles().empty()) {
379 const auto& selected_points = ow_map_canvas_.selected_points();
380 if (selected_points.size() >= 2) {
381 // selected_points are now stored in world coordinates
382 const auto start = selected_points[0];
383 const auto end = selected_points[1];
384
385 int selection_width =
386 std::abs(static_cast<int>((end.x - start.x) / 16)) + 1;
387 int selection_height =
388 std::abs(static_cast<int>((end.y - start.y) / 16)) + 1;
389
390 scratch_space_.width = std::max(1, std::min(selection_width, 32));
391 scratch_space_.height = std::max(1, std::min(selection_height, 32));
392 scratch_space_.in_use = true;
393 scratch_space_.name = absl::StrFormat(
394 "Selection %dx%d", scratch_space_.width, scratch_space_.height);
395
396 int bitmap_width = scratch_space_.width * 16;
397 int bitmap_height = scratch_space_.height * 16;
398 std::vector<uint8_t> empty_data(bitmap_width * bitmap_height, 0);
399 scratch_space_.scratch_bitmap.Create(bitmap_width, bitmap_height, 8,
400 empty_data);
401 if (all_gfx_loaded_) {
407 }
408
411
412 int idx = 0;
413 for (int y = 0;
415 idx < static_cast<int>(ow_map_canvas_.selected_tiles().size());
416 ++y) {
417 for (int x = 0;
418 x < scratch_space_.width &&
419 idx < static_cast<int>(ow_map_canvas_.selected_tiles().size());
420 ++x) {
421 if (idx < static_cast<int>(ow_map_canvas_.selected_tiles().size())) {
422 int tile_id = overworld_.GetTileFromPosition(
424 if (x < 32 && y < 32) {
425 scratch_space_.tile_data[x][y] = tile_id;
426 }
427 UpdateScratchBitmapTile(x, y, tile_id);
428 idx++;
429 }
430 }
431 }
432 }
433 } else {
436 scratch_space_.name = absl::StrFormat("Map %d Area", current_map_);
437 scratch_space_.in_use = true;
438 }
439
442 return absl::OkStatus();
443}
444
446 if (!scratch_space_.in_use) {
447 return absl::FailedPreconditionError("Scratch space is empty");
448 }
449
450 util::logf("Loading scratch space: %s", scratch_space_.name.c_str());
451
452 return absl::OkStatus();
453}
454
456 scratch_space_.in_use = false;
457 scratch_space_.name = "Scratch Space";
458
459 // Clear tile data
460 for (auto& row : scratch_space_.tile_data) {
461 row.fill(0);
462 }
463
464 // Clear the bitmap
467 std::fill(data.begin(), data.end(), 0);
471 }
472
474 return FlushScratchPadIfDirty();
475}
476
477} // namespace yaze::editor
EditorDependencies dependencies_
Definition editor.h:333
void UpdateScratchBitmapTile(int tile_x, int tile_y, int tile_id)
absl::Status SaveCurrentSelectionToScratch()
absl::Status RebuildScratchBitmapFromTileData()
absl::Status SaveScratchPad() const
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
static Arena & Get()
Definition arena.cc:21
void WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:581
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:201
bool is_active() const
Definition bitmap.h:405
void set_modified(bool modified)
Definition bitmap.h:409
int height() const
Definition bitmap.h:395
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
int width() const
Definition bitmap.h:394
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:399
RAII timer for automatic timing management.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1162
auto select_rect_active() const
Definition canvas.h:393
auto selected_tiles() const
Definition canvas.h:394
auto drawn_tile_position() const
Definition canvas.h:356
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1098
auto selected_points() const
Definition canvas.h:458
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
void set_current_world(int world)
Definition overworld.h:735
int GetTileFromPosition(ImVec2 position) const
Definition overworld.h:636
const gfx::SnesPalette & current_area_palette() const
Definition overworld.h:705
void set_current_map(int i)
Definition overworld.h:734
#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:23
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:277
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
void EndPadding()
Definition style.cc:281
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:293
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
SharedClipboard * shared_clipboard
Definition editor.h:186
std::array< std::array< int, 32 >, 32 > tile_data
std::vector< int > overworld_tile16_ids
Definition editor.h:119
std::optional< float > grid_step