573 const std::string& destination_usage) {
577 if (browser.
sources.empty() && browser.
path.empty()) {
584 "Bundled art catalog is unavailable; open a catalog JSON.";
595 const auto valid_candidate =
596 ValidateCatalogArtPreviewInputs(browser,
destination,
false);
597 if (!valid_candidate.ok())
598 browser.
status = std::string(valid_candidate.message());
600 "Browse complete art assets or source sheets. Try an art rectangle on "
602 "selected object's existing graphics before applying pixel changes.");
603 if (ImGui::InputTextWithHint(
"Catalog JSON",
"references.catalog.json",
609 options.
filters.push_back({
"Graphics catalog",
"json"});
619 if (!browser.
status.empty())
620 ImGui::TextWrapped(
"%s", browser.
status.c_str());
623 ImGui::TextWrapped(
"Loaded source: %s", browser.
loaded_path.c_str());
624 ImGui::InputTextWithHint(
"Filter sources",
"Project or sheet name",
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);
634 DrawKindFilter(browser);
635 DrawMetadataFilter(
"Group", browser.
group_filter, groups);
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];
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;
652 ? name +
" palette (" + std::to_string(source.palette.size()) +
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;
664 if (!SelectedSource(browser))
666 const auto assembled = EnsureAssetPreview(browser);
667 if (!assembled.ok()) {
668 ImGui::TextWrapped(
"Asset preview unavailable: %s",
669 std::string(assembled.message()).c_str());
673 for (
int i = 0; i < 16; ++i) {
675 std::min(255, i * 255 / ((1 << browser.
asset_preview->bpp) - 1));
676 colors[i] = IM_COL32(level, level, level, 255);
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",
688 const char* palette_label =
689 browser.
palette == -3 ?
"Source asset palette"
690 : browser.
palette == -1 ?
"Grayscale"
691 : browser.
palette == -2 ?
"Destination palette"
693 const bool palette_open = ImGui::BeginCombo(
"Preview colors", palette_label);
696 ImGui::BeginDisabled(!source_colors_available);
697 if (ImGui::Selectable(
"Source asset palette", browser.
palette == -3))
699 ImGui::EndDisabled();
700 if (ImGui::Selectable(
"Grayscale", browser.
palette == -1))
702 ImGui::BeginDisabled(!eight_color_preview);
703 if (ImGui::Selectable(
"Destination palette", browser.
palette == -2))
705 for (
size_t i = 0; i < browser.
palettes.size(); ++i) {
710 ImGui::PushID(
static_cast<int>(i));
711 if (ImGui::Selectable(browser.
palettes[i].label.c_str(),
713 browser.
palette =
static_cast<int>(i);
716 ImGui::EndDisabled();
719 if (!eight_color_preview)
721 "This 4bpp asset uses source colors or grayscale. Destination and "
722 "catalog preview palettes contain only 8 colors.");
725 i < browser.
asset_preview->palette.size() && i < colors.size(); ++i) {
727 colors[i] = IM_COL32((c & 31) * 255 / 31, ((c >> 5) & 31) * 255 / 31,
728 ((c >> 10) & 31) * 255 / 31, 255);
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);
738 ImGui::TextUnformatted(
"Destination sheet colors");
740 Colors destination_colors = colors;
741 bool destination_colors_available =
false;
743 if (
const auto* choice =
745 const auto resolved =
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);
753 destination_colors_available =
true;
760 destination.game_data->gfx_bitmaps[sheet].surface())
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);
767 destination_colors_available =
true;
771 if (!destination_colors_available) {
773 "Destination colors unavailable; select a loaded sheet "
774 "or a destination preview palette.");
777 if (browser.
palette == -2 && destination_colors_available)
778 colors = destination_colors;
780 "Preview colors only. Paste preserves pixel indices and "
781 "does not replace destination palette colors.");
783 DrawProvenance(source);
785 if (rom_sheet_depth.ok())
787 "Apply replaces the existing source tiles. Objects sharing those "
788 "tiles will also show the new artwork.");
790 ImGui::TextWrapped(
"%s", std::string(rom_sheet_depth.message()).c_str());
791 DrawLogicalAsset(browser, colors);
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());
800 "Open an object's graphics source to try this art on its existing "
803 ImGui::BeginDisabled(!rom_sheet_depth.ok() || !
destination.source_context ||
804 !
destination.source_context->render_staged_preview);
809 ?
"Detached candidate ready. Apply changes pixel indices only."
810 : std::string(status.message());
812 ImGui::EndDisabled();
814 DrawStagedPreview(browser);
815 ImGui::BeginDisabled(!undo_manager ||
destination.selection.is_floating);
820 browser.
status = !status.ok() ? std::string(status.message())
822 ?
"Applied imported art. Undo restores prior pixels."
823 :
"No pixels changed; no Undo step was added.";
825 ImGui::EndDisabled();
830 "Detached candidate canceled; destination pixels were unchanged.";
833 if (!destination_colors_available)
835 const bool show_page_copy = ImGui::CollapsingHeader(
836 "Page-local tile copy", source.source_kind ==
"indexed-png"
837 ? ImGuiTreeNodeFlags_None
838 : ImGuiTreeNodeFlags_DefaultOpen);
842 if (source.page_count > 1) {
843 const bool open = ImGui::BeginCombo(
"Source page", source.title.c_str());
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));
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,
869 }
else if (!browser.
selected.erase(clicked)) {
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);
890 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left))
895 for (
int i = 0; i < 64; ++i)
911 if (!store || !store->HasSheet(
destination.current_sheet_id)) {
913 "Select a loaded destination sheet in this ROM session.");
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",
921 std::set<int> target_tiles;
923 int min_x = 15, min_y = 3;
925 min_x = std::min(min_x, tile % 16);
926 min_y = std::min(min_y, tile / 16);
931 ? (tile / 16 - min_y) * 16 + tile % 16 - min_x
937 target_tiles.insert(target);
941 const int target = DrawSheet(
"##CatalogDestinationPixels", before,
942 destination_colors, target_tiles);
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(),
951 :
"packed in row-major order");
952 ImGui::BeginDisabled(!rom_sheet_depth.ok() || browser.
selected.empty() ||
953 !undo_manager ||
destination.selection.is_floating);
955 const std::vector<int> selected(browser.
selected.begin(),
959 SelectedSource(browser)->indexed_pixels, selected,
963 browser.
status = std::string(plan.status().message());
966 plan->destination_revision =
968 browser.
pending = std::move(*plan);
969 browser.
status =
"Ready to apply one undoable tile copy.";
972 ImGui::EndDisabled();
974 if (browser.
pending->destination_revision !=
976 browser.
pending->before_pixels != before) {
978 browser.
status =
"Destination changed; preview the copy again.";
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);
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.";