yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
catalog_tile_browser.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <cstdlib>
7#include <iterator>
8#include <optional>
9#include <string>
10#include <utility>
11
18#include "app/gui/core/style.h"
21#include "imgui/imgui.h"
22#include "imgui/misc/cpp/imgui_stdlib.h"
23#include "util/file_util.h"
24#include "util/macro.h"
25#include "zelda3/game_data.h"
27
28namespace yaze::editor {
29namespace {
30using Colors = std::array<ImU32, 16>;
31
32bool SameRect(const CatalogArtRect& a, const CatalogArtRect& b) {
33 return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height;
34}
35
37 const CatalogTileBrowserState& browser) {
38 return browser.source >= 0 && size_t(browser.source) < browser.sources.size()
39 ? &browser.sources[browser.source]
40 : nullptr;
41}
42
44 const auto* source = SelectedSource(browser);
45 if (!source)
46 return absl::FailedPreconditionError("Select a catalog asset");
47 const auto id = source->asset_id.empty() ? source->id : source->asset_id;
48 if (browser.asset_preview && browser.asset_cache_id == id)
49 return absl::OkStatus();
50 ASSIGN_OR_RETURN(auto image, AssembleCatalogAsset(browser.sources, id));
52 browser.asset_cache_id = id;
53 browser.asset_preview = std::move(image);
54 browser.art_selection = {0, 0, browser.asset_preview->width,
55 browser.asset_preview->height};
56 browser.art_drag_x = browser.art_drag_y = -1;
57 return absl::OkStatus();
58}
59
61 browser.source = source;
62 browser.selected.clear();
63 browser.last_source_tile = browser.drag_start = -1;
64 browser.pending.reset();
65 browser.asset_cache_id.clear();
66 browser.asset_preview.reset();
68 if (SelectedSource(browser)) {
69 const auto status = EnsureAssetPreview(browser);
70 if (!status.ok())
71 browser.status = std::string(status.message());
72 }
73}
74
76 const GraphicsEditContext& b) {
77 if (a.owner != b.owner || a.resolved.surface != b.resolved.surface ||
78 a.resolved.id != b.resolved.id ||
82 a.targets.size() != b.targets.size() ||
83 a.pattern_cells.size() != b.pattern_cells.size())
84 return false;
85 for (size_t i = 0; i < a.targets.size(); ++i)
86 if (a.targets[i].sheet_id != b.targets[i].sheet_id ||
87 a.targets[i].tile_index != b.targets[i].tile_index)
88 return false;
89 for (size_t i = 0; i < a.pattern_cells.size(); ++i) {
90 const auto& x = a.pattern_cells[i];
91 const auto& y = b.pattern_cells[i];
92 if (x.x != y.x || x.y != y.y || x.target != y.target ||
93 x.hflip != y.hflip || x.vflip != y.vflip)
94 return false;
95 }
96 return true;
97}
98
99bool Matches(std::string value, std::string filter) {
100 const auto lower = [](unsigned char c) {
101 return std::tolower(c);
102 };
103 std::transform(value.begin(), value.end(), value.begin(), lower);
104 std::transform(filter.begin(), filter.end(), filter.begin(), lower);
105 return value.find(filter) != std::string::npos;
106}
107
108// Draw from the immutable source or owning store; never from the global Arena.
109int DrawSheet(const char* id, const std::vector<uint8_t>& pixels,
110 const Colors& colors, const std::set<int>& selected = {},
111 int* hovered_tile = nullptr) {
112 if (pixels.size() != 4096)
113 return -1;
114 const float scale =
115 std::clamp(ImGui::GetContentRegionAvail().x / 128.f, 2.f, 5.f);
116 const ImVec2 origin = ImGui::GetCursorScreenPos();
117 ImGui::InvisibleButton(id, ImVec2(128 * scale, 32 * scale));
118 gui::AutoRegisterLastItem("canvas", id);
119 auto* draw = ImGui::GetWindowDrawList();
120 for (int y = 0; y < 32; ++y) {
121 for (int x = 0; x < 128; ++x) {
122 const uint8_t index = pixels[y * 128 + x];
123 const ImU32 color =
124 index == 0 ? (((x / 4 + y / 4) % 2) ? IM_COL32(60, 60, 60, 255)
125 : IM_COL32(38, 38, 38, 255))
126 : colors[index & 15];
127 draw->AddRectFilled(
128 ImVec2(origin.x + x * scale, origin.y + y * scale),
129 ImVec2(origin.x + (x + 1) * scale, origin.y + (y + 1) * scale),
130 color);
131 }
132 }
133 for (int tile : selected) {
134 const ImVec2 a(origin.x + (tile % 16) * 8 * scale,
135 origin.y + (tile / 16) * 8 * scale);
136 draw->AddRect(a, ImVec2(a.x + 8 * scale, a.y + 8 * scale),
137 IM_COL32(255, 210, 55, 255), 0.f, 0, 2.f);
138 }
139 if (ImGui::IsItemHovered()) {
140 const auto mouse = ImGui::GetIO().MousePos;
141 const int x = std::clamp(int((mouse.x - origin.x) / (8 * scale)), 0, 15);
142 const int y = std::clamp(int((mouse.y - origin.y) / (8 * scale)), 0, 3);
143 if (hovered_tile)
144 *hovered_tile = y * 16 + x;
145 ImGui::SetTooltip("Tile $%02X", y * 16 + x);
146 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
147 return y * 16 + x;
148 }
149 return -1;
150}
151void DrawLogicalAsset(CatalogTileBrowserState& browser, const Colors& colors) {
152 if (!browser.asset_preview)
153 return;
154 const auto& image = *browser.asset_preview;
155 ImGui::Text("Complete asset: %d x %d pixels; %d bpp", image.width,
156 image.height, image.bpp);
157 ImGui::TextUnformatted(
158 "Drag an 8-pixel rectangle, or select the whole asset.");
159 const float scale =
160 std::clamp(ImGui::GetContentRegionAvail().x / image.width, 1.f, 4.f);
161 ImGui::BeginChild("CatalogLogicalAsset",
162 ImVec2(0, std::min(330.f, image.height * scale + 16)),
163 ImGuiChildFlags_Borders,
164 ImGuiWindowFlags_HorizontalScrollbar);
165 const auto origin = ImGui::GetCursorScreenPos();
166 ImGui::InvisibleButton("Logical asset pixels",
167 ImVec2(image.width * scale, image.height * scale));
168 gui::AutoRegisterLastItem("canvas", "Logical asset pixels");
169 auto* draw = ImGui::GetWindowDrawList();
170 for (int y = 0; y < image.height; ++y) {
171 for (int x = 0; x < image.width;) {
172 const auto index = image.indexed_pixels[y * image.width + x];
173 const auto color_at = [&](int px) {
174 return index == 0 ? ((px / 4 + y / 4) % 2 ? IM_COL32(60, 60, 60, 255)
175 : IM_COL32(38, 38, 38, 255))
176 : colors[index & 15];
177 };
178 const auto color = color_at(x);
179 int end = x + 1;
180 while (end < image.width &&
181 image.indexed_pixels[y * image.width + end] == index &&
182 color_at(end) == color)
183 ++end;
184 draw->PrimReserve(6, 4);
185 draw->PrimRect(ImVec2(origin.x + x * scale, origin.y + y * scale),
186 ImVec2(origin.x + end * scale, origin.y + (y + 1) * scale),
187 color);
188 x = end;
189 }
190 }
191 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
192 const auto mouse = ImGui::GetMousePos();
193 browser.art_drag_x = std::clamp(int((mouse.x - origin.x) / (8 * scale)), 0,
194 image.width / 8 - 1);
195 browser.art_drag_y = std::clamp(int((mouse.y - origin.y) / (8 * scale)), 0,
196 image.height / 8 - 1);
197 }
198 if (browser.art_drag_x >= 0 && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
199 const auto mouse = ImGui::GetMousePos();
200 const int x = std::clamp(int((mouse.x - origin.x) / (8 * scale)), 0,
201 image.width / 8 - 1);
202 const int y = std::clamp(int((mouse.y - origin.y) / (8 * scale)), 0,
203 image.height / 8 - 1);
204 const CatalogArtRect rectangle{std::min(x, browser.art_drag_x) * 8,
205 std::min(y, browser.art_drag_y) * 8,
206 (std::abs(x - browser.art_drag_x) + 1) * 8,
207 (std::abs(y - browser.art_drag_y) + 1) * 8};
208 if (!SameRect(rectangle, browser.art_selection)) {
209 browser.art_selection = rectangle;
211 }
212 }
213 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left))
214 browser.art_drag_x = browser.art_drag_y = -1;
215 const auto& selected = browser.art_selection;
216 draw->AddRect(
217 ImVec2(origin.x + selected.x * scale, origin.y + selected.y * scale),
218 ImVec2(origin.x + (selected.x + selected.width) * scale,
219 origin.y + (selected.y + selected.height) * scale),
220 IM_COL32(255, 210, 55, 255), 0.0f, 2.0f, ImDrawFlags_None);
221 ImGui::EndChild();
222 int crop_x = selected.x / 8, crop_y = selected.y / 8;
223 int crop_width = selected.width / 8, crop_height = selected.height / 8;
224 const auto edit_crop = [](const char* label, int& value) {
225 ImGui::SetNextItemWidth(140);
226 // A scalar input retains its own item ID for keyboard focus and widget
227 // discovery. Step buttons wrap InputInt in a group whose final ID is zero.
228 const bool changed = ImGui::InputInt(label, &value, 0, 0,
229 ImGuiInputTextFlags_EnterReturnsTrue);
230 gui::AutoRegisterLastItem("input", label);
231 return changed;
232 };
233 ImGui::TextUnformatted("Crop in 8-pixel tiles; press Enter to commit.");
234 bool crop_changed = edit_crop("Crop X (tiles)", crop_x);
235 crop_changed |= edit_crop("Crop Y (tiles)", crop_y);
236 crop_changed |= edit_crop("Crop width (tiles)", crop_width);
237 crop_changed |= edit_crop("Crop height (tiles)", crop_height);
238 if (crop_changed) {
239 crop_x = std::clamp(crop_x, 0, image.width / 8 - 1);
240 crop_y = std::clamp(crop_y, 0, image.height / 8 - 1);
241 crop_width = std::clamp(crop_width, 1, image.width / 8 - crop_x);
242 crop_height = std::clamp(crop_height, 1, image.height / 8 - crop_y);
243 const CatalogArtRect rectangle{crop_x * 8, crop_y * 8, crop_width * 8,
244 crop_height * 8};
245 if (!SameRect(rectangle, browser.art_selection)) {
246 browser.art_selection = rectangle;
248 }
249 }
250 if (gui::AutoButton("Select whole asset")) {
251 browser.art_selection = {0, 0, image.width, image.height};
253 }
254 ImGui::SameLine();
255 ImGui::Text("Crop %d,%d: %d x %d pixels", selected.x, selected.y,
256 selected.width, selected.height);
257}
258
260 if (!browser.staged_bitmap.is_active())
261 return;
262 const float scale = std::clamp(
263 ImGui::GetContentRegionAvail().x / browser.staged_bitmap.width(), 0.25f,
264 2.f);
266 options.canvas_size = ImVec2(browser.staged_bitmap.width() * scale + 4,
267 browser.staged_bitmap.height() * scale + 4);
268 options.draw_grid = options.draw_context_menu = options.draw_overlay =
269 options.render_popups = options.use_child_window = false;
270 // The scrollbar helper takes content size and fills the remaining viewport.
271 // Bound the preview viewport so Apply/Cancel remain reachable below it.
272 const float preview_height =
273 std::clamp(options.canvas_size.y + ImGui::GetStyle().WindowPadding.y * 2,
274 64.f, 330.f);
275 ImGui::BeginChild("CatalogDetachedPreview", ImVec2(0, preview_height),
276 ImGuiChildFlags_Borders,
277 ImGuiWindowFlags_HorizontalScrollbar);
278 auto canvas = gui::BeginCanvas(browser.staged_canvas, options);
279 browser.staged_canvas.DrawBitmap(browser.staged_bitmap, 2, 2, scale);
280 gui::EndCanvas(browser.staged_canvas, canvas, options);
281 ImGui::EndChild();
282}
283
285 const char* label;
286 std::optional<CatalogResourceKind> kind;
288};
289
291 {"All resources", std::nullopt, false},
292 {"Overworld tile motifs", CatalogResourceKind::kOverworldTile, false},
293 {"Dungeon tile motifs", CatalogResourceKind::kDungeonTile, false},
294 {"Interior tile motifs", CatalogResourceKind::kInteriorTile, false},
295 {"Sprite family poses", CatalogResourceKind::kSpritePose, false},
296 {"Palettes", std::nullopt, true},
297 {"Other source sheets", CatalogResourceKind::kOtherSheet, false}};
298
300 const int count = static_cast<int>(std::size(kKindFilterOptions));
301 const auto& option =
302 kKindFilterOptions[std::clamp(browser.kind_filter, 0, count - 1)];
303 return {option.kind, option.palettes_only, browser.group_filter,
304 browser.category_filter, browser.search};
305}
306
308 const int count = static_cast<int>(std::size(kKindFilterOptions));
309 browser.kind_filter = std::clamp(browser.kind_filter, 0, count - 1);
310 const bool open = ImGui::BeginCombo(
311 "Resource kind", kKindFilterOptions[browser.kind_filter].label);
312 gui::AutoRegisterLastItem("combo", "Resource kind");
313 if (!open)
314 return;
315 for (int i = 0; i < count; ++i)
316 if (ImGui::Selectable(kKindFilterOptions[i].label,
317 browser.kind_filter == i))
318 browser.kind_filter = i;
319 ImGui::EndCombo();
320}
321
322ImU32 Bgr555Color(uint16_t color) {
323 return IM_COL32((color & 31) * 255 / 31, ((color >> 5) & 31) * 255 / 31,
324 ((color >> 10) & 31) * 255 / 31, 255);
325}
326
327void DrawPaletteSwatches(const std::vector<uint16_t>& palette) {
328 if (palette.empty())
329 return;
330 ImGui::Text("Declared palette: %zu colors (BGR555)", palette.size());
331 const float size = ImGui::GetFrameHeight();
332 auto* draw = ImGui::GetWindowDrawList();
333 const auto origin = ImGui::GetCursorScreenPos();
334 for (size_t i = 0; i < palette.size(); ++i) {
335 const ImVec2 a(origin.x + i * (size + 2), origin.y);
336 draw->AddRectFilled(a, ImVec2(a.x + size, a.y + size),
337 Bgr555Color(palette[i]));
338 if (i == 0)
339 draw->AddLine(a, ImVec2(a.x + size, a.y + size),
340 IM_COL32(200, 60, 60, 255), 2.f);
341 }
342 ImGui::Dummy(ImVec2(palette.size() * (size + 2), size));
343 if (ImGui::IsItemHovered())
344 ImGui::SetTooltip("ID 0 is transparent");
345}
346
348 const auto field = [](const char* label, const std::string& value) {
349 if (!value.empty())
350 ImGui::TextWrapped("%s: %s", label, value.c_str());
351 };
352 ImGui::TextWrapped("%s | %s | %s",
354 source.group.c_str(), source.category.c_str());
355 ImGui::Text("Bit depth: %dbpp (%d color IDs)%s", source.bpp, 1 << source.bpp,
356 source.resources_bound ? "" : " | unbound art");
357 field("Material", source.material);
358 field("Game", source.game);
359 field("Credit", source.credit);
360 field("Source SHA256", source.source_sha256);
361 field("Source origin", source.source_origin);
362 field("Source URL", source.source_url);
363 field("Conversion", source.conversion);
364 if (source.color_reduction)
365 ImGui::TextWrapped(
366 "Color reduction: source colors were reduced to fit this depth.");
367 field("Notes", source.notes);
368 field("Provenance", source.provenance_status);
369 field("Identification", source.identification_confidence);
370 field("Behavior", source.behavior_status);
371 ImGui::TextWrapped("Source: %s", source.source_path.c_str());
372 if (!source.binary_path.empty())
373 ImGui::TextWrapped("Planar %dbpp page verified: %s", source.bpp,
374 source.binary_path.c_str());
376}
377
378bool DrawMetadataFilter(const char* label, std::string& selected,
379 const std::set<std::string>& values) {
380 const bool open =
381 ImGui::BeginCombo(label, selected.empty() ? "All" : selected.c_str());
382 gui::AutoRegisterLastItem("combo", label);
383 bool changed = false;
384 if (open) {
385 if (ImGui::Selectable("All", selected.empty())) {
386 selected.clear();
387 changed = true;
388 }
389 for (const auto& value : values) {
390 if (ImGui::Selectable(value.c_str(), value == selected)) {
391 selected = value;
392 changed = true;
393 }
394 }
395 ImGui::EndCombo();
396 }
397 return changed;
398}
399} // namespace
400
404
406 browser.object_pending.reset();
407 browser.candidate_source_revision.reset();
408 browser.candidate_source_id.clear();
410}
411
413 const std::string& filename) {
414 auto sources = LoadCatalogSheetSources(filename);
415 if (!sources.ok()) {
416 browser.status = "Catalog load failed; previous library retained: " +
417 std::string(sources.status().message());
418 return sources.status();
419 }
420 std::vector<CatalogPreviewPalette> palettes;
421 auto loaded_palettes = LoadCatalogPreviewPalettes(filename);
422 if (loaded_palettes.ok())
423 palettes = std::move(*loaded_palettes);
424 browser.sources = std::move(*sources);
425 browser.palettes = std::move(palettes);
426 browser.path = browser.loaded_path = filename;
427 browser.pending.reset();
428 browser.palette =
429 browser.sources.empty() || browser.sources[0].palette.empty() ? -1 : -3;
430 ResetSourceSelection(browser, browser.sources.empty() ? -1 : 0);
431 browser.status = "Loaded " + std::to_string(browser.sources.size()) +
432 " verified source pages.";
433 if (!loaded_palettes.ok() &&
434 std::none_of(browser.sources.begin(), browser.sources.end(),
435 [](const auto& source) { return !source.palette.empty(); }))
436 browser.status += " Palette preview unavailable: " +
437 std::string(loaded_palettes.status().message());
438 return absl::OkStatus();
439}
440
441namespace {
444 bool resolve_pattern) {
445 if (!browser.object_pending)
446 return absl::OkStatus();
447 auto refuse = [&](const std::string& message) {
449 return absl::FailedPreconditionError(message);
450 };
451 const auto& plan = *browser.object_pending;
452 const auto* source = SelectedSource(browser);
453 if (plan.owner != destination.game_data ||
454 plan.source_context_revision != destination.source_context_revision ||
455 !destination.source_context || !source ||
456 source->id != browser.candidate_source_id ||
457 !SameRect(browser.art_selection, browser.candidate_selection))
458 return refuse("Source or destination context changed; try the art again.");
459 const auto* store = ActiveSheetStore(destination);
460 if (!store)
461 return refuse("Destination graphics session is inactive.");
462 for (const auto& [sheet, before] : plan.before_sheets) {
463 const auto revision = plan.sheet_revisions.find(sheet);
464 if (!store->HasSheet(sheet) || revision == plan.sheet_revisions.end() ||
465 store->Revision(sheet) != revision->second)
466 return refuse("Destination pixels changed; try the art again.");
467 const auto current = store->Sheet(sheet);
468 if (current.size() != before.size() ||
469 !std::equal(current.begin(), current.end(), before.begin()))
470 return refuse("Destination pixels changed; try the art again.");
471 }
472 const auto& context = *destination.source_context;
473 if (!plan.source || !context.resolve_live_source)
474 return refuse("Selected object cannot resolve its live source.");
475 // A room-aware source trace replays the preceding object stream. Keep that
476 // work at Try/Apply boundaries when the source supplies a semantic stamp.
477 // Callers without a stamp retain full validation on each draw.
478 if (resolve_pattern || !context.preview_revision) {
480 const auto resolved = context.resolve_live_source(fresh);
481 if (!resolved.ok() || !SameSourcePattern(*plan.source, fresh))
482 return refuse("Selected object pattern changed; try the art again.");
483 }
484 if (context.preview_revision) {
485 const auto revision = context.preview_revision();
486 if (!revision.ok() || !browser.candidate_source_revision ||
487 *revision != *browser.candidate_source_revision)
488 return refuse("Selected object preview changed; try the art again.");
489 } else if (browser.candidate_source_revision) {
490 return refuse("Selected object preview context changed.");
491 }
492 return absl::OkStatus();
493}
494} // namespace
495
498 return ValidateCatalogArtPreviewInputs(browser, destination, true);
499}
500
504 RETURN_IF_ERROR(EnsureAssetPreview(browser));
505 if (!destination.source_context ||
506 !destination.source_context->render_staged_preview)
507 return absl::FailedPreconditionError(
508 "Open an object's graphics source to preview imported art.");
511 browser.art_selection));
512 auto overrides = plan.before_sheets;
513 for (const auto& diff : plan.sheets) {
514 auto& pixels = overrides.at(diff.sheet);
515 for (size_t i = 0; i < diff.offsets.size(); ++i)
516 pixels.at(diff.offsets[i]) = diff.after.at(i);
517 }
519 auto image, destination.source_context->render_staged_preview(overrides));
520 const bool indexed =
522 if (image.width <= 0 || image.height <= 0 || image.width > 1024 ||
523 image.height > 1024 ||
524 image.pixels.size() !=
525 static_cast<size_t>(image.width) * image.height * (indexed ? 1 : 4))
526 return absl::InvalidArgumentError(
527 "Detached object renderer returned invalid image dimensions.");
528 if (!destination.source_context ||
529 destination.source_context->owner != plan.owner ||
530 destination.source_context_revision != plan.source_context_revision)
531 return absl::FailedPreconditionError(
532 "Selected object context changed during preview.");
533 std::optional<uint64_t> revision;
534 if (destination.source_context->preview_revision) {
535 ASSIGN_OR_RETURN(revision, destination.source_context->preview_revision());
536 }
537 browser.staged_bitmap.Create(image.width, image.height, indexed ? 8 : 32,
538 image.pixels);
539 if (indexed) {
540 std::vector<SDL_Color> colors(image.colors.begin(), image.colors.end());
541 if (image.index0_transparent)
542 colors[0].a = 0;
543 browser.staged_bitmap.SetPalette(colors);
544 }
545 browser.staged_bitmap.metadata().purpose =
548 &browser.staged_bitmap);
549 browser.candidate_source_revision = revision;
550 browser.candidate_source_id = SelectedSource(browser)->id;
551 browser.candidate_selection = browser.art_selection;
552 browser.object_pending = std::move(plan);
553 browser.destination_owner = destination.game_data;
554 browser.destination_sheet = destination.current_sheet_id;
555 return ValidateCatalogArtPreview(browser, destination);
556}
557
560 UndoManager& history) {
562 if (!browser.object_pending)
563 return absl::FailedPreconditionError("Try the art before applying it.");
567 return absl::OkStatus();
568}
569
572 UndoManager* undo_manager,
573 const std::string& destination_usage) {
574 gui::AutoWidgetScope widgets("Graphics/Catalog");
575 if (!browser.default_attempted) {
576 browser.default_attempted = true;
577 if (browser.sources.empty() && browser.path.empty()) {
578 const auto bundled =
579 AssetLoader::FindAssetFile("graphics/catalog/minish/catalog.json");
580 if (bundled.ok()) {
581 (void)LoadCatalogTileBrowser(browser, bundled->string());
582 } else {
583 browser.status =
584 "Bundled art catalog is unavailable; open a catalog JSON.";
585 }
586 }
587 }
588 if (browser.destination_owner != destination.game_data ||
589 browser.destination_sheet != destination.current_sheet_id) {
590 browser.pending.reset();
592 browser.destination_owner = destination.game_data;
593 browser.destination_sheet = destination.current_sheet_id;
594 }
595 const auto valid_candidate =
596 ValidateCatalogArtPreviewInputs(browser, destination, false);
597 if (!valid_candidate.ok())
598 browser.status = std::string(valid_candidate.message());
599 ImGui::TextWrapped(
600 "Browse complete art assets or source sheets. Try an art rectangle on "
601 "the "
602 "selected object's existing graphics before applying pixel changes.");
603 if (ImGui::InputTextWithHint("Catalog JSON", "references.catalog.json",
604 &browser.path))
605 browser.pending.reset();
606 gui::AutoRegisterLastItem("input", "Catalog JSON");
607 if (gui::AutoButton("Browse catalog")) {
609 options.filters.push_back({"Graphics catalog", "json"});
610 const auto path = util::FileDialogWrapper::ShowOpenFileDialog(options);
611 if (!path.empty()) {
612 browser.path = path;
613 browser.pending.reset();
614 }
615 }
616 ImGui::SameLine();
617 if (gui::AutoButton("Load catalog"))
618 (void)LoadCatalogTileBrowser(browser, browser.path);
619 if (!browser.status.empty())
620 ImGui::TextWrapped("%s", browser.status.c_str());
621 if (browser.sources.empty())
622 return;
623 ImGui::TextWrapped("Loaded source: %s", browser.loaded_path.c_str());
624 ImGui::InputTextWithHint("Filter sources", "Project or sheet name",
625 &browser.search);
626 gui::AutoRegisterLastItem("input", "Filter sources");
627 std::set<std::string> groups, categories;
628 for (const auto& source : browser.sources) {
629 if (!source.group.empty())
630 groups.insert(source.group);
631 if (!source.category.empty())
632 categories.insert(source.category);
633 }
634 DrawKindFilter(browser);
635 DrawMetadataFilter("Group", browser.group_filter, groups);
636 DrawMetadataFilter("Category", browser.category_filter, categories);
637 const auto filter = BrowserFilter(browser);
638 if (ImGui::BeginListBox("##CatalogSources", ImVec2(-1, 130))) {
639 for (size_t i = 0; i < browser.sources.size(); ++i) {
640 const auto& source = browser.sources[i];
641 if (source.page != 0 || !CatalogSourceMatches(source, filter))
642 continue;
643 ImGui::PushID(static_cast<int>(i));
644 const auto* current_source = SelectedSource(browser);
645 const bool selected =
646 current_source && (source.asset_id.empty()
647 ? current_source->id == source.id
648 : current_source->asset_id == source.asset_id);
649 const auto& name = source.name.empty() ? source.title : source.name;
650 const auto label =
651 filter.palettes_only
652 ? name + " palette (" + std::to_string(source.palette.size()) +
653 " colors)"
654 : name + " (" + std::to_string(source.bpp) + "bpp)";
655 if (ImGui::Selectable(label.c_str(), selected)) {
656 ResetSourceSelection(browser, static_cast<int>(i));
657 browser.palette = source.palette.empty() ? -1 : -3;
658 }
659 gui::AutoRegisterLastItem("selectable", name);
660 ImGui::PopID();
661 }
662 ImGui::EndListBox();
663 }
664 if (!SelectedSource(browser))
665 return;
666 const auto assembled = EnsureAssetPreview(browser);
667 if (!assembled.ok()) {
668 ImGui::TextWrapped("Asset preview unavailable: %s",
669 std::string(assembled.message()).c_str());
670 return;
671 }
672 Colors colors;
673 for (int i = 0; i < 16; ++i) {
674 const int level =
675 std::min(255, i * 255 / ((1 << browser.asset_preview->bpp) - 1));
676 colors[i] = IM_COL32(level, level, level, 255);
677 }
678 if (browser.palette >= int(browser.palettes.size()))
679 browser.palette = -3;
680 const bool source_colors_available = !browser.asset_preview->palette.empty();
681 const bool eight_color_preview = browser.asset_preview->bpp <= 3;
682 if ((browser.palette == -3 && !source_colors_available) ||
683 (!eight_color_preview && (browser.palette == -2 || browser.palette >= 0)))
684 browser.palette = source_colors_available ? -3 : -1;
685 ImGui::InputTextWithHint("Filter palettes", "ROM, walls, sprites, overworld",
686 &browser.palette_search);
687 gui::AutoRegisterLastItem("input", "Filter palettes");
688 const char* palette_label =
689 browser.palette == -3 ? "Source asset palette"
690 : browser.palette == -1 ? "Grayscale"
691 : browser.palette == -2 ? "Destination palette"
692 : browser.palettes[browser.palette].label.c_str();
693 const bool palette_open = ImGui::BeginCombo("Preview colors", palette_label);
694 gui::AutoRegisterLastItem("combo", "Preview colors");
695 if (palette_open) {
696 ImGui::BeginDisabled(!source_colors_available);
697 if (ImGui::Selectable("Source asset palette", browser.palette == -3))
698 browser.palette = -3;
699 ImGui::EndDisabled();
700 if (ImGui::Selectable("Grayscale", browser.palette == -1))
701 browser.palette = -1;
702 ImGui::BeginDisabled(!eight_color_preview);
703 if (ImGui::Selectable("Destination palette", browser.palette == -2))
704 browser.palette = -2;
705 for (size_t i = 0; i < browser.palettes.size(); ++i) {
706 if (!Matches(
707 browser.palettes[i].label + " " + browser.palettes[i].source_path,
708 browser.palette_search))
709 continue;
710 ImGui::PushID(static_cast<int>(i));
711 if (ImGui::Selectable(browser.palettes[i].label.c_str(),
712 browser.palette == int(i)))
713 browser.palette = static_cast<int>(i);
714 ImGui::PopID();
715 }
716 ImGui::EndDisabled();
717 ImGui::EndCombo();
718 }
719 if (!eight_color_preview)
720 ImGui::TextWrapped(
721 "This 4bpp asset uses source colors or grayscale. Destination and "
722 "catalog preview palettes contain only 8 colors.");
723 if (browser.palette == -3 && browser.asset_preview) {
724 for (size_t i = 0;
725 i < browser.asset_preview->palette.size() && i < colors.size(); ++i) {
726 const auto c = browser.asset_preview->palette[i];
727 colors[i] = IM_COL32((c & 31) * 255 / 31, ((c >> 5) & 31) * 255 / 31,
728 ((c >> 10) & 31) * 255 / 31, 255);
729 }
730 }
731 if (browser.palette >= 0) {
732 const auto& palette = browser.palettes[browser.palette];
733 ImGui::TextWrapped("Palette source: %s", palette.source_path.c_str());
734 for (int i = 0; i < 8; ++i)
735 colors[i] = IM_COL32(palette.colors[i][0], palette.colors[i][1],
736 palette.colors[i][2], 255);
737 }
738 ImGui::TextUnformatted("Destination sheet colors");
740 Colors destination_colors = colors;
741 bool destination_colors_available = false;
742 if (destination.game_data) {
743 if (const auto* choice =
744 destination.PreviewPalette(destination.current_sheet_id)) {
745 const auto resolved =
746 ResolveGraphicsPreviewPalette(*destination.game_data, *choice);
747 if (resolved.ok()) {
748 for (int i = 0; i < 8; ++i) {
749 const auto rgb = resolved->colors[i].rgb();
750 destination_colors[i] =
751 IM_COL32(int(rgb.x), int(rgb.y), int(rgb.z), 255);
752 }
753 destination_colors_available = true;
754 }
755 } else {
756 const auto sheet = destination.current_sheet_id;
757 const auto* loaded =
758 sheet < destination.game_data->gfx_bitmaps.size()
760 destination.game_data->gfx_bitmaps[sheet].surface())
761 : nullptr;
762 if (loaded && loaded->ncolors >= 8) {
763 for (int i = 0; i < 8; ++i) {
764 const auto color = loaded->colors[i];
765 destination_colors[i] = IM_COL32(color.r, color.g, color.b, 255);
766 }
767 destination_colors_available = true;
768 }
769 }
770 }
771 if (!destination_colors_available) {
772 ImGui::TextWrapped(
773 "Destination colors unavailable; select a loaded sheet "
774 "or a destination preview palette.");
775 browser.pending.reset();
776 }
777 if (browser.palette == -2 && destination_colors_available)
778 colors = destination_colors;
779 ImGui::TextWrapped(
780 "Preview colors only. Paste preserves pixel indices and "
781 "does not replace destination palette colors.");
782 const auto& source = browser.sources[browser.source];
783 DrawProvenance(source);
784 const auto rom_sheet_depth = ValidateCatalogSourceForRomSheet(source);
785 if (rom_sheet_depth.ok())
786 ImGui::TextWrapped(
787 "Apply replaces the existing source tiles. Objects sharing those "
788 "tiles will also show the new artwork.");
789 else
790 ImGui::TextWrapped("%s", std::string(rom_sheet_depth.message()).c_str());
791 DrawLogicalAsset(browser, colors);
792 if (destination.source_context) {
793 const auto& context = *destination.source_context;
794 ImGui::TextWrapped("Selected object: %s", context.label.c_str());
795 ImGui::Text("Target pattern: %d x %d pixels; %zu source targets",
796 context.pattern_width, context.pattern_height,
797 context.targets.size());
798 } else {
799 ImGui::TextWrapped(
800 "Open an object's graphics source to try this art on its existing "
801 "pattern.");
802 }
803 ImGui::BeginDisabled(!rom_sheet_depth.ok() || !destination.source_context ||
804 !destination.source_context->render_staged_preview);
805 if (gui::AutoButton("Try on selected object")) {
806 const auto status = TryCatalogArtOnObject(browser, destination);
807 browser.status =
808 status.ok()
809 ? "Detached candidate ready. Apply changes pixel indices only."
810 : std::string(status.message());
811 }
812 ImGui::EndDisabled();
813 if (browser.object_pending) {
814 DrawStagedPreview(browser);
815 ImGui::BeginDisabled(!undo_manager || destination.selection.is_floating);
816 if (gui::AutoButton("Apply imported art")) {
817 const bool changed = !browser.object_pending->sheets.empty();
818 const auto status =
819 ApplyCatalogArtToObject(browser, destination, *undo_manager);
820 browser.status = !status.ok() ? std::string(status.message())
821 : changed
822 ? "Applied imported art. Undo restores prior pixels."
823 : "No pixels changed; no Undo step was added.";
824 }
825 ImGui::EndDisabled();
826 ImGui::SameLine();
827 if (gui::AutoButton("Cancel imported art")) {
829 browser.status =
830 "Detached candidate canceled; destination pixels were unchanged.";
831 }
832 }
833 if (!destination_colors_available)
834 return;
835 const bool show_page_copy = ImGui::CollapsingHeader(
836 "Page-local tile copy", source.source_kind == "indexed-png"
837 ? ImGuiTreeNodeFlags_None
838 : ImGuiTreeNodeFlags_DefaultOpen);
839 gui::AutoRegisterLastItem("header", "Page-local tile copy");
840 if (!show_page_copy)
841 return;
842 if (source.page_count > 1) {
843 const bool open = ImGui::BeginCombo("Source page", source.title.c_str());
844 gui::AutoRegisterLastItem("combo", "Source page");
845 if (open) {
846 for (size_t i = 0; i < browser.sources.size(); ++i) {
847 const auto& page = browser.sources[i];
848 if (page.asset_id == source.asset_id &&
849 ImGui::Selectable(page.title.c_str(), browser.source == int(i)))
850 ResetSourceSelection(browser, static_cast<int>(i));
851 }
852 ImGui::EndCombo();
853 }
854 }
855 ImGui::Text("Page %d/%d: %dbpp, %d bytes planar", source.page + 1,
856 source.page_count, source.bpp, 64 * 8 * source.bpp);
857 ImGui::TextUnformatted(
858 "Click tiles, drag a rectangle, or Shift-click a range.");
859 int hovered_source = -1;
860 const int clicked = DrawSheet("##CatalogSourcePixels",
861 SelectedSource(browser)->indexed_pixels, colors,
862 browser.selected, &hovered_source);
863 if (clicked >= 0) {
864 browser.drag_start = clicked;
865 if (ImGui::GetIO().KeyShift && browser.last_source_tile >= 0) {
866 for (int i = std::min(clicked, browser.last_source_tile);
867 i <= std::max(clicked, browser.last_source_tile); ++i)
868 browser.selected.insert(i);
869 } else if (!browser.selected.erase(clicked)) {
870 browser.selected.insert(clicked);
871 }
872 browser.last_source_tile = clicked;
873 browser.pending.reset();
874 }
875 if (browser.drag_start >= 0 && hovered_source >= 0 &&
876 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
877 std::set<int> rectangle;
878 const int x0 = std::min(browser.drag_start % 16, hovered_source % 16);
879 const int x1 = std::max(browser.drag_start % 16, hovered_source % 16);
880 const int y0 = std::min(browser.drag_start / 16, hovered_source / 16);
881 const int y1 = std::max(browser.drag_start / 16, hovered_source / 16);
882 for (int y = y0; y <= y1; ++y)
883 for (int x = x0; x <= x1; ++x)
884 rectangle.insert(y * 16 + x);
885 if (rectangle != browser.selected) {
886 browser.selected = std::move(rectangle);
887 browser.pending.reset();
888 }
889 }
890 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left))
891 browser.drag_start = -1;
892 if (gui::AutoCheckbox("Keep source tile layout", &browser.preserve_layout))
893 browser.pending.reset();
894 if (gui::AutoButton("Select whole sheet")) {
895 for (int i = 0; i < 64; ++i)
896 browser.selected.insert(i);
897 browser.pending.reset();
898 }
899 ImGui::SameLine();
900 if (gui::AutoButton("Clear selection")) {
901 browser.selected.clear();
902 browser.pending.reset();
903 }
904 if (gui::AutoInputInt("First destination tile",
905 &browser.first_destination_tile)) {
906 browser.first_destination_tile =
907 std::clamp(browser.first_destination_tile, 0, 63);
908 browser.pending.reset();
909 }
910 const auto* store = ActiveSheetStore(destination);
911 if (!store || !store->HasSheet(destination.current_sheet_id)) {
912 ImGui::TextWrapped(
913 "Select a loaded destination sheet in this ROM session.");
914 browser.pending.reset();
915 return;
916 }
917 const auto current = store->Sheet(destination.current_sheet_id);
918 const std::vector<uint8_t> before(current.begin(), current.end());
919 ImGui::Text("Destination sheet $%02X — click its first tile",
920 destination.current_sheet_id);
921 std::set<int> target_tiles;
922 if (!browser.selected.empty()) {
923 int min_x = 15, min_y = 3;
924 for (int tile : browser.selected) {
925 min_x = std::min(min_x, tile % 16);
926 min_y = std::min(min_y, tile / 16);
927 }
928 int i = 0;
929 for (int tile : browser.selected) {
930 const int offset = browser.preserve_layout
931 ? (tile / 16 - min_y) * 16 + tile % 16 - min_x
932 : i;
933 const int target = browser.first_destination_tile + offset;
934 if (target < 64 &&
935 (!browser.preserve_layout ||
936 browser.first_destination_tile % 16 + tile % 16 - min_x < 16))
937 target_tiles.insert(target);
938 ++i;
939 }
940 }
941 const int target = DrawSheet("##CatalogDestinationPixels", before,
942 destination_colors, target_tiles);
943 if (target >= 0) {
944 browser.first_destination_tile = target;
945 browser.pending.reset();
946 }
947 if (!destination_usage.empty())
948 ImGui::TextWrapped("Used by: %s", destination_usage.c_str());
949 ImGui::Text("%zu selected tiles; %s", browser.selected.size(),
950 browser.preserve_layout ? "source layout preserved"
951 : "packed in row-major order");
952 ImGui::BeginDisabled(!rom_sheet_depth.ok() || browser.selected.empty() ||
953 !undo_manager || destination.selection.is_floating);
954 if (gui::AutoButton("Preview tile copy")) {
955 const std::vector<int> selected(browser.selected.begin(),
956 browser.selected.end());
957 auto plan = PreviewCatalogTileTransfer(
958 destination.current_sheet_id, before,
959 SelectedSource(browser)->indexed_pixels, selected,
960 browser.first_destination_tile, browser.preserve_layout);
961 browser.pending.reset();
962 if (!plan.ok())
963 browser.status = std::string(plan.status().message());
964 else {
965 plan->destination_owner = destination.game_data;
966 plan->destination_revision =
967 store->Revision(destination.current_sheet_id);
968 browser.pending = std::move(*plan);
969 browser.status = "Ready to apply one undoable tile copy.";
970 }
971 }
972 ImGui::EndDisabled();
973 if (browser.pending) {
974 if (browser.pending->destination_revision !=
975 store->Revision(destination.current_sheet_id) ||
976 browser.pending->before_pixels != before) {
977 browser.pending.reset();
978 browser.status = "Destination changed; preview the copy again.";
979 return;
980 }
981 ImGui::Text("After copy: %zu changed tiles",
982 browser.pending->changed_tiles.size());
983 DrawSheet("##CatalogAfterPixels", browser.pending->after_pixels,
984 destination_colors, target_tiles);
985 if (gui::AutoButton("Apply tile copy")) {
986 const auto result =
987 ApplyCatalogTileTransfer(*browser.pending, destination, undo_manager);
988 browser.status = !result.ok() ? std::string(result.message())
989 : browser.pending->changed_tiles.empty()
990 ? "No pixels changed; no undo step was added."
991 : "Copied tiles. Undo restores prior pixels.";
992 browser.pending.reset();
993 }
994 ImGui::SameLine();
995 if (gui::AutoButton("Cancel copy"))
996 browser.pending.reset();
997 }
998}
999} // namespace yaze::editor
static absl::StatusOr< std::filesystem::path > FindAssetFile(const std::string &relative_path)
Shared state between GraphicsEditor panel components.
Manages undo/redo stacks for a single editor.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
void RetireBitmap(Bitmap &bitmap)
Explicitly retire resources owned by a bitmap that is being erased.
Definition arena.cc:63
static Arena & Get()
Definition arena.cc:24
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
BitmapMetadata & metadata()
Definition bitmap.h:395
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
RAII scope that enables automatic widget registration.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1295
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
One ROM session's graphics sheet pixels, with a revision per sheet.
The Graphics editor's path for sheet pixel writes.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
void DrawLogicalAsset(CatalogTileBrowserState &browser, const Colors &colors)
bool Matches(std::string value, std::string filter)
bool DrawMetadataFilter(const char *label, std::string &selected, const std::set< std::string > &values)
absl::Status ValidateCatalogArtPreviewInputs(CatalogTileBrowserState &browser, const GraphicsEditorState &destination, bool resolve_pattern)
void DrawPaletteSwatches(const std::vector< uint16_t > &palette)
bool SameRect(const CatalogArtRect &a, const CatalogArtRect &b)
const CatalogSheetSource * SelectedSource(const CatalogTileBrowserState &browser)
absl::Status EnsureAssetPreview(CatalogTileBrowserState &browser)
void ResetSourceSelection(CatalogTileBrowserState &browser, int source)
int DrawSheet(const char *id, const std::vector< uint8_t > &pixels, const Colors &colors, const std::set< int > &selected={}, int *hovered_tile=nullptr)
CatalogSourceFilter BrowserFilter(const CatalogTileBrowserState &browser)
bool SameSourcePattern(const GraphicsEditContext &a, const GraphicsEditContext &b)
Editors are the view controllers for the application.
absl::StatusOr< std::vector< CatalogPreviewPalette > > LoadCatalogPreviewPalettes(const std::string &catalog_filename)
zelda3::GraphicsSheetStore * ActiveSheetStore(const GraphicsEditorState &state)
const char * CatalogResourceKindName(CatalogResourceKind kind)
void CancelCatalogArtPreview(CatalogTileBrowserState &browser)
void DrawCatalogTileBrowser(CatalogTileBrowserState &browser, GraphicsEditorState &destination, UndoManager *undo_manager, const std::string &destination_usage)
absl::StatusOr< std::vector< CatalogSheetSource > > LoadCatalogSheetSources(const std::string &filename)
absl::Status ApplyCatalogTileTransfer(const CatalogTileTransferPlan &plan, GraphicsEditorState &state, UndoManager *undo_manager)
absl::Status ValidateCatalogArtPreview(CatalogTileBrowserState &browser, const GraphicsEditorState &destination)
absl::Status LoadCatalogTileBrowser(CatalogTileBrowserState &browser, const std::string &filename)
void DrawGraphicsPreviewPaletteSelector(GraphicsEditorState &state, GraphicsPaletteSelectorLayout layout, GraphicsPaletteSelectorScope scope)
absl::StatusOr< GraphicsPalettePreview > ResolveGraphicsPreviewPalette(zelda3::GameData &data, const GraphicsPaletteSelection &selection)
absl::Status ApplyCatalogArtToObject(CatalogTileBrowserState &browser, GraphicsEditorState &destination, UndoManager &history)
absl::Status ApplyPixelIndexPlan(GraphicsEditorState &state, UndoManager &history, const PixelIndexPlan &plan)
absl::Status TryCatalogArtOnObject(CatalogTileBrowserState &browser, GraphicsEditorState &destination)
absl::StatusOr< CatalogAssetPreview > AssembleCatalogAsset(const std::vector< CatalogSheetSource > &sources, const std::string &asset_id)
absl::Status ValidateCatalogSourceForRomSheet(const CatalogSheetSource &source)
bool CatalogSourceMatches(const CatalogSheetSource &source, const CatalogSourceFilter &filter)
absl::StatusOr< CatalogTileTransferPlan > PreviewCatalogTileTransfer(uint16_t destination_sheet, const std::vector< uint8_t > &destination_pixels, const std::vector< uint8_t > &source_pixels, const std::vector< int > &source_tiles, int first_destination_tile, bool preserve_layout)
absl::StatusOr< PixelIndexPlan > PlanCatalogObjectTransfer(const GraphicsEditorState &state, const CatalogAssetPreview &asset, const CatalogArtRect &crop)
void EndCanvas(Canvas &canvas)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
bool AutoInputInt(const char *label, int *v, int step=1, int step_fast=100, ImGuiInputTextFlags flags=0)
bool AutoCheckbox(const char *label, bool *v)
bool AutoButton(const char *label, const ImVec2 &size=ImVec2(0, 0))
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:398
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Room transition destination.
Definition zelda.h:448
std::optional< CatalogTileTransferPlan > pending
std::optional< uint64_t > candidate_source_revision
std::optional< PixelIndexPlan > object_pending
std::optional< CatalogAssetPreview > asset_preview
std::vector< CatalogSheetSource > sources
std::vector< CatalogPreviewPalette > palettes
zelda3::ResolvedGraphicsContext resolved
std::vector< GraphicsEditTarget > targets
std::function< absl::Status(GraphicsEditContext &) resolve_live_source)
std::vector< GraphicsSourceCell > pattern_cells
std::vector< FileDialogFilter > filters
Definition file_util.h:18
Automatic widget registration helpers for ImGui Test Engine integration.