yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tile16_editor.cc
Go to the documentation of this file.
1#include "tile16_editor.h"
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <array>
6#include <memory>
7
8#include "absl/status/status.h"
9#include "absl/strings/str_format.h"
14#include "app/gfx/core/bitmap.h"
19#include "app/gui/core/input.h"
20#include "app/gui/core/style.h"
24#include "imgui/imgui.h"
25#include "rom/rom.h"
26#include "util/hex.h"
27#include "util/log.h"
28#include "util/macro.h"
29#include "zelda3/game_data.h"
35
36namespace yaze {
37namespace editor {
38
39using namespace ImGui;
40
41// Display scales used for the tile8 source/preview rendering.
42constexpr float kTile8DisplayScale = 4.0f;
43
44namespace {
45
47
49 std::vector<gfx::SnesColor> grayscale;
50 grayscale.reserve(gfx::SnesPalette::kMaxColors);
51 for (int i = 0; i < static_cast<int>(gfx::SnesPalette::kMaxColors); ++i) {
52 const float value = static_cast<float>(i) / 255.0f;
53 grayscale.emplace_back(ImVec4(value, value, value, 1.0f));
54 }
55 if (!grayscale.empty()) {
56 grayscale[0].set_transparent(true);
57 }
58 return gfx::SnesPalette(grayscale);
59}
60
62 return zelda3::MutableTile16QuadrantInfo(*tile, quadrant);
63}
64
66 int quadrant) {
67 return zelda3::Tile16QuadrantInfo(tile, quadrant);
68}
69
73
74const char* EditModeLabel(Tile16EditMode mode) {
75 switch (mode) {
77 return "Paint";
79 return "Pick";
81 return "Usage Probe";
82 default:
83 return "Paint";
84 }
85}
86
87} // namespace
88
90 gfx::Bitmap& tile16_blockset_bmp, gfx::Bitmap& current_gfx_bmp,
91 std::array<uint8_t, 0x200>& all_tiles_types) {
92 all_tiles_types_ = all_tiles_types;
93 tile16_blockset_bmp_ = &tile16_blockset_bmp;
94 current_gfx_bmp_ = &current_gfx_bmp;
95
96 // Note: LoadTile8() will be called after palette is set by overworld editor
97 // This ensures proper palette coordination from the start
98
99 // Initialize current tile16 bitmap - this will be set by SetCurrentTile
101 std::vector<uint8_t>(kTile16PixelCount, 0));
102 current_tile16_bmp_.SetPalette(tile16_blockset_bmp.palette());
103 // Queue texture for later rendering.
106
107 // Initialize enhanced canvas features with proper sizing
110
111 // Attach blockset canvas to the selector widget
113 blockset_selector_.SetTileCount(kTile16Count);
114
115 // Configure canvases with proper initialization
118
119 // Initialize enhanced palette editors if ROM is available
120 if (rom_) {
123 }
124
125 // Initialize the current tile16 properly from the blockset
126 if (tile16_blockset_) {
127 RETURN_IF_ERROR(SetCurrentTile(0)); // Start with tile 0
128 }
129
131
132 // Setup collision type labels for tile8 canvas
133 ImVector<std::string> tile16_names;
134 for (int i = 0; i < 0x200; ++i) {
135 std::string str = util::HexByte(all_tiles_types_[i]);
136 tile16_names.push_back(str);
137 }
138 *tile8_source_canvas_.mutable_labels(0) = tile16_names;
140
141 // Setup tile info table
143 [&]() { Text(tr("Tile16: %02X"), current_tile16_); });
145 [&]() { Text(tr("Tile8: %02X"), current_tile8_); });
146 gui::AddTableColumn(tile_edit_table_, "##tile16Flip", [&]() {
147 Checkbox(tr("X Flip"), &x_flip);
148 Checkbox(tr("Y Flip"), &y_flip);
149 Checkbox(tr("Priority"), &priority_tile);
150 });
151
152 return absl::OkStatus();
153}
154
155absl::Status Tile16Editor::Update() {
157 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
158 }
159
160 if (BeginMenuBar()) {
161 if (BeginMenu(tr("View"))) {
162 Checkbox(tr("Show Collision Types"),
164 EndMenu();
165 }
166
167 if (BeginMenu(tr("Edit"))) {
168 if (MenuItem(tr("Copy Current Tile16"), "Ctrl+C")) {
170 }
171 if (MenuItem(tr("Paste to Current Tile16"), "Ctrl+V")) {
173 }
174 EndMenu();
175 }
176
177 if (BeginMenu(tr("File"))) {
178 if (MenuItem(tr("Write Pending to ROM"), "Ctrl+S")) {
180 }
181 if (MenuItem(tr("Refresh Blockset Preview"), "Ctrl+Shift+S")) {
183 }
184 Separator();
185 bool live_preview = live_preview_enabled_;
186 if (MenuItem(tr("Live Preview"), nullptr, &live_preview)) {
187 EnableLivePreview(live_preview);
188 }
189 EndMenu();
190 }
191
192 if (BeginMenu(tr("Scratch Space"))) {
193 for (int i = 0; i < 4; i++) {
194 std::string slot_name = "Slot " + std::to_string(i + 1);
195 if (scratch_space_[i].has_data) {
196 if (MenuItem((slot_name + " (Load)").c_str())) {
198 }
199 if (MenuItem((slot_name + " (Save)").c_str())) {
201 }
202 if (MenuItem((slot_name + " (Clear)").c_str())) {
204 }
205 } else {
206 if (MenuItem((slot_name + " (Save)").c_str())) {
208 }
209 }
210 if (i < 3)
211 Separator();
212 }
213 EndMenu();
214 }
215
216 EndMenuBar();
217 }
218
219 // About popup
220 if (BeginPopupModal("About Tile16 Editor", NULL,
221 ImGuiWindowFlags_AlwaysAutoResize)) {
222 Text(tr("Tile16 Editor for Link to the Past"));
223 Text(tr("This editor allows you to edit 16x16 tiles used in the game."));
224 Text(tr("Features:"));
225 BulletText(
226 tr("Edit Tile16 graphics by placing 8x8 tiles in the quadrants"));
227 BulletText(tr("Copy and paste Tile16 graphics"));
228 BulletText(tr("Save and load Tile16 graphics to/from scratch space"));
229 BulletText(tr("Preview Tile16 graphics at a larger size"));
230 Separator();
231 if (Button(tr("Close"))) {
232 CloseCurrentPopup();
233 }
234 EndPopup();
235 }
236
237 // Unsaved changes confirmation dialog
239 OpenPopup("Unsaved Changes##Tile16Editor");
240 }
241 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
242 ImGuiWindowFlags_AlwaysAutoResize)) {
243 Text(tr("Tile %d has staged changes."), current_tile16_);
244 Text(tr("What would you like to do?"));
245 Separator();
246
247 if (Button(tr("Keep Staged & Continue"), ImVec2(220, 0))) {
248 if (IsItemHovered()) {
249 SetTooltip(tr(
250 "Switch to the requested tile now. Other tiles with staged edits "
251 "stay in the write queue until you use Write Pending or Discard."));
252 }
255 if (!status.ok()) {
256 util::logf("Failed to switch to tile %d: %s",
257 pending_tile_switch_target_, status.message().data());
258 }
259 }
262 CloseCurrentPopup();
263 }
264
265 if (gui::SuccessButton("Write Pending & Continue", ImVec2(220, 0))) {
266 auto status = CommitAllChanges();
267 if (status.ok() && pending_tile_switch_target_ >= 0) {
269 }
270 if (!status.ok()) {
271 util::logf("Failed to write/switch pending changes: %s",
272 status.message().data());
273 }
276 CloseCurrentPopup();
277 }
278
279 if (gui::DangerButton("Discard Current & Continue", ImVec2(220, 0))) {
284 if (!status.ok()) {
285 util::logf("Failed to switch to tile %d: %s",
286 pending_tile_switch_target_, status.message().data());
287 }
288 }
291 CloseCurrentPopup();
292 }
293
294 if (Button(tr("Cancel"), ImVec2(220, 0))) {
297 CloseCurrentPopup();
298 }
299
300 EndPopup();
301 }
302
303 // Handle keyboard shortcuts (shared implementation)
305
307
308 // Draw palette settings popup if enabled
310
311 // Update live preview if dirty
313
314 return absl::OkStatus();
315}
316
318 // REFACTORED: Single unified table layout in UpdateTile16Edit
320}
321
324 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
325 }
326
327 // Menu button for context menu
328 if (Button(ICON_MD_MENU " Menu")) {
329 OpenPopup("##Tile16EditorContextMenu");
330 }
331 SameLine();
332 TextDisabled(tr("Right-click for more options"));
333
334 // Context menu
336
337 // About popup
338 if (BeginPopupModal("About Tile16 Editor", NULL,
339 ImGuiWindowFlags_AlwaysAutoResize)) {
340 Text(tr("Tile16 Editor for Link to the Past"));
341 Text(tr("This editor allows you to edit 16x16 tiles used in the game."));
342 Text(tr("Features:"));
343 BulletText(
344 tr("Edit Tile16 graphics by placing 8x8 tiles in the quadrants"));
345 BulletText(tr("Copy and paste Tile16 graphics"));
346 BulletText(tr("Save and load Tile16 graphics to/from scratch space"));
347 BulletText(tr("Preview Tile16 graphics at a larger size"));
348 Separator();
349 if (Button(tr("Close"))) {
350 CloseCurrentPopup();
351 }
352 EndPopup();
353 }
354
355 // Unsaved changes confirmation dialog
357 OpenPopup("Unsaved Changes##Tile16Editor");
358 }
359 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
360 ImGuiWindowFlags_AlwaysAutoResize)) {
361 Text(tr("Tile %d has staged changes."), current_tile16_);
362 Text(tr("What would you like to do?"));
363 Separator();
364
365 if (Button(tr("Keep Staged & Continue"), ImVec2(220, 0))) {
366 if (IsItemHovered()) {
367 SetTooltip(tr(
368 "Switch to the requested tile now. Other tiles with staged edits "
369 "stay in the write queue until you use Write Pending or Discard."));
370 }
373 if (!status.ok()) {
374 util::logf("Failed to switch to tile %d: %s",
375 pending_tile_switch_target_, status.message().data());
376 }
377 }
380 CloseCurrentPopup();
381 }
382
383 if (gui::SuccessButton("Write Pending & Continue", ImVec2(220, 0))) {
384 auto status = CommitAllChanges();
385 if (status.ok() && pending_tile_switch_target_ >= 0) {
387 }
388 if (!status.ok()) {
389 util::logf("Failed to write/switch pending changes: %s",
390 status.message().data());
391 }
394 CloseCurrentPopup();
395 }
396
397 if (gui::DangerButton("Discard Current & Continue", ImVec2(220, 0))) {
402 if (!status.ok()) {
403 util::logf("Failed to switch to tile %d: %s",
404 pending_tile_switch_target_, status.message().data());
405 }
406 }
409 CloseCurrentPopup();
410 }
411
412 if (Button(tr("Cancel"), ImVec2(220, 0))) {
415 CloseCurrentPopup();
416 }
417
418 EndPopup();
419 }
420
421 // Handle keyboard shortcuts (shared implementation)
423
427
428 return absl::OkStatus();
429}
430
432 if (BeginPopup("##Tile16EditorContextMenu")) {
433 if (BeginMenu(tr("View"))) {
434 if (MenuItem(tr("Show Grid"), nullptr, show_tile_grid_)) {
436 }
437 if (MenuItem(tr("Show Collision IDs"), nullptr,
442 }
443 EndMenu();
444 }
445
446 if (BeginMenu(tr("Edit"))) {
447 if (MenuItem(tr("Copy Current Tile16"), "Ctrl+C")) {
449 }
450 if (MenuItem(tr("Paste to Current Tile16"), "Ctrl+V")) {
452 }
453 Separator();
454 if (MenuItem(tr("Flip Horizontal"), "H")) {
456 }
457 if (MenuItem(tr("Flip Vertical"), "V")) {
459 }
460 if (MenuItem(tr("Rotate"), "R")) {
462 }
463 if (MenuItem(tr("Clear"), "Delete")) {
465 }
466 EndMenu();
467 }
468
469 if (BeginMenu(tr("File"))) {
470 if (MenuItem(tr("Write Pending to ROM"), "Ctrl+S")) {
472 }
473 if (MenuItem(tr("Refresh Blockset Preview"), "Ctrl+Shift+S")) {
475 }
476 Separator();
477 bool live_preview = live_preview_enabled_;
478 if (MenuItem(tr("Live Preview"), nullptr, &live_preview)) {
479 EnableLivePreview(live_preview);
480 }
481 EndMenu();
482 }
483
484 if (BeginMenu(tr("Scratch Space"))) {
485 for (int i = 0; i < 4; i++) {
486 std::string slot_name = "Slot " + std::to_string(i + 1);
487 if (scratch_space_[i].has_data) {
488 if (MenuItem((slot_name + " (Load)").c_str())) {
490 }
491 if (MenuItem((slot_name + " (Save)").c_str())) {
493 }
494 if (MenuItem((slot_name + " (Clear)").c_str())) {
496 }
497 } else {
498 if (MenuItem((slot_name + " (Save)").c_str())) {
500 }
501 }
502 if (i < 3)
503 Separator();
504 }
505 EndMenu();
506 }
507
508 EndPopup();
509 }
510}
511
514 gui::BeginChildWithScrollbar("##Tile16EditorBlocksetScrollRegion");
515
516 // Tile ID search/jump bar
519 }
520
521 // Configure canvas frame options for blockset view
522 gui::CanvasFrameOptions frame_opts;
523 frame_opts.draw_grid = true;
524 frame_opts.grid_step = 32.0f; // Tile16 grid
525 frame_opts.draw_context_menu = true;
526 frame_opts.draw_overlay = true;
527 frame_opts.render_popups = true;
528 frame_opts.use_child_window = false;
529
530 auto canvas_rt = gui::BeginCanvas(blockset_canvas_, frame_opts);
532
533 // Ensure selector is synced with current selection
536 }
537
538 if (tile16_blockset_bmp_ == nullptr) {
539 return absl::FailedPreconditionError(
540 "Tile16 blockset bitmap not initialized");
541 }
542
543 // Render the selector widget (handles bitmap, grid, highlights, interaction)
544 auto result = blockset_selector_.Render(*tile16_blockset_bmp_, true);
545
546 if (result.selection_changed) {
547 // Use RequestTileSwitch to handle pending changes confirmation
548 RequestTileSwitch(result.selected_tile);
549 util::logf("Selected Tile16 from blockset: %d", result.selected_tile);
550 }
551
552 gui::EndCanvas(blockset_canvas_, canvas_rt, frame_opts);
553 EndChild();
554
555 return absl::OkStatus();
556}
557
558// ROM data access methods
560 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
561 return nullptr;
562 }
563 return &current_tile16_data_;
564}
565
567 auto* tile_data = GetCurrentTile16Data();
568 if (!tile_data) {
569 return absl::FailedPreconditionError("Cannot access current tile16 data");
570 }
571
572 // Write the modified tile16 data back to ROM
575
576 util::logf("ROM Tile16 data written for tile %d", current_tile16_);
577 return absl::OkStatus();
578}
579
581 if (!tile16_blockset_) {
582 return absl::FailedPreconditionError("Tile16 blockset not available");
583 }
584
585 // CRITICAL FIX: Force regeneration without using problematic tile cache
586 // Directly mark atlas as modified to trigger regeneration from ROM data
587
588 // Mark atlas as modified to trigger regeneration
590
591 // Queue texture update via Arena's deferred system
594
595 util::logf("Tile16 blockset refreshed and regenerated");
596 return absl::OkStatus();
597}
598
600 const gfx::Tile16& tile_data, gfx::Bitmap* output_bitmap) const {
602 tile_data, current_gfx_individual_, output_bitmap);
603}
604
606 const gfx::Bitmap& tile_bitmap) {
607 if (!tile_bitmap.is_active()) {
608 return;
609 }
610
611 if (tile16_blockset_bmp_ != nullptr) {
613 }
616 }
617
620 tile_bitmap);
622 }
623}
624
626 gfx::ScopedTimer timer("tile16_blockset_update");
627
628 if (!tile16_blockset_) {
629 return absl::FailedPreconditionError("Tile16 blockset not initialized");
630 }
631
632 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
633 return absl::OutOfRangeError("Current tile16 ID out of range");
634 }
635
637 return absl::FailedPreconditionError("Current tile16 bitmap is not active");
638 }
639
641
645 }
649 }
650
651 return absl::OkStatus();
652}
653
655 // Rebuild preview from `current_tile16_data_` (SetCurrentTile prefers pending
656 // maps over ROM). Name is historical.
657 auto* tile_data = GetCurrentTile16Data();
658 if (!tile_data) {
659 return absl::FailedPreconditionError("Cannot access current tile16 data");
660 }
661
662 // Tests and some initialization paths reach regeneration before tile8 previews
663 // are built; lazily populate them so metadata->bitmap rendering can proceed.
664 if (current_gfx_individual_.empty()) {
665 if (!HasCurrentGfxBitmap()) {
666 return absl::FailedPreconditionError("Tile8 source bitmap not active");
667 }
669 }
670
671 // Shared render path used by regeneration, stamping, and multi-tile updates.
673
674 // Set the appropriate palette using the same system as overworld
676
677 // Queue texture creation via Arena's deferred system
680
681 util::logf("Regenerated Tile16 bitmap for tile %d from ROM data",
683 return absl::OkStatus();
684}
685
686absl::Status Tile16Editor::DrawToCurrentTile16(ImVec2 pos,
687 const gfx::Bitmap* source_tile) {
688 constexpr int kTile8Size = 8;
689 (void)source_tile;
690
691 // Save undo state before making changes
692 auto now = std::chrono::steady_clock::now();
693 auto time_since_last_edit =
694 std::chrono::duration_cast<std::chrono::milliseconds>(now -
696 .count();
697
698 if (time_since_last_edit > 100) { // 100ms threshold
700 last_edit_time_ = now;
701 }
702
703 // Validate inputs
704 if (current_tile8_ < 0 ||
705 current_tile8_ >= static_cast<int>(current_gfx_individual_.size())) {
706 return absl::OutOfRangeError(
707 absl::StrFormat("Invalid tile8 index: %d", current_tile8_));
708 }
709
711 return absl::FailedPreconditionError("Target tile16 bitmap not active");
712 }
713
714 const int tile8_count =
715 static_cast<int>(std::min<size_t>(current_gfx_individual_.size(), 1024));
716 const int max_tile8_id = std::max(0, tile8_count - 1);
717 const int tile8_row_stride =
718 std::max(1, current_gfx_bmp_->width() / kTile8Size);
719 const int quadrant_x = (pos.x >= kTile8Size) ? 1 : 0;
720 const int quadrant_y = (pos.y >= kTile8Size) ? 1 : 0;
721 const int quadrant_index = quadrant_x + (quadrant_y * 2);
722 active_quadrant_ = std::clamp(quadrant_index, 0, 3);
723
724 zelda3::Tile16StampRequest stamp_request;
725 stamp_request.current_tile16 = current_tile16_data_;
726 stamp_request.current_tile16_id = current_tile16_;
727 stamp_request.selected_tile8_id = current_tile8_;
728 stamp_request.stamp_size = tile8_stamp_size_;
729 stamp_request.quadrant_index = quadrant_index;
730 stamp_request.palette_id = current_palette_;
731 stamp_request.x_flip = x_flip;
732 stamp_request.y_flip = y_flip;
733 stamp_request.priority = priority_tile;
734 stamp_request.tile8_row_stride = tile8_row_stride;
735 stamp_request.tile16_row_stride = kTilesPerRow;
736 stamp_request.max_tile8_id = max_tile8_id;
737 stamp_request.max_tile16_id = kTile16Count - 1;
738
739 ASSIGN_OR_RETURN(auto staged_tiles,
740 zelda3::BuildTile16StampMutations(stamp_request));
741
742 for (const auto& mutation : staged_tiles) {
743 const int tile16_id = mutation.tile16_id;
744 const gfx::Tile16& tile_data = mutation.tile_data;
745 gfx::Bitmap staged_bitmap;
746 RETURN_IF_ERROR(BuildTile16BitmapFromData(tile_data, &staged_bitmap));
747 if (current_tile16_bmp_.palette().size() > 0) {
748 staged_bitmap.SetPalette(current_tile16_bmp_.palette());
749 }
750
751 if (tile16_id == current_tile16_) {
752 current_tile16_data_ = tile_data;
753 SyncTilesInfoArray(&current_tile16_data_);
755 staged_bitmap.vector());
761 } else {
762 pending_tile16_changes_[tile16_id] = tile_data;
763 pending_tile16_bitmaps_[tile16_id] = staged_bitmap;
764 preview_dirty_ = true;
765 }
766
767 CopyTileBitmapToBlockset(tile16_id, staged_bitmap);
768 }
769
773 }
777 }
778
780
783 }
784
786 "Local tile16 stamp staged (size=%dx, tiles=%zu). Use 'Write Pending' to "
787 "commit.",
788 tile8_stamp_size_, staged_tiles.size());
789
790 return absl::OkStatus();
791}
792
793absl::Status Tile16Editor::HandleTile16CanvasClick(const ImVec2& tile_position,
794 bool left_click,
795 bool right_click) {
796 if (!left_click && !right_click) {
797 return absl::OkStatus();
798 }
799
800 if (right_click) {
801 RETURN_IF_ERROR(PickTile8FromTile16(tile_position));
802 util::logf("Picked tile8 from tile16 at (%d, %d)",
803 static_cast<int>(tile_position.x),
804 static_cast<int>(tile_position.y));
805 return absl::OkStatus();
806 }
807
808 switch (edit_mode_) {
810 // Pass nullptr to let DrawToCurrentTile16 handle flipping and store
811 // correct TileInfo metadata. The preview bitmap is pre-flipped for
812 // display only.
813 RETURN_IF_ERROR(DrawToCurrentTile16(tile_position, nullptr));
814 break;
817 RETURN_IF_ERROR(PickTile8FromTile16(tile_position));
818 break;
819 }
820
821 return absl::OkStatus();
822}
823
825 const ImVec2& display_position) {
826 return ImVec2(display_position.x / kTile8DisplayScale,
827 display_position.y / kTile8DisplayScale);
828}
829
831 static bool show_advanced_controls = false;
832 static bool show_debug_info = false;
833
834 // Modern header with improved styling
835 gui::StyleVarGuard header_var_guard(
836 {{ImGuiStyleVar_FramePadding, ImVec2(8, 4)},
837 {ImGuiStyleVar_ItemSpacing, ImVec2(8, 4)}});
838
839 const bool has_pending = has_pending_changes();
840 const bool current_tile_pending = is_tile_modified(current_tile16_);
841 const int pending_count = pending_changes_count();
842
843 RETURN_IF_ERROR(DrawCompactActionStatusRow(has_pending, current_tile_pending,
844 pending_count, &show_debug_info,
845 &show_advanced_controls));
846
847 ImGui::Separator();
848
849 const float layout_width = ImGui::GetContentRegionAvail().x;
850 const bool compact_layout = layout_width < 1040.0f;
851 const int layout_columns = compact_layout ? 1 : 3;
852
853 // Responsive workbench layout. Wide windows keep source/blockset/edit rail
854 // side-by-side; narrow docks stack rows instead of clipping the Tile8 sheet.
855 if (ImGui::BeginTable(
856 "##Tile16EditLayout", layout_columns,
857 ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) {
858 if (compact_layout) {
859 ImGui::TableSetupColumn("Tile16 Workbench",
860 ImGuiTableColumnFlags_WidthStretch, 1.0f);
861 } else {
862 ImGui::TableSetupColumn("Tile16 Blockset",
863 ImGuiTableColumnFlags_WidthStretch, 0.30f);
864 ImGui::TableSetupColumn("Tile8 Source",
865 ImGuiTableColumnFlags_WidthStretch, 0.48f);
866 ImGui::TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthStretch,
867 0.22f);
868 }
869
870 ImGui::TableNextRow();
871
872 // ========== COLUMN 1: Tile16 Blockset ==========
873 ImGui::TableNextColumn();
874 ImGui::BeginGroup();
875
876 ImGui::Text(tr("Tile16 Blockset"));
877 ImGui::SameLine();
878
879 // Show current tile and total tiles
881 ImGui::TextDisabled(tr("0x%03X / 0x%03X"), current_tile16_,
882 std::max(0, total_tiles - 1));
883
884 // Navigation controls row
885 {
886 gui::StyleVarGuard nav_spacing_guard(ImGuiStyleVar_ItemSpacing,
887 ImVec2(4, 4));
888
889 // Jump to Tile ID input - live navigation as user types
890 ImGui::SetNextItemWidth(80);
891 if (ImGui::InputInt("##JumpToTile", &jump_to_tile_id_, 0, 0)) {
892 // Clamp to valid range
893 jump_to_tile_id_ = std::clamp(jump_to_tile_id_, 0, total_tiles - 1);
896 scroll_to_current_ = true;
897 }
898 }
899 if (ImGui::IsItemHovered()) {
900 ImGui::SetTooltip(tr("Tile ID (0-%d) - navigates as you type"),
901 total_tiles - 1);
902 }
903
904 // Page navigation
905 int total_pages = (total_tiles + kTilesPerPage - 1) / kTilesPerPage;
907
908 ImGui::SameLine();
909 if (ImGui::Button("<<")) {
911 scroll_to_current_ = true;
912 }
913 if (ImGui::IsItemHovered())
914 ImGui::SetTooltip(tr("First page"));
915
916 ImGui::SameLine();
917 if (ImGui::Button("<")) {
918 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
919 RequestTileSwitch(new_tile);
920 scroll_to_current_ = true;
921 }
922 if (ImGui::IsItemHovered())
923 ImGui::SetTooltip(tr("Previous page (PageUp)"));
924
925 ImGui::SameLine();
926 ImGui::TextDisabled(tr("Page %d/%d"), current_page_ + 1, total_pages);
927
928 ImGui::SameLine();
929 if (ImGui::Button(">")) {
930 int new_tile =
931 std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
932 RequestTileSwitch(new_tile);
933 scroll_to_current_ = true;
934 }
935 if (ImGui::IsItemHovered())
936 ImGui::SetTooltip(tr("Next page (PageDown)"));
937
938 ImGui::SameLine();
939 if (ImGui::Button(">>")) {
940 RequestTileSwitch(total_tiles - 1);
941 scroll_to_current_ = true;
942 }
943 if (ImGui::IsItemHovered())
944 ImGui::SetTooltip(tr("Last page"));
945
946 // Handle keyboard shortcuts for page navigation
947 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
948 if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
949 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
950 RequestTileSwitch(new_tile);
951 scroll_to_current_ = true;
952 }
953 if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
954 int new_tile =
955 std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
956 RequestTileSwitch(new_tile);
957 scroll_to_current_ = true;
958 }
959 if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
961 scroll_to_current_ = true;
962 }
963 if (ImGui::IsKeyPressed(ImGuiKey_End)) {
964 RequestTileSwitch(total_tiles - 1);
965 scroll_to_current_ = true;
966 }
967
968 // Arrow keys for single-tile navigation (when Ctrl not held)
969 if (!ImGui::GetIO().KeyCtrl) {
970 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
971 if (current_tile16_ > 0) {
973 scroll_to_current_ = true;
974 }
975 }
976 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
977 if (current_tile16_ < total_tiles - 1) {
979 scroll_to_current_ = true;
980 }
981 }
982 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
985 scroll_to_current_ = true;
986 }
987 }
988 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
989 if (current_tile16_ + kTilesPerRow < total_tiles) {
991 scroll_to_current_ = true;
992 }
993 }
994 }
995 }
996
997 } // nav_spacing_guard scope
998
999 // Blockset canvas with scrolling
1000 const float blockset_height =
1001 compact_layout
1002 ? std::min(
1003 300.0f,
1004 std::max(200.0f, ImGui::GetContentRegionAvail().y * 0.38f))
1005 : ImGui::GetContentRegionAvail().y;
1006 if (BeginChild("##BlocksetScrollable", ImVec2(0, blockset_height), true,
1007 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1010 }
1011
1012 if (scroll_to_current_) {
1014 scroll_to_current_ = false;
1015 }
1016
1017 // Configure canvas frame options for blockset
1018 gui::CanvasFrameOptions blockset_frame_opts;
1019 blockset_frame_opts.draw_grid = show_tile_grid_;
1020 blockset_frame_opts.grid_step = 32.0f;
1021 blockset_frame_opts.draw_context_menu = true;
1022 blockset_frame_opts.draw_overlay = true;
1023 blockset_frame_opts.render_popups = true;
1024 blockset_frame_opts.use_child_window = false;
1025
1026 auto blockset_rt =
1027 gui::BeginCanvas(blockset_canvas_, blockset_frame_opts);
1028
1029 if (tile16_blockset_bmp_ != nullptr) {
1030 auto result = blockset_selector_.Render(
1032 if (result.selection_changed) {
1033 RequestTileSwitch(result.selected_tile);
1034 util::logf("Selected Tile16 from blockset: %d", result.selected_tile);
1035 }
1036 }
1038
1039 gui::EndCanvas(blockset_canvas_, blockset_rt, blockset_frame_opts);
1040 }
1041 EndChild();
1042 ImGui::EndGroup();
1043
1044 // ========== COLUMN 2: Tile8 Source ==========
1045 ImGui::TableNextColumn();
1046 ImGui::BeginGroup();
1047 RETURN_IF_ERROR(DrawTile8SourcePanel(compact_layout ? 320.0f : 0.0f));
1048 ImGui::EndGroup();
1049
1050 // ========== COLUMN 3: Tile16 Editor + Controls ==========
1051 TableNextColumn();
1052 ImGui::BeginGroup();
1053
1054 // Fixed size container to prevent canvas expansion
1055 if (ImGui::BeginChild("##Tile16FixedCanvas", ImVec2(90, 90), true,
1056 ImGuiWindowFlags_NoScrollbar |
1057 ImGuiWindowFlags_NoScrollWithMouse)) {
1058 // Configure canvas frame options for tile16 editor
1059 gui::CanvasFrameOptions tile16_edit_frame_opts;
1060 tile16_edit_frame_opts.canvas_size = ImVec2(64, 64);
1061 tile16_edit_frame_opts.draw_grid = show_tile_grid_;
1062 tile16_edit_frame_opts.grid_step =
1063 kTile8Size *
1064 kTile8DisplayScale; // 8x8 source pixels at 4x preview scale.
1065 tile16_edit_frame_opts.draw_context_menu = true;
1066 tile16_edit_frame_opts.draw_overlay = true;
1067 tile16_edit_frame_opts.render_popups = true;
1068 tile16_edit_frame_opts.use_child_window = false;
1069
1070 auto tile16_edit_rt =
1071 gui::BeginCanvas(tile16_edit_canvas_, tile16_edit_frame_opts);
1072
1073 // Draw current tile16 bitmap with dynamic zoom
1077 }
1078
1079 // Handle tile8 painting with improved hover preview
1080 if (current_tile8_ >= 0 &&
1081 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
1082 // Create a display tile that shows the current palette selection
1084 tile8_preview_bmp_.Create(8, 8, 8,
1085 std::vector<uint8_t>(kTile8PixelCount, 0));
1086 }
1087
1088 // Get the original pixel data (already has sheet offsets from
1089 // ProcessGraphicsBuffer)
1090 auto& preview_data = tile8_preview_bmp_.mutable_data();
1091 std::copy(current_gfx_individual_[current_tile8_].begin(),
1093 preview_data.begin());
1094
1095 // Apply the correct sheet-aware palette slice for the preview
1096 const gfx::SnesPalette* display_palette = nullptr;
1097 if (overworld_palette_.size() >= 256) {
1098 display_palette = &overworld_palette_;
1099 } else if (palette_.size() >= 256) {
1100 display_palette = &palette_;
1101 } else {
1102 display_palette =
1104 }
1105
1106 if (display_palette && !display_palette->empty()) {
1108 *display_palette, current_palette_));
1109 }
1110
1111 // Apply flips if needed
1112 if (x_flip || y_flip) {
1113 auto& data = tile8_preview_bmp_.mutable_data();
1114
1115 if (x_flip) {
1116 for (int y = 0; y < 8; ++y) {
1117 for (int x = 0; x < 4; ++x) {
1118 std::swap(data[y * 8 + x], data[y * 8 + (7 - x)]);
1119 }
1120 }
1121 }
1122
1123 if (y_flip) {
1124 for (int y = 0; y < 4; ++y) {
1125 for (int x = 0; x < 8; ++x) {
1126 std::swap(data[y * 8 + x], data[(7 - y) * 8 + x]);
1127 }
1128 }
1129 }
1130 }
1131
1132 // Push pixel changes to the existing surface before queuing texture work
1134
1135 // Queue texture creation/update on the persistent preview bitmap to
1136 // avoid dangling stack pointers in the arena queue
1137 const auto preview_command =
1141 gfx::Arena::Get().QueueTextureCommand(preview_command,
1143
1144 // CRITICAL FIX: Handle tile painting with simple click instead of
1145 // click+drag Draw the preview first
1148
1149 const bool left_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
1150 const bool right_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
1151
1152 if (left_clicked || right_clicked) {
1153 const ImGuiIO& io = ImGui::GetIO();
1154 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
1155 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
1156 io.MousePos.y - canvas_pos.y);
1157
1158 // The hover tile painter and bitmap share a 0,0 origin, matching
1159 // ZScream/HMagic's direct preview hit-test behavior.
1160 const ImVec2 tile_position =
1162 int tile_x = static_cast<int>(tile_position.x);
1163 int tile_y = static_cast<int>(tile_position.y);
1164
1165 // Clamp to valid range (0-15 for 16x16 tile)
1166 tile_x = std::max(0, std::min(15, tile_x));
1167 tile_y = std::max(0, std::min(15, tile_y));
1168
1169 util::logf(
1170 "Tile16 canvas click: (%.2f, %.2f) -> Tile16: (%d, %d), mode=%s",
1171 mouse_pos.x, mouse_pos.y, tile_x, tile_y,
1172 EditModeLabel(edit_mode_));
1173
1174 RETURN_IF_ERROR(HandleTile16CanvasClick(ImVec2(tile_x, tile_y),
1175 left_clicked, right_clicked));
1176 }
1177 }
1178
1179 gui::EndCanvas(tile16_edit_canvas_, tile16_edit_rt,
1180 tile16_edit_frame_opts);
1181 }
1182 ImGui::EndChild();
1183
1184 Separator();
1185
1186 // === Compact Controls Section ===
1187
1188 // Tile8 info and preview
1189 if (current_tile8_ >= 0 &&
1190 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
1191 Text(tr("Tile8: %02X"), current_tile8_);
1192 SameLine();
1193 auto* tile8_texture = tile8_preview_bmp_.texture();
1194 if (tile8_texture) {
1195 ImGui::Image((ImTextureID)(intptr_t)tile8_texture, ImVec2(24, 24));
1196 }
1197
1198 const int sheet_idx = GetSheetIndexForTile8(current_tile8_);
1199 ImGui::SameLine();
1200 ImGui::TextDisabled(tr("G%d"), sheet_idx);
1201 if (ImGui::IsItemHovered()) {
1202 ImGui::BeginTooltip();
1203 ImGui::Text(tr("Graphics chunk: %d"), sheet_idx);
1204 ImGui::EndTooltip();
1205 }
1206 }
1207
1208 // Tile8 transform options in compact form
1209 Checkbox(tr("X Flip"), &x_flip);
1210 SameLine();
1211 Checkbox(tr("Y Flip"), &y_flip);
1212 SameLine();
1213 Checkbox(tr("Priority"), &priority_tile);
1214
1215 Text(tr("Stamp:"));
1216 SameLine();
1217 if (ImGui::RadioButton(tr("1x"), tile8_stamp_size_ == 1)) {
1219 }
1220 SameLine();
1221 if (ImGui::RadioButton(tr("2x"), tile8_stamp_size_ == 2)) {
1223 }
1224 SameLine();
1225 if (ImGui::RadioButton(tr("4x"), tile8_stamp_size_ == 4)) {
1227 }
1228 HOVER_HINT(
1229 "1x: paint one quadrant\n2x: fill current tile16 from a 2x2 tile8 "
1230 "block\n4x: stamp a 2x2 tile16 patch from a 4x4 tile8 block");
1231
1232 Text(tr("Edit Mode:"));
1233 if (ImGui::RadioButton(tr("Paint (P)"),
1236 }
1237 SameLine();
1238 if (ImGui::RadioButton(tr("Pick (I)"),
1241 }
1242 SameLine();
1243 if (ImGui::RadioButton(tr("Usage (U)"),
1246 }
1247 HOVER_HINT(
1248 "Paint: left-click places Tile8 into Tile16.\n"
1249 "Pick: left-click samples Tile8 from Tile16.\n"
1250 "Usage: keeps usage overlay visible and samples on click.\n"
1251 "Right-click on Tile16 preview always samples.");
1252
1253 Separator();
1254
1256
1257 Separator();
1258
1260
1261 // Advanced controls (collapsible)
1262 if (show_advanced_controls) {
1263 Separator();
1264 Text(tr("Advanced:"));
1265
1266 if (Button(tr("Palette Settings"), ImVec2(-1, 0))) {
1268 }
1269
1270 if (Button(tr("Analyze Data"), ImVec2(-1, 0))) {
1272 }
1273 HOVER_HINT("Analyze tile8 source data format and palette state");
1274
1275 if (Button(tr("Manual Edit"), ImVec2(-1, 0))) {
1276 ImGui::OpenPopup("ManualTile8Editor");
1277 }
1278
1279 if (Button(tr("Refresh Blockset"), ImVec2(-1, 0))) {
1281 }
1282
1283 // Scratch space in compact form
1284 Text(tr("Scratch:"));
1286
1287 // Manual tile8 editor popup
1289 }
1290
1291 // Compact debug information panel
1292 if (show_debug_info) {
1293 Separator();
1294 Text(tr("Debug:"));
1295 ImGui::TextDisabled(tr("T16:%02X T8:%d Pal:%d"), current_tile16_,
1297
1298 if (current_tile8_ >= 0) {
1299 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1300 int actual_slot = GetActualPaletteSlot(current_palette_, sheet_index);
1301 ImGui::TextDisabled(tr("Sheet:%d Slot:%d"), sheet_index, actual_slot);
1302 }
1303
1304 // Compact palette mapping table
1305 if (ImGui::CollapsingHeader(tr("Palette Map"),
1306 ImGuiTreeNodeFlags_DefaultOpen)) {
1307 ImGui::BeginChild("##PaletteMappingScroll", ImVec2(0, 120), true);
1308 if (ImGui::BeginTable("##PalMap", 2,
1309 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1310 ImGuiTableFlags_SizingFixedFit)) {
1311 ImGui::TableSetupColumn("Btn", ImGuiTableColumnFlags_WidthFixed, 30);
1312 ImGui::TableSetupColumn("CGRAM", ImGuiTableColumnFlags_WidthFixed,
1313 70);
1314 ImGui::TableHeadersRow();
1315
1316 for (int i = 0; i < 8; ++i) {
1317 ImGui::TableNextRow();
1318 ImGui::TableNextColumn();
1319 ImGui::Text("%d", i);
1320 ImGui::TableNextColumn();
1321 ImGui::Text(tr("0x%02X"), GetActualPaletteSlot(i, 0));
1322 }
1323 ImGui::EndTable();
1324 }
1325 ImGui::EndChild();
1326 }
1327
1328 // Color preview - compact
1329 if (ImGui::CollapsingHeader(tr("Colors"))) {
1330 if (overworld_palette_.size() >= 256) {
1331 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1332 ImGui::Text(tr("Slot %d:"), actual_slot);
1333
1334 for (int i = 0;
1335 i < 8 &&
1336 (actual_slot + i) < static_cast<int>(overworld_palette_.size());
1337 ++i) {
1338 int color_index = actual_slot + i;
1339 auto color = overworld_palette_[color_index];
1340 ImVec4 display_color = color.rgb();
1341
1342 ImGui::ColorButton(absl::StrFormat("##c%d", i).c_str(),
1343 display_color, ImGuiColorEditFlags_NoTooltip,
1344 ImVec2(20, 20));
1345 if (ImGui::IsItemHovered()) {
1346 ImGui::SetTooltip(tr("%d:0x%04X"), color_index, color.snes());
1347 }
1348
1349 if ((i + 1) % 4 != 0)
1350 ImGui::SameLine();
1351 }
1352 }
1353 }
1354 }
1355
1356 ImGui::EndGroup();
1357 EndTable();
1358 }
1359
1360 // Draw palette settings and canvas popups
1362
1363 // Show canvas popup windows if opened from context menu
1370
1371 return absl::OkStatus();
1372}
1373
1375 bool has_pending, bool current_tile_pending, int pending_count,
1376 bool* show_debug_info, bool* show_advanced_controls) {
1378 has_pending, current_tile_pending, undo_manager_.CanUndo(),
1380 const float available_width = ImGui::GetContentRegionAvail().x;
1381 const int action_count = 7;
1382 const int columns = ComputeTile16CompactActionColumnCount(available_width);
1383 const int rows = ComputeTile16ActionRowCount(action_count, columns);
1384 const float row_height = ImGui::GetTextLineHeightWithSpacing() +
1385 rows * ImGui::GetFrameHeightWithSpacing() +
1386 ImGui::GetStyle().FramePadding.y * 2.0f;
1387 absl::Status action_status = absl::OkStatus();
1388
1389 if (ImGui::BeginChild(
1390 "##Tile16CompactStatus", ImVec2(0, row_height), false,
1391 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
1392 ImGui::Text(tr("Tile16 0x%03X"), current_tile16_);
1393 ImGui::SameLine();
1394 if (has_pending) {
1395 ImGui::TextDisabled(tr("%s, %d pending"),
1396 current_tile_pending ? "dirty" : "clean",
1397 pending_count);
1398 } else {
1399 ImGui::TextDisabled(tr("clean"));
1400 }
1401 ImGui::SameLine();
1402 ImGui::TextDisabled("| %s", EditModeLabel(edit_mode_));
1403 if (show_debug_info != nullptr && *show_debug_info &&
1405 const auto seconds_since_write =
1406 std::chrono::duration_cast<std::chrono::seconds>(
1407 std::chrono::steady_clock::now() - last_rom_write_time_)
1408 .count();
1409 ImGui::SameLine();
1410 ImGui::TextDisabled(tr("| Last write: %d tile%s, %lds ago"),
1412 last_rom_write_count_ == 1 ? "" : "s",
1413 static_cast<long>(seconds_since_write));
1414 }
1415
1416 if (ImGui::BeginTable("##Tile16CompactActions", columns,
1417 ImGuiTableFlags_SizingStretchProp)) {
1418 int action_index = 0;
1419 auto next_action_cell = [&]() {
1420 if (action_index % columns == 0) {
1421 ImGui::TableNextRow();
1422 }
1423 ImGui::TableNextColumn();
1424 ++action_index;
1425 };
1426
1427 next_action_cell();
1428 if (!action_state.can_write_pending) {
1429 ImGui::BeginDisabled();
1430 }
1431 if (gui::SuccessButton("Write Pending",
1432 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
1433 action_status = CommitAllChanges();
1434 }
1435 if (!action_state.can_write_pending) {
1436 ImGui::EndDisabled();
1437 }
1438 if (ImGui::IsItemHovered()) {
1439 ImGui::SetTooltip(tr("Write all %d pending Tile16 edits to ROM"),
1440 pending_count);
1441 }
1442
1443 next_action_cell();
1444 if (!action_state.can_discard_current) {
1445 ImGui::BeginDisabled();
1446 }
1447 if (ImGui::Button(tr("Discard Current"),
1448 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
1450 }
1451 if (!action_state.can_discard_current) {
1452 ImGui::EndDisabled();
1453 }
1454
1455 next_action_cell();
1456 if (!action_state.can_discard_all) {
1457 ImGui::BeginDisabled();
1458 }
1459 if (gui::DangerButton("Discard All",
1460 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
1462 }
1463 if (!action_state.can_discard_all) {
1464 ImGui::EndDisabled();
1465 }
1466
1467 next_action_cell();
1468 if (!action_state.can_undo) {
1469 ImGui::BeginDisabled();
1470 }
1471 if (ImGui::Button(tr("Undo"),
1472 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
1473 action_status = Undo();
1474 }
1475 if (!action_state.can_undo) {
1476 ImGui::EndDisabled();
1477 }
1478
1479 next_action_cell();
1480 if (!action_state.can_redo) {
1481 ImGui::BeginDisabled();
1482 }
1483 if (ImGui::Button(tr("Redo"),
1484 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
1485 action_status = Redo();
1486 }
1487 if (!action_state.can_redo) {
1488 ImGui::EndDisabled();
1489 }
1490
1491 next_action_cell();
1492 const char* advanced_label = *show_advanced_controls
1493 ? "Advanced*##Tile16AdvancedToggle"
1494 : "Advanced##Tile16AdvancedToggle";
1495 if (ImGui::Button(advanced_label,
1496 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
1497 *show_advanced_controls = !*show_advanced_controls;
1498 }
1499
1500 next_action_cell();
1501 const char* debug_label = *show_debug_info ? "Debug*##Tile16DebugToggle"
1502 : "Debug##Tile16DebugToggle";
1503 if (ImGui::Button(debug_label,
1504 ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
1505 *show_debug_info = !*show_debug_info;
1506 }
1507
1508 ImGui::EndTable();
1509 }
1510 }
1511 ImGui::EndChild();
1512 return action_status;
1513}
1514
1516 bool show_debug_info) {
1517 // Palette selector - this is the paint brush palette for new placements.
1518 Text(tr("Brush Palette:"));
1519 if (show_debug_info) {
1520 SameLine();
1521 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1522 ImGui::TextDisabled(tr("(Slot %d)"), actual_slot);
1523 }
1524 ImGui::TextDisabled(tr("Used for new tile8 placements"));
1525
1526 // Compact palette grid
1527 ImGui::BeginGroup();
1528 float available_width = ImGui::GetContentRegionAvail().x;
1529 float button_size = ComputeTile16PaletteButtonSize(available_width);
1530 const gui::ButtonColorSet default_button_colors{
1531 gui::GetThemeColor(ImGuiCol_Button),
1532 gui::GetThemeColor(ImGuiCol_ButtonHovered),
1533 gui::GetThemeColor(ImGuiCol_ButtonActive),
1534 };
1535 const gui::ButtonColorSet selected_button_colors =
1537
1538 for (int row = 0; row < 2; ++row) {
1539 for (int col = 0; col < 4; ++col) {
1540 if (col > 0)
1541 ImGui::SameLine();
1542
1543 int i = row * 4 + col;
1544 bool is_current = (current_palette_ == i);
1545
1546 // Modern button styling with better visual hierarchy
1547 ImGui::PushID(i);
1548
1549 const gui::ButtonColorSet& palette_button_colors =
1550 is_current ? selected_button_colors : default_button_colors;
1551 const ImVec4 palette_button_border =
1552 is_current ? gui::GetSuccessColor()
1553 : gui::GetThemeColor(ImGuiCol_Border);
1554 gui::StyleColorGuard palette_btn_colors(
1555 {{ImGuiCol_Button, palette_button_colors.button},
1556 {ImGuiCol_ButtonHovered, palette_button_colors.hovered},
1557 {ImGuiCol_ButtonActive, palette_button_colors.active},
1558 {ImGuiCol_Border, palette_button_border}});
1559 gui::StyleVarGuard palette_btn_border(ImGuiStyleVar_FrameBorderSize,
1560 1.0f);
1561
1562 if (ImGui::Button(absl::StrFormat("%d", i).c_str(),
1563 ImVec2(button_size, button_size))) {
1564 if (current_palette_ != i) {
1565 current_palette_ = i;
1566 auto status = RefreshAllPalettes();
1567 if (!status.ok()) {
1568 util::logf("Failed to refresh palettes: %s",
1569 status.message().data());
1570 } else {
1571 util::logf("Palette successfully changed to %d", current_palette_);
1572 }
1573 }
1574 }
1575
1576 ImGui::PopID();
1577
1578 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
1579 current_palette_ = static_cast<uint8_t>(i);
1581 }
1582
1583 // Tooltip with palette info
1584 if (ImGui::IsItemHovered()) {
1585 ImGui::BeginTooltip();
1586 if (show_debug_info) {
1587 ImGui::Text(tr("Palette %d -> CGRAM 0x%02X"), i,
1588 GetActualPaletteSlot(i, 0));
1589 } else {
1590 ImGui::Text(tr("Brush Palette %d"), i);
1591 ImGui::TextDisabled(tr("Applied to new tile8 placements"));
1592 ImGui::TextDisabled(tr("RMB: apply to all tile quadrants"));
1593 ImGui::TextDisabled(tr("Quadrant metadata is shown in strip below"));
1594 ImGui::TextDisabled(
1595 tr("Hotkeys: Ctrl+1..8 palette, 1..4 quadrant focus"));
1596 if (is_current) {
1598 }
1599 }
1600 ImGui::EndTooltip();
1601 }
1602 }
1603 }
1604 ImGui::EndGroup();
1605
1606 if (auto* tile_data = GetCurrentTile16Data(); tile_data != nullptr) {
1607 active_quadrant_ = std::clamp(active_quadrant_, 0, 3);
1608 Text(tr("Quadrant Focus:"));
1609 SameLine();
1610 ImGui::TextDisabled("1-4");
1611 static constexpr std::array<const char*, 4> kQuadrantLabels = {"TL", "TR",
1612 "BL", "BR"};
1613 const float quadrant_button_width = std::max(58.0f, button_size + 24.0f);
1614 const gui::ButtonColorSet active_quadrant_colors =
1616 for (int q = 0; q < 4; ++q) {
1617 if (q > 0) {
1618 SameLine();
1619 }
1620
1621 const gfx::TileInfo& info = TileInfoForQuadrant(*tile_data, q);
1622 const uint8_t quadrant_palette = info.palette_;
1623 const bool is_active_quadrant = (active_quadrant_ == q);
1624 const bool matches_brush = (quadrant_palette == current_palette_);
1625
1626 ImGui::PushID(100 + q);
1627 const gui::ButtonColorSet& quadrant_button_colors =
1628 is_active_quadrant ? active_quadrant_colors
1629 : (matches_brush ? selected_button_colors
1630 : default_button_colors);
1631 const ImVec4 quadrant_border =
1632 is_active_quadrant
1634 : (matches_brush ? gui::GetSuccessColor()
1635 : gui::GetThemeColor(ImGuiCol_Border));
1636 gui::StyleColorGuard quadrant_btn_colors(
1637 {{ImGuiCol_Button, quadrant_button_colors.button},
1638 {ImGuiCol_ButtonHovered, quadrant_button_colors.hovered},
1639 {ImGuiCol_ButtonActive, quadrant_button_colors.active},
1640 {ImGuiCol_Border, quadrant_border}});
1641 gui::StyleVarGuard quadrant_btn_border(ImGuiStyleVar_FrameBorderSize,
1642 is_active_quadrant ? 2.0f : 1.0f);
1643
1644 if (ImGui::Button(absl::StrFormat("%d %s:%d", q + 1, kQuadrantLabels[q],
1645 quadrant_palette)
1646 .c_str(),
1647 ImVec2(quadrant_button_width, 0))) {
1648 active_quadrant_ = q;
1649 if (current_palette_ != quadrant_palette) {
1650 current_palette_ = quadrant_palette;
1651 auto status = RefreshAllPalettes();
1652 if (!status.ok()) {
1653 util::logf("Failed to refresh palettes: %s",
1654 status.message().data());
1655 }
1656 }
1657 }
1658
1659 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
1660 active_quadrant_ = q;
1662 }
1663
1664 if (ImGui::IsItemHovered()) {
1665 ImGui::BeginTooltip();
1666 ImGui::Text(tr("Quadrant %s metadata"), kQuadrantLabels[q]);
1667 ImGui::Separator();
1668 ImGui::Text(tr("Tile8: %02X"), info.id_);
1669 ImGui::Text(tr("Palette: %d"), quadrant_palette);
1670 ImGui::Text(tr("Flip: H:%s V:%s"), info.horizontal_mirror_ ? "Y" : "N",
1671 info.vertical_mirror_ ? "Y" : "N");
1672 ImGui::Text(tr("Priority: %s"), info.over_ ? "Y" : "N");
1673 ImGui::TextDisabled(tr("LMB: set brush palette from this quadrant"));
1674 ImGui::TextDisabled(
1675 tr("RMB: apply current brush palette to this quadrant"));
1676 ImGui::TextDisabled(tr("Keys 1..4: focus TL/TR/BL/BR"));
1677 ImGui::EndTooltip();
1678 }
1679
1680 ImGui::PopID();
1681 }
1682
1683 const gfx::TileInfo& active_info =
1684 TileInfoForQuadrant(*tile_data, active_quadrant_);
1685 ImGui::TextDisabled(tr("Active %s: Tile8 %02X | P%d | H:%s V:%s | Pri:%s"),
1686 kQuadrantLabels[active_quadrant_], active_info.id_,
1687 active_info.palette_,
1688 active_info.horizontal_mirror_ ? "Y" : "N",
1689 active_info.vertical_mirror_ ? "Y" : "N",
1690 active_info.over_ ? "Y" : "N");
1691
1692 if (Button(tr("Apply Brush to Active Quadrant"), ImVec2(-1, 0))) {
1695 }
1696 HOVER_HINT(
1697 "Copy the Brush Palette into the selected quadrant metadata.\n"
1698 "Use keys 1..4 to change active quadrant quickly.");
1699 }
1700
1701 // Copy the current brush palette into all stored quadrant palette fields.
1702 if (Button(tr("Apply Brush to All Quadrants"), ImVec2(-1, 0))) {
1704 }
1705 HOVER_HINT(
1706 "Copy the Brush Palette into Tile Palette metadata for all 4 "
1707 "quadrants.\n"
1708 "Tip: right-click any brush palette button above for a one-step apply.");
1709 return absl::OkStatus();
1710}
1711
1712absl::Status Tile16Editor::DrawTile8SourcePanel(float preferred_height) {
1714 ImGui::Text(tr("Tile8 Source"));
1715 ImGui::SameLine();
1716 ImGui::Checkbox(tr("Grid##Tile16GridToggle"), &show_tile_grid_);
1717 ImGui::SameLine();
1718 if (ImGui::Checkbox(tr("IDs##Tile16CollisionIds"),
1721 }
1722
1723 int source_bitmap_height = 0;
1726 if (current_gfx_bmp_ != nullptr && current_gfx_bmp_->is_active()) {
1728 ImGui::GetContentRegionAvail().x, current_gfx_bmp_->width());
1729 source_bitmap_height = current_gfx_bmp_->height();
1733 ImGui::SameLine();
1734 ImGui::TextDisabled(tr("%.1fx"), tile8_source_display_scale_);
1735 }
1736
1737 const float panel_height = ComputeTile8SourcePanelHeight(
1738 ImGui::GetContentRegionAvail().y, source_bitmap_height,
1739 tile8_source_display_scale_, preferred_height);
1740
1741 if (BeginChild("##Tile8SourceScrollable", ImVec2(0, panel_height), true,
1742 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1743 gui::CanvasFrameOptions tile8_frame_opts;
1744 tile8_frame_opts.draw_grid = show_tile_grid_;
1745 tile8_frame_opts.grid_step =
1746 8.0f * tile8_source_display_scale_; // Tile8 grid
1747 tile8_frame_opts.draw_context_menu = true;
1748 tile8_frame_opts.draw_overlay = true;
1749 tile8_frame_opts.render_popups = true;
1750 tile8_frame_opts.use_child_window = false;
1751
1752 auto tile8_rt = gui::BeginCanvas(tile8_source_canvas_, tile8_frame_opts);
1753
1755
1756 const bool left_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
1757 const bool right_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
1758
1759 // ZScream parity: hold right-click on tile8 source to show usage overlay.
1760 const bool temporary_usage = ComputeTile8UsageHighlight(
1761 ImGui::IsItemHovered(), ImGui::IsMouseDown(ImGuiMouseButton_Right));
1763 (edit_mode_ == Tile16EditMode::kUsageProbe) || temporary_usage;
1764
1765 if (left_clicked || right_clicked) {
1768 }
1769
1770 if (current_gfx_bmp_ != nullptr) {
1773 }
1774
1775 gui::EndCanvas(tile8_source_canvas_, tile8_rt, tile8_frame_opts);
1776 }
1777 EndChild();
1778
1779 return absl::OkStatus();
1780}
1781
1782absl::Status Tile16Editor::HandleTile8SourceSelection(bool right_clicked,
1783 float display_scale) {
1784 const ImGuiIO& io = ImGui::GetIO();
1785 ImVec2 canvas_pos = tile8_source_canvas_.zero_point();
1786 ImVec2 mouse_pos =
1787 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1788
1789 const int new_tile8 = ComputeTile8IndexFromCanvasMouse(
1790 mouse_pos.x, mouse_pos.y, current_gfx_bmp_->width(),
1791 static_cast<int>(current_gfx_individual_.size()), display_scale);
1792 if (new_tile8 < 0 || new_tile8 == current_tile8_) {
1793 return absl::OkStatus();
1794 }
1795
1796 current_tile8_ = new_tile8;
1798 if (right_clicked) {
1800 }
1801 util::logf("Selected Tile8: %d", current_tile8_);
1802
1803 return absl::OkStatus();
1804}
1805
1807 // Local tile-shaping actions stay in the right column.
1808 if (Button(tr("Clear"), ImVec2(-1, 0))) {
1810 }
1811
1812 if (Button(tr("Copy"), ImVec2(-1, 0))) {
1814 }
1815
1816 if (Button(tr("Paste"), ImVec2(-1, 0))) {
1818 }
1819
1820 return absl::OkStatus();
1821}
1822
1824 if (!HasCurrentGfxBitmap() || current_gfx_bmp_->data() == nullptr) {
1825 return absl::FailedPreconditionError(
1826 "Current graphics bitmap not initialized");
1827 }
1828
1830
1831 // Calculate how many 8x8 tiles we can fit based on the current graphics
1832 // bitmap size SNES graphics are typically 128 pixels wide (16 tiles of 8
1833 // pixels each)
1834 const int tiles_per_row = current_gfx_bmp_->width() / 8;
1835 const int total_rows = current_gfx_bmp_->height() / 8;
1836 const int total_tiles = tiles_per_row * total_rows;
1837
1838 current_gfx_individual_.reserve(total_tiles);
1839
1840 // Extract individual 8x8 tiles from the graphics bitmap
1841 for (int tile_y = 0; tile_y < total_rows; ++tile_y) {
1842 for (int tile_x = 0; tile_x < tiles_per_row; ++tile_x) {
1843 zelda3::Tile8PixelData tile_data{};
1844
1845 // Extract tile data from the main graphics bitmap.
1846 // Preserve encoded palette offsets unless normalization is enabled.
1847 for (int py = 0; py < 8; ++py) {
1848 for (int px = 0; px < 8; ++px) {
1849 int src_x = tile_x * 8 + px;
1850 int src_y = tile_y * 8 + py;
1851 int src_index = src_y * current_gfx_bmp_->width() + src_x;
1852 int dst_index = py * 8 + px;
1853
1854 if (src_index < static_cast<int>(current_gfx_bmp_->size()) &&
1855 dst_index < 64) {
1856 uint8_t pixel_value = current_gfx_bmp_->data()[src_index];
1857
1859 pixel_value &= palette_normalization_mask_;
1860 }
1861
1862 tile_data[dst_index] = pixel_value;
1863 }
1864 }
1865 }
1866
1867 current_gfx_individual_.push_back(tile_data);
1868 }
1869 }
1870
1871 // Apply current palette settings to all tiles when a display palette is ready.
1872 // Some integration/headless initialization paths populate tile graphics before
1873 // the overworld palette is available; defer palette refresh in that case.
1874 if (rom_) {
1875 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
1876 if (display_palette && !display_palette->empty()) {
1878 } else {
1879 util::logf(
1880 "LoadTile8: display palette not available yet; deferring refresh");
1881 }
1882 }
1883
1884 // Ensure canvas scroll size matches the full tilesheet at preview scale
1888
1889 util::logf("Loaded %zu individual tile8 graphics",
1891 return absl::OkStatus();
1892}
1893
1894absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
1895 if (tile_id < 0 || tile_id >= kTile16Count) {
1896 return absl::OutOfRangeError(
1897 absl::StrFormat("Invalid tile16 id: %d", tile_id));
1898 }
1899
1900 if (!tile16_blockset_ || !rom_) {
1901 return absl::FailedPreconditionError(
1902 "Tile16 blockset or ROM not initialized");
1903 }
1904
1905 // Commit any in-progress edits before switching the current tile selection so
1906 // undo/redo captures the correct "after" state.
1908
1909 current_tile16_ = tile_id;
1910 jump_to_tile_id_ = tile_id; // Sync input field with current tile
1912
1913 // Load editable tile16 metadata from pending state first, then ROM. The
1914 // bitmap cache is derived data and must be rebuilt from the current Tile8
1915 // source so map/graphics refreshes cannot resurrect stale preview pixels.
1916 auto pending_it = pending_tile16_changes_.find(current_tile16_);
1917 const bool loaded_pending_metadata =
1918 pending_it != pending_tile16_changes_.end();
1919 if (pending_it != pending_tile16_changes_.end()) {
1920 current_tile16_data_ = pending_it->second;
1921 } else {
1924 }
1925 SyncTilesInfoArray(&current_tile16_data_);
1926
1928
1929 if (loaded_pending_metadata) {
1931 }
1932
1933 util::logf("SetCurrentTile: loaded tile %d successfully", tile_id);
1934
1937 }
1938 return absl::OkStatus();
1939}
1940
1941void Tile16Editor::RequestTileSwitch(int target_tile_id) {
1942 // Validate that the tile16 editor is properly initialized
1943 if (!tile16_blockset_ || !rom_) {
1944 util::logf(
1945 "RequestTileSwitch: Editor not initialized (blockset=%p, rom=%p)",
1947 return;
1948 }
1949
1950 // Validate target tile ID
1951 if (target_tile_id < 0 || target_tile_id >= kTile16Count) {
1952 util::logf("RequestTileSwitch: Invalid target tile ID %d", target_tile_id);
1953 return;
1954 }
1955
1956 // Check if we're already on this tile
1957 if (target_tile_id == current_tile16_) {
1958 return;
1959 }
1960
1961 // Check if current tile has pending changes
1963 // Store target and show dialog
1964 pending_tile_switch_target_ = target_tile_id;
1966 util::logf("Tile %d has pending changes, showing confirmation dialog",
1968 } else {
1969 // No pending changes, switch directly
1970 auto status = SetCurrentTile(target_tile_id);
1971 if (!status.ok()) {
1972 util::logf("Failed to switch to tile %d: %s", target_tile_id,
1973 status.message().data());
1974 }
1975 }
1976}
1977
1978absl::Status Tile16Editor::CopyTile16ToClipboard(int tile_id) {
1979 if (tile_id < 0 || tile_id >= kTile16Count) {
1980 return absl::InvalidArgumentError("Invalid tile ID");
1981 }
1982 if (!rom_) {
1983 return absl::FailedPreconditionError("ROM not available");
1984 }
1985
1986 auto pending_tile_it = pending_tile16_changes_.find(tile_id);
1987 if (tile_id == current_tile16_) {
1989 } else if (pending_tile_it != pending_tile16_changes_.end()) {
1990 clipboard_tile16_.tile_data = pending_tile_it->second;
1991 } else {
1994 }
1995 SyncTilesInfoArray(&clipboard_tile16_.tile_data);
1996
1997 bool bitmap_copied = false;
1998 auto pending_bitmap_it = pending_tile16_bitmaps_.find(tile_id);
1999 if (tile_id == current_tile16_ && current_tile16_bmp_.is_active()) {
2003 bitmap_copied = true;
2004 } else if (pending_bitmap_it != pending_tile16_bitmaps_.end() &&
2005 pending_bitmap_it->second.is_active()) {
2007 pending_bitmap_it->second.vector());
2008 clipboard_tile16_.bitmap.SetPalette(pending_bitmap_it->second.palette());
2009 bitmap_copied = true;
2010 } else if (tile16_blockset_) {
2011 auto tile_pixels = gfx::GetTilemapData(*tile16_blockset_, tile_id);
2012 if (!tile_pixels.empty()) {
2015 bitmap_copied = true;
2016 }
2017 }
2018
2019 if (bitmap_copied) {
2022 }
2023
2025 return absl::OkStatus();
2026}
2027
2056
2058 if (slot < 0 || slot >= kNumScratchSlots) {
2059 return absl::InvalidArgumentError("Invalid scratch space slot");
2060 }
2062 return absl::FailedPreconditionError("No active tile16 to save");
2063 }
2064
2065 scratch_space_[slot].tile_data = current_tile16_data_;
2066 SyncTilesInfoArray(&scratch_space_[slot].tile_data);
2067 scratch_space_[slot].bitmap.Create(kTile16Size, kTile16Size, 8,
2069 scratch_space_[slot].bitmap.SetPalette(current_tile16_bmp_.palette());
2070 // Queue texture creation via Arena's deferred system
2072 &scratch_space_[slot].bitmap);
2073
2074 scratch_space_[slot].has_data = true;
2075 return absl::OkStatus();
2076}
2077
2079 if (slot < 0 || slot >= kNumScratchSlots) {
2080 return absl::InvalidArgumentError("Invalid scratch space slot");
2081 }
2082
2083 if (!scratch_space_[slot].has_data) {
2084 return absl::FailedPreconditionError("Scratch space slot is empty");
2085 }
2086
2087 SaveUndoState();
2088
2089 current_tile16_data_ = scratch_space_[slot].tile_data;
2090 SyncTilesInfoArray(&current_tile16_data_);
2091
2092 if (scratch_space_[slot].bitmap.is_active()) {
2094 scratch_space_[slot].bitmap.vector());
2095 current_tile16_bmp_.SetPalette(scratch_space_[slot].bitmap.palette());
2099 } else {
2101 }
2102
2106 }
2108 return absl::OkStatus();
2109}
2110
2111absl::Status Tile16Editor::ClearScratchSpace(int slot) {
2112 if (slot < 0 || slot >= kNumScratchSlots) {
2113 return absl::InvalidArgumentError("Invalid scratch space slot");
2114 }
2115
2116 scratch_space_[slot].has_data = false;
2117 return absl::OkStatus();
2118}
2119
2120// Advanced editing features
2123 return absl::FailedPreconditionError("No active tile16 to flip");
2124 }
2125
2126 SaveUndoState();
2127
2129
2134 }
2135
2136 // Track this tile as having pending changes
2138
2139 return absl::OkStatus();
2140}
2141
2144 return absl::FailedPreconditionError("No active tile16 to flip");
2145 }
2146
2147 SaveUndoState();
2148
2150
2155 }
2156
2157 // Track this tile as having pending changes
2159
2160 return absl::OkStatus();
2161}
2162
2165 return absl::FailedPreconditionError("No active tile16 to rotate");
2166 }
2167
2168 SaveUndoState();
2169
2170 // Tile16 metadata does not support arbitrary 8x8 rotation flags.
2171 // Rotate the 2x2 quadrant layout in a persistable way.
2173
2178 }
2179
2180 // Track this tile as having pending changes
2182
2183 return absl::OkStatus();
2184}
2185
2186absl::Status Tile16Editor::FillTile16WithTile8(int tile8_id) {
2187 if (current_gfx_individual_.empty()) {
2188 if (!HasCurrentGfxBitmap()) {
2189 return absl::FailedPreconditionError("Source tile8 bitmap not active");
2190 }
2192 }
2193
2194 if (tile8_id < 0 ||
2195 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2196 return absl::InvalidArgumentError("Invalid tile8 ID");
2197 }
2198
2199 SaveUndoState();
2200
2201 const gfx::TileInfo fill_info(static_cast<uint16_t>(tile8_id),
2204 for (int quadrant = 0; quadrant < 4; ++quadrant) {
2205 TileInfoForQuadrant(&current_tile16_data_, quadrant) = fill_info;
2206 }
2207 SyncTilesInfoArray(&current_tile16_data_);
2208
2213 }
2214
2215 // Track this tile as having pending changes
2217
2218 return absl::OkStatus();
2219}
2220
2223 return absl::FailedPreconditionError("No active tile16 to clear");
2224 }
2225
2226 SaveUndoState();
2227
2228 const gfx::TileInfo clear_info(0, current_palette_, false, false, false);
2229 for (int quadrant = 0; quadrant < 4; ++quadrant) {
2230 TileInfoForQuadrant(&current_tile16_data_, quadrant) = clear_info;
2231 }
2232 SyncTilesInfoArray(&current_tile16_data_);
2233
2238 }
2239
2240 // Track this tile as having pending changes
2242
2243 return absl::OkStatus();
2244}
2245
2246// Palette management
2247absl::Status Tile16Editor::CyclePalette(bool forward) {
2248 uint8_t new_palette = current_palette_;
2249
2250 if (forward) {
2251 new_palette = (new_palette + 1) % 8;
2252 } else {
2253 new_palette = (new_palette == 0) ? 7 : new_palette - 1;
2254 }
2255
2256 current_palette_ = new_palette;
2257
2258 // Use the RefreshAllPalettes method which handles all the coordination
2260
2261 util::logf("Cycled to palette slot %d", current_palette_);
2262 return absl::OkStatus();
2263}
2264
2265absl::Status Tile16Editor::PreviewPaletteChange(uint8_t palette_id) {
2266 if (!show_palette_preview_) {
2267 return absl::OkStatus();
2268 }
2269
2270 if (palette_id >= 8) {
2271 return absl::InvalidArgumentError("Invalid palette ID");
2272 }
2273
2274 // Create a preview bitmap with the new palette
2275 if (!preview_tile16_.is_active()) {
2277 } else {
2278 // Recreate the preview bitmap with new data
2280 }
2281
2282 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
2283 if (!display_palette || display_palette->empty()) {
2284 return absl::OkStatus();
2285 }
2286
2287 const bool use_sub_palette_view =
2289 if (use_sub_palette_view) {
2290 const int sheet_index = GetSheetIndexForTile8(current_tile8_);
2291 const int palette_slot =
2292 GetActualPaletteSlot(static_cast<int>(palette_id), sheet_index);
2293 if (palette_slot >= 0 &&
2294 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
2296 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
2297 } else {
2298 preview_tile16_.SetPaletteWithTransparent(*display_palette, 1, 15);
2299 }
2300 } else {
2301 preview_tile16_.SetPalette(*display_palette);
2302 }
2303
2304 // Queue texture update via Arena's deferred system
2307 preview_dirty_ = true;
2308
2309 return absl::OkStatus();
2310}
2311
2312absl::Status Tile16Editor::ApplyPaletteToAll(uint8_t palette_id) {
2313 if (palette_id >= 8) {
2314 return absl::InvalidArgumentError("Invalid palette ID");
2315 }
2316
2317 auto* tile_data = GetCurrentTile16Data();
2318 if (!tile_data) {
2319 return absl::FailedPreconditionError("No current tile16 data");
2320 }
2321
2322 SaveUndoState();
2323 zelda3::SetTile16AllQuadrantPalettes(tile_data, palette_id);
2324
2325 // Update current palette to match
2326 current_palette_ = palette_id;
2327
2328 // Regenerate bitmap with new per-quadrant palette metadata
2330
2331 // Keep blockset/editor previews in sync with other tile16 edit operations.
2335 }
2336
2337 // Mark as modified
2339
2340 util::logf("Applied palette %d to all quadrants of tile %d", palette_id,
2342 return absl::OkStatus();
2343}
2344
2346 uint8_t palette_id) {
2347 if (palette_id >= 8) {
2348 return absl::InvalidArgumentError("Invalid palette ID");
2349 }
2350 if (quadrant < 0 || quadrant > 3) {
2351 return absl::InvalidArgumentError("Invalid quadrant index");
2352 }
2353
2354 auto* tile_data = GetCurrentTile16Data();
2355 if (!tile_data) {
2356 return absl::FailedPreconditionError("No current tile16 data");
2357 }
2358
2359 SaveUndoState();
2360 if (!zelda3::SetTile16QuadrantPalette(tile_data, quadrant, palette_id)) {
2361 return absl::InvalidArgumentError("Invalid quadrant index");
2362 }
2363 current_palette_ = palette_id;
2364
2369 }
2370
2372 util::logf("Applied palette %d to quadrant %d of tile %d", palette_id,
2373 quadrant, current_tile16_);
2374 return absl::OkStatus();
2375}
2376
2377// Undo/Redo system (unified UndoManager framework)
2378
2394
2396 if (!pending_undo_before_.has_value())
2397 return;
2399 pending_undo_before_.reset();
2400 return;
2401 }
2402
2403 // Capture the current (post-edit) state as the "after" snapshot
2404 Tile16Snapshot after;
2405 after.tile_id = current_tile16_;
2409 after.palette = current_palette_;
2410 after.x_flip = x_flip;
2411 after.y_flip = y_flip;
2412 after.priority = priority_tile;
2413
2414 // Build the restore callback that captures `this`
2415 auto restore_fn = [this](const Tile16Snapshot& snap) {
2416 RestoreFromSnapshot(snap);
2417 };
2418
2419 undo_manager_.Push(std::make_unique<Tile16EditAction>(
2420 std::move(*pending_undo_before_), std::move(after), restore_fn));
2421
2422 pending_undo_before_.reset();
2423}
2424
2427 return;
2428 }
2429
2430 // Finalize any previously pending snapshot before starting a new one
2432
2433 Tile16Snapshot before;
2434 before.tile_id = current_tile16_;
2438 before.palette = current_palette_;
2439 before.x_flip = x_flip;
2440 before.y_flip = y_flip;
2441 before.priority = priority_tile;
2442
2443 pending_undo_before_ = std::move(before);
2444}
2445
2446absl::Status Tile16Editor::Undo() {
2448 return undo_manager_.Undo();
2449}
2450
2451absl::Status Tile16Editor::Redo() {
2452 return undo_manager_.Redo();
2453}
2454
2456 if (!tile16_blockset_) {
2457 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2458 }
2459
2460 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2461 return absl::OutOfRangeError("Current tile16 ID out of range");
2462 }
2463
2464 if (current_palette_ >= 8) {
2465 return absl::OutOfRangeError("Current palette ID out of range");
2466 }
2467
2468 return absl::OkStatus();
2469}
2470
2471bool Tile16Editor::IsTile16Valid(int tile_id) const {
2472 return tile16_blockset_ != nullptr && tile_id >= 0 && tile_id < kTile16Count;
2473}
2474
2475// Integration with overworld system
2477 if (!rom_) {
2478 return absl::FailedPreconditionError("ROM not available");
2479 }
2480
2482 return absl::FailedPreconditionError("No active tile16 to save");
2483 }
2484
2485 // Write the tile16 data to ROM first
2487
2488 // Update the tile16 blockset with current changes
2490
2491 // Commit changes to the tile16 blockset
2493
2496
2497 // Mark ROM as dirty so changes persist when saving
2498 rom_->set_dirty(true);
2501 last_rom_write_time_ = std::chrono::steady_clock::now();
2503
2504 util::logf("Tile16 %d saved to ROM", current_tile16_);
2505 return absl::OkStatus();
2506}
2507
2509 if (!tile16_blockset_) {
2510 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2511 }
2512
2513 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2514 return absl::OutOfRangeError("Current tile16 ID out of range");
2515 }
2516
2517 // Update atlas directly instead of using problematic tile cache
2519
2520 return absl::OkStatus();
2521}
2522
2524 if (!tile16_blockset_) {
2525 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2526 }
2527
2528 // Regenerate the tilemap data if needed
2530 // Queue texture update via Arena's deferred system
2533 }
2534
2535 // Update individual cached tiles
2536 // Note: With the new tile cache system, tiles are automatically managed
2537 // and don't need manual modification tracking like the old system
2538 // The cache handles LRU eviction and automatic updates
2539
2540 return absl::OkStatus();
2541}
2542
2544 std::vector<Tile16Commit> commits;
2545 commits.push_back({current_tile16_, current_tile16_data_});
2546
2547 // Step 1: Update ROM data with current tile16 changes
2549
2550 // Step 2: Update the local blockset to reflect changes
2552
2553 // Step 3: Update the atlas directly
2555
2556 // Step 4: Notify the parent editor (overworld editor) to regenerate its
2557 // blockset
2560 }
2561
2566 last_rom_write_time_ = std::chrono::steady_clock::now();
2568
2569 util::logf("Committed Tile16 %d changes to overworld system",
2571 return absl::OkStatus();
2572}
2573
2575 // Drop the current tile's staged copy first; SetCurrentTile consults pending
2576 // state before ROM state.
2580
2581 // Reload the current tile16 from ROM to discard any local changes
2583
2584 util::logf("Discarded Tile16 changes for tile %d", current_tile16_);
2585 return absl::OkStatus();
2586}
2587
2589 if (pending_tile16_changes_.empty()) {
2590 return absl::OkStatus(); // Nothing to commit
2591 }
2592
2593 const int written_count = static_cast<int>(pending_tile16_changes_.size());
2594 std::vector<Tile16Commit> commits;
2595 commits.reserve(pending_tile16_changes_.size());
2596 util::logf("Committing %zu pending tile16 changes to ROM",
2598
2599 // Write all pending changes to ROM
2600 for (const auto& [tile_id, tile_data] : pending_tile16_changes_) {
2601 auto status = rom_->WriteTile16(tile_id, zelda3::kTile16Ptr, tile_data);
2602 if (!status.ok()) {
2603 util::logf("Failed to write tile16 %d: %s", tile_id,
2604 status.message().data());
2605 return status;
2606 }
2607 commits.push_back({tile_id, tile_data});
2608 }
2609
2610 // Clear pending changes before parent refresh (overworld reads committed ROM).
2613
2614 // Local atlas hint; full rebuild is typically done in overworld callback.
2616
2617 // Notify parent editor to refresh overworld display
2620 }
2621
2622 rom_->set_dirty(true);
2624 last_rom_write_count_ = written_count;
2625 last_rom_write_time_ = std::chrono::steady_clock::now();
2627 util::logf("All pending tile16 changes committed successfully");
2628 return absl::OkStatus();
2629}
2630
2632 if (pending_tile16_changes_.empty()) {
2633 return;
2634 }
2635
2636 util::logf("Discarding %zu pending tile16 changes",
2638
2642
2643 // Reload current tile to restore original state
2644 auto status = SetCurrentTile(current_tile16_);
2645 if (!status.ok()) {
2646 util::logf("Failed to reload tile after discard: %s",
2647 status.message().data());
2648 }
2649}
2650
2653 if (it != pending_tile16_changes_.end()) {
2654 pending_tile16_changes_.erase(it);
2657 util::logf("Discarded pending changes for tile %d", current_tile16_);
2658 }
2659
2660 // Reload tile from ROM
2661 auto status = SetCurrentTile(current_tile16_);
2662 if (!status.ok()) {
2663 util::logf("Failed to reload tile after discard: %s",
2664 status.message().data());
2665 }
2666}
2667
2669 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2670 return;
2671 }
2672
2673 SyncTilesInfoArray(&current_tile16_data_);
2676 preview_dirty_ = true;
2678
2679 util::logf("Marked tile %d as modified (total pending: %zu)", current_tile16_,
2681}
2682
2684 if (!rom_) {
2685 return absl::FailedPreconditionError("ROM not available for usage cache");
2686 }
2687
2688 const int total_tiles = zelda3::ComputeTile16Count(tile16_blockset_);
2689 auto tile_provider = [this](int tile_id) -> absl::StatusOr<gfx::Tile16> {
2690 auto pending_it = pending_tile16_changes_.find(tile_id);
2691 if (pending_it != pending_tile16_changes_.end()) {
2692 return pending_it->second;
2693 }
2694 return rom_->ReadTile16(tile_id, zelda3::kTile16Ptr);
2695 };
2696
2697 RETURN_IF_ERROR(zelda3::BuildTile8UsageIndex(total_tiles, tile_provider,
2699
2701 return absl::OkStatus();
2702}
2703
2707 return;
2708 }
2709
2711 auto cache_status = RebuildTile8UsageCache();
2712 if (!cache_status.ok()) {
2713 util::logf("Tile8 usage cache rebuild failed: %s",
2714 cache_status.message().data());
2715 return;
2716 }
2717 }
2718
2719 const auto& hits = tile8_usage_cache_[current_tile8_];
2720 if (hits.empty()) {
2721 return;
2722 }
2723
2724 ImDrawList* draw_list = ImGui::GetWindowDrawList();
2725 const ImVec2 canvas_pos = blockset_canvas_.zero_point();
2726 const float scale = blockset_canvas_.GetGlobalScale();
2727 const float tile16_display = 32.0f * scale;
2728 const float quadrant_display = 16.0f * scale;
2729 const int tiles_per_row =
2730 std::max(1, tile16_blockset_bmp_->width() / kTile16Size);
2731
2732 for (const auto& hit : hits) {
2733 const int tile_x = hit.tile16_id % tiles_per_row;
2734 const int tile_y = hit.tile16_id / tiles_per_row;
2735 const int quad_x = hit.quadrant % 2;
2736 const int quad_y = hit.quadrant / 2;
2737
2738 const ImVec2 min(
2739 canvas_pos.x + (tile_x * tile16_display) + (quad_x * quadrant_display),
2740 canvas_pos.y + (tile_y * tile16_display) + (quad_y * quadrant_display));
2741 const ImVec2 max(min.x + quadrant_display, min.y + quadrant_display);
2742
2743 // Mirrors ZScream's right-click usage tint (purple, transparent).
2744 draw_list->AddRectFilled(min, max, IM_COL32(150, 0, 210, 80));
2745 draw_list->AddRect(min, max, IM_COL32(215, 170, 255, 180));
2746 }
2747}
2748
2749absl::Status Tile16Editor::PickTile8FromTile16(const ImVec2& position) {
2750 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2751 return absl::InvalidArgumentError("Invalid tile16 or ROM not set");
2752 }
2753
2754 // Determine which quadrant of the tile16 was clicked
2755 int quad_x = (position.x < 8) ? 0 : 1; // Left or right half
2756 int quad_y = (position.y < 8) ? 0 : 1; // Top or bottom half
2757 int quadrant = quad_x + (quad_y * 2); // 0=TL, 1=TR, 2=BL, 3=BR
2758 active_quadrant_ = std::clamp(quadrant, 0, 3);
2759
2760 // Get the tile16 data structure
2761 auto* tile16_data = GetCurrentTile16Data();
2762 if (!tile16_data) {
2763 return absl::FailedPreconditionError("Failed to get tile16 data");
2764 }
2765
2766 // Extract tile metadata from the clicked quadrant.
2767 gfx::TileInfo tile_info = TileInfoForQuadrant(*tile16_data, quadrant);
2768
2769 // Set the current tile8 and palette
2770 current_tile8_ = tile_info.id_;
2771 current_palette_ = tile_info.palette_;
2772
2773 // Update the flip states based on the tile info
2774 x_flip = tile_info.horizontal_mirror_;
2775 y_flip = tile_info.vertical_mirror_;
2776 priority_tile = tile_info.over_;
2777
2778 // Refresh the palette to match the picked tile
2781
2782 util::logf("Picked tile8 %d with palette %d from quadrant %d of tile16 %d",
2784
2785 return absl::OkStatus();
2786}
2787
2788// Get the actual CGRAM palette slot for a Tile16 palette button.
2789// ZScream and the authoritative OverworldMap::BuildTiles16Gfx path encode
2790// Tile16 colors as `(pixel & 0x0F) + (tile.palette * 0x10)`. The graphics
2791// sheet contributes the low nibble, including the left/right half of each
2792// palette row; it does not offset the tile palette row itself.
2794 int sheet_index) const {
2795 (void)sheet_index;
2796 const int clamped_button = std::clamp(palette_button, 0, 7);
2797 return clamped_button * 16;
2798}
2799
2800// Get the graphics chunk that contains a tile8 ID.
2802 // Overworld current_gfx is packed into 0x1000-byte graphics chunks. In the
2803 // editor's 128px-wide 8bpp bitmap view, each chunk is 64 Tile8 entries.
2804 constexpr int kTile8sPerGraphicsChunk = 64;
2805 int sheet_index = tile8_id / kTile8sPerGraphicsChunk;
2806
2807 return std::min(15, std::max(0, sheet_index));
2808}
2809
2813
2815 const gfx::SnesPalette& source, int target_row) const {
2816 return CreateRemappedPaletteForTile8(source, target_row, current_tile8_);
2817}
2818
2820 const gfx::SnesPalette& source, int target_row, int tile8_id) const {
2821 // Create a remapped 256-color palette where all pixel values (0-255)
2822 // are mapped to the target palette row based on their low nibble.
2823 //
2824 // This allows the source bitmap (which has pre-encoded palette offsets)
2825 // to be viewed with the same Tile16 palette row that painting will encode.
2826 //
2827 // For each palette index i:
2828 // - Extract the color index: low_nibble = i & 0x0F
2829 // - Map to target row: target_row * 16 + low_nibble
2830 // - Copy the color from source palette at that position
2831
2832 gfx::SnesPalette remapped;
2833 (void)tile8_id;
2834 const int actual_target_row = std::clamp(target_row, 0, 7);
2835
2836 for (int i = 0; i < 256; ++i) {
2837 int low_nibble = i & 0x0F;
2838 int target_index = (actual_target_row * 16) + low_nibble;
2839
2840 // Make color 0 of each row transparent
2841 if (low_nibble == 0) {
2842 gfx::SnesColor transparent_color(0);
2843 transparent_color.set_transparent(true);
2844 remapped.AddColor(transparent_color);
2845 } else if (target_index < static_cast<int>(source.size())) {
2846 remapped.AddColor(source[target_index]);
2847 } else {
2848 // Fallback to black if out of bounds
2849 remapped.AddColor(gfx::SnesColor(0));
2850 }
2851 }
2852
2853 return remapped;
2854}
2855
2856int Tile16Editor::GetEncodedPaletteRow(uint8_t pixel_value) const {
2857 // Determine which palette row a pixel value encodes
2858 // ProcessGraphicsBuffer adds 0x88 (136) to sheets 0, 3, 4, 5
2859 // So pixel values map to rows as follows:
2860 // 0x00-0x0F (0-15): Row 0
2861 // 0x10-0x1F (16-31): Row 1
2862 // ...
2863 // 0x80-0x8F (128-143): Row 8
2864 // 0x90-0x9F (144-159): Row 9
2865 // etc.
2866 return pixel_value / 16;
2867}
2868
2870 if (overworld_palette_.size() >= 256) {
2871 return &overworld_palette_;
2872 }
2873 if (palette_.size() >= 256) {
2874 return &palette_;
2875 }
2876 if (game_data() && !game_data()->palette_groups.overworld_main.empty()) {
2878 }
2879 return nullptr;
2880}
2881
2883 const gfx::Bitmap& bitmap) const {
2884 if (!bitmap.is_active() || bitmap.data() == nullptr) {
2885 return false;
2886 }
2887
2888 for (size_t i = 0; i < bitmap.size(); ++i) {
2889 if ((bitmap.data()[i] & 0xF0) != 0) {
2890 return true;
2891 }
2892 }
2893 return false;
2894}
2895
2898 return;
2899 }
2900
2901 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
2902 gfx::SnesPalette fallback_palette;
2903 if (!display_palette || display_palette->empty()) {
2904 fallback_palette = BuildFallbackDisplayPalette();
2905 display_palette = &fallback_palette;
2906 }
2907
2908 // Most tile16 edit paths now encode the palette row directly in pixel indices
2909 // via (pixel & 0x0F) + (palette * 0x10). In that case, apply the full palette.
2910 // If normalization produced low-nibble-only pixels, keep the legacy sub-palette
2911 // view path so the advanced normalization workflow still renders correctly.
2914 const int palette_slot = GetActualPaletteSlotForCurrentTile16();
2915 if (palette_slot >= 0 &&
2916 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
2918 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
2919 } else {
2920 current_tile16_bmp_.SetPaletteWithTransparent(*display_palette, 1, 15);
2921 }
2922 } else {
2923 current_tile16_bmp_.SetPalette(*display_palette);
2924 }
2925
2929}
2930
2931// Helper methods for palette management
2932absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) {
2933 if (tile8_id < 0 ||
2934 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2935 return absl::InvalidArgumentError("Invalid tile8 ID");
2936 }
2937
2938 if (!rom_) {
2939 return absl::FailedPreconditionError("ROM not set");
2940 }
2941
2942 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
2943 if (!display_palette || display_palette->empty()) {
2944 return absl::FailedPreconditionError("No overworld palette available");
2945 }
2946
2947 // Validate current_palette_ index
2948 if (current_palette_ < 0 || current_palette_ >= 8) {
2949 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
2950 current_palette_ = 0;
2951 }
2952
2953 const int sheet_index = GetSheetIndexForTile8(tile8_id);
2954 const int palette_slot =
2955 GetActualPaletteSlot(static_cast<int>(current_palette_), sheet_index);
2956
2957 if (HasCurrentGfxBitmap()) {
2959 *display_palette, current_palette_, tile8_id));
2963 }
2964
2965 util::logf("Updated tile8 %d with palette slot %d (palette size: %zu colors)",
2966 tile8_id, palette_slot, display_palette->size());
2967
2968 return absl::OkStatus();
2969}
2970
2972 if (!rom_) {
2973 return absl::FailedPreconditionError("ROM not set");
2974 }
2975
2976 // Validate current_palette_ index
2977 if (current_palette_ < 0 || current_palette_ >= 8) {
2978 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
2979 current_palette_ = 0;
2980 }
2981
2982 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
2983 gfx::SnesPalette fallback_palette;
2984 if (!display_palette || display_palette->empty()) {
2985 fallback_palette = BuildFallbackDisplayPalette();
2986 display_palette = &fallback_palette;
2987 util::logf("Display palette unavailable; using fallback grayscale palette");
2988 }
2989 util::logf("Using resolved display palette with %zu colors",
2990 display_palette->size());
2991
2992 // The source bitmap (current_gfx_bmp_) contains 8bpp indexed pixel data with
2993 // palette rows already encoded in the high nibble. Remap the display palette
2994 // so every source index shows the currently selected brush row while the
2995 // underlying pixel data remains untouched.
2996 if (HasCurrentGfxBitmap()) {
2997 gfx::SnesPalette remapped_palette =
2999 current_gfx_bmp_->SetPalette(remapped_palette);
3000 util::logf("Applied brush palette %d to source bitmap", current_palette_);
3001
3003 // Queue texture update via Arena's deferred system
3006 }
3007
3008 // Update current tile16 being edited - regenerate from ROM so per-quadrant
3009 // palette metadata is applied via the pixel transform
3011 auto regen_status = RegenerateTile16BitmapFromROM();
3012 if (!regen_status.ok()) {
3013 // Fallback: just apply palette directly
3014 current_tile16_bmp_.SetPalette(*display_palette);
3018 }
3019 }
3020
3021 util::logf(
3022 "Successfully refreshed all palettes in tile16 editor with palette %d",
3024 return absl::OkStatus();
3025}
3026
3028 util::logf("=== TILE8 SOURCE DATA ANALYSIS ===");
3029
3030 // Analyze current_gfx_bmp_
3031 util::logf("current_gfx_bmp_:");
3032 util::logf(" - Active: %s", HasCurrentGfxBitmap() ? "yes" : "no");
3033 util::logf(" - Size: %dx%d", current_gfx_bmp_->width(),
3035 util::logf(" - Depth: %d bpp", current_gfx_bmp_->depth());
3036 util::logf(" - Data size: %zu bytes", current_gfx_bmp_->size());
3037 util::logf(" - Palette size: %zu colors",
3039
3040 // Analyze pixel value distribution in first 64 pixels (first tile8)
3041 if (current_gfx_bmp_->data() && current_gfx_bmp_->size() >= 64) {
3042 std::map<uint8_t, int> pixel_counts;
3043 for (size_t i = 0; i < 64; ++i) {
3044 uint8_t val = current_gfx_bmp_->data()[i];
3045 pixel_counts[val]++;
3046 }
3047 util::logf(" - First tile8 (Sheet 0) pixel distribution:");
3048 for (const auto& [val, count] : pixel_counts) {
3049 int row = GetEncodedPaletteRow(val);
3050 int col = val & 0x0F;
3051 util::logf(" Value 0x%02X (%3d) = Row %d, Col %d: %d pixels", val, val,
3052 row, col, count);
3053 }
3054
3055 // Check if values are in expected 4bpp range
3056 bool all_4bpp = true;
3057 for (const auto& [val, count] : pixel_counts) {
3058 if (val > 15) {
3059 all_4bpp = false;
3060 break;
3061 }
3062 }
3063 util::logf(" - Values in raw 4bpp range (0-15): %s",
3064 all_4bpp ? "yes" : "NO (pre-encoded)");
3065
3066 // Show what the remapping does
3067 util::logf(" - Palette remapping for viewing:");
3068 util::logf(" Selected palette: %d (row %d)", current_palette_,
3070 util::logf(" Pixels are remapped: (value & 0x0F) + (selected_row * 16)");
3071 }
3072
3073 // Analyze current_gfx_individual_
3074 util::logf("current_gfx_individual_:");
3075 util::logf(" - Count: %zu tiles", current_gfx_individual_.size());
3076
3077 if (!current_gfx_individual_.empty()) {
3078 const auto& first_tile = current_gfx_individual_[0];
3079 util::logf(" - First tile:");
3080 util::logf(" - Size: 8x8");
3081 util::logf(" - Depth: 8 bpp");
3082 std::map<uint8_t, int> pixel_counts;
3083 for (uint8_t val : first_tile) {
3084 pixel_counts[val]++;
3085 }
3086 util::logf(" - Pixel distribution:");
3087 for (const auto& [val, count] : pixel_counts) {
3088 util::logf(" Value 0x%02X (%3d): %d pixels", val, val, count);
3089 }
3090 }
3091
3092 // Analyze palette state
3093 util::logf("Palette state:");
3094 util::logf(" - current_palette_: %d", current_palette_);
3095 util::logf(" - overworld_palette_ size: %zu", overworld_palette_.size());
3096 util::logf(" - palette_ size: %zu", palette_.size());
3097
3098 // Calculate expected palette slot
3099 int palette_slot = GetActualPaletteSlot(current_palette_, 0);
3100 util::logf(" - GetActualPaletteSlot(%d, 0) = %d", current_palette_,
3101 palette_slot);
3102 util::logf(" - Expected palette offset for SetPaletteWithTransparent: %d",
3103 palette_slot + 1);
3104
3105 // Show first 16 colors of the overworld palette
3106 if (overworld_palette_.size() >= 16) {
3107 util::logf(" - First 16 palette colors (row 0):");
3108 for (int i = 0; i < 16; ++i) {
3109 auto color = overworld_palette_[i];
3110 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3111 static_cast<int>(color.rgb().x),
3112 static_cast<int>(color.rgb().y),
3113 static_cast<int>(color.rgb().z));
3114 }
3115 }
3116
3117 // Show colors at the selected palette slot
3118 if (overworld_palette_.size() >= static_cast<size_t>(palette_slot + 16)) {
3119 util::logf(" - Colors at palette slot %d (row %d):", palette_slot,
3120 palette_slot / 16);
3121 for (int i = 0; i < 16; ++i) {
3122 auto color = overworld_palette_[palette_slot + i];
3123 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3124 static_cast<int>(color.rgb().x),
3125 static_cast<int>(color.rgb().y),
3126 static_cast<int>(color.rgb().z));
3127 }
3128 }
3129
3130 util::logf("=== END ANALYSIS ===");
3131}
3132
3135 if (Begin("Advanced Palette Settings", &show_palette_settings_)) {
3136 Text(tr("Pixel Normalization & Color Correction:"));
3137
3138 int mask_value = static_cast<int>(palette_normalization_mask_);
3139 if (SliderInt(tr("Normalization Mask"), &mask_value, 1, 255, "0x%02X")) {
3140 palette_normalization_mask_ = static_cast<uint8_t>(mask_value);
3141 }
3142
3143 Checkbox(tr("Auto Normalize Pixels"), &auto_normalize_pixels_);
3144
3145 if (Button(tr("Apply to All Graphics"))) {
3146 auto reload_result = LoadTile8();
3147 if (!reload_result.ok()) {
3148 Text(tr("Error: %s"), reload_result.message().data());
3149 }
3150 }
3151
3152 SameLine();
3153 if (Button(tr("Reset Defaults"))) {
3156 auto reload_result = LoadTile8();
3157 (void)reload_result; // Suppress warning
3158 }
3159
3160 Separator();
3161 Text(tr("Current State:"));
3162 static constexpr std::array<const char*, 7> palette_group_names = {
3163 "OW Main", "OW Aux", "OW Anim", "Dungeon",
3164 "Sprites", "Armor", "Sword"};
3165 Text(tr("Palette Group: %d (%s)"), current_palette_group_,
3167 ? palette_group_names[current_palette_group_]
3168 : "Unknown");
3169 Text(tr("Current Palette: %d"), current_palette_);
3170
3171 Separator();
3172 Text(tr("Sheet-Specific Fixes:"));
3173
3174 // Sheet-specific palette fixes
3175 static bool fix_sheet_0 = true;
3176 static bool fix_sprite_sheets = true;
3177 static bool use_transparent_for_terrain = false;
3178
3179 if (Checkbox(tr("Fix Sheet 0 (Trees)"), &fix_sheet_0)) {
3180 auto reload_result = LoadTile8();
3181 if (!reload_result.ok()) {
3182 Text(tr("Error reloading: %s"), reload_result.message().data());
3183 }
3184 }
3185 HOVER_HINT(
3186 "Use direct palette for sheet 0 instead of transparent palette");
3187
3188 if (Checkbox(tr("Fix Sprite Sheets"), &fix_sprite_sheets)) {
3189 auto reload_result = LoadTile8();
3190 if (!reload_result.ok()) {
3191 Text(tr("Error reloading: %s"), reload_result.message().data());
3192 }
3193 }
3194 HOVER_HINT("Use direct palette for sprite graphics sheets");
3195
3196 if (Checkbox(tr("Transparent for Terrain"),
3197 &use_transparent_for_terrain)) {
3198 auto reload_result = LoadTile8();
3199 if (!reload_result.ok()) {
3200 Text(tr("Error reloading: %s"), reload_result.message().data());
3201 }
3202 }
3203 HOVER_HINT("Force transparent palette for terrain graphics");
3204
3205 Separator();
3206 Text(tr("Color Analysis:"));
3207 if (current_tile8_ >= 0 &&
3208 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
3209 Text(tr("Selected Tile8 Analysis:"));
3210 const auto& tile_data = current_gfx_individual_[current_tile8_];
3211 std::map<uint8_t, int> pixel_counts;
3212 for (uint8_t pixel : tile_data) {
3213 pixel_counts[pixel & 0x0F]++; // Normalize to 4-bit
3214 }
3215
3216 Text(tr("Pixel Value Distribution:"));
3217 for (const auto& pair : pixel_counts) {
3218 int value = pair.first;
3219 int count = pair.second;
3220 Text(tr(" Value %d (0x%X): %d pixels"), value, value, count);
3221 }
3222
3223 Text(tr("Palette Colors Used:"));
3224 const gfx::SnesPalette* analysis_palette = ResolveDisplayPalette();
3225 if (analysis_palette == nullptr || analysis_palette->empty()) {
3226 analysis_palette =
3228 }
3229 for (const auto& pair : pixel_counts) {
3230 int value = pair.first;
3231 int count = pair.second;
3232 if (value < static_cast<int>(analysis_palette->size())) {
3233 auto color = (*analysis_palette)[value];
3234 ImVec4 display_color = color.rgb();
3235 ImGui::ColorButton(("##analysis" + std::to_string(value)).c_str(),
3236 display_color, ImGuiColorEditFlags_NoTooltip,
3237 ImVec2(16, 16));
3238 if (ImGui::IsItemHovered()) {
3239 ImGui::SetTooltip(tr("Index %d: 0x%04X (%d pixels)"), value,
3240 color.snes(), count);
3241 }
3242 if (value % 8 != 7)
3243 ImGui::SameLine();
3244 }
3245 }
3246 }
3247
3248 // Enhanced ROM Palette Management Section
3249 Separator();
3250 if (CollapsingHeader(tr("ROM Palette Manager")) && rom_) {
3251 Text(tr("Experimental ROM Palette Selection:"));
3252 HOVER_HINT(
3253 "Use ROM palettes to experiment with different color schemes");
3254
3255 if (Button(tr("Open Enhanced Palette Editor"))) {
3257 }
3258 SameLine();
3259 if (Button(tr("Show Color Analysis"))) {
3261 }
3262
3263 // Quick palette application
3264 static int quick_group = 0;
3265 static int quick_index = 0;
3266
3267 SliderInt(tr("ROM Group"), &quick_group, 0, 6);
3268 SliderInt(tr("Palette Index"), &quick_index, 0, 7);
3269
3270 if (Button(tr("Apply to Tile8 Source"))) {
3271 if (tile8_source_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3272 util::logf("Applied ROM palette group %d, index %d to Tile8 source",
3273 quick_group, quick_index);
3274 }
3275 }
3276 SameLine();
3277 if (Button(tr("Apply to Tile16 Editor"))) {
3278 if (tile16_edit_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3279 util::logf(
3280 "Applied ROM palette group %d, index %d to Tile16 editor",
3281 quick_group, quick_index);
3282 }
3283 }
3284 }
3285 }
3286 End();
3287 }
3288}
3289
3291 Text(tr("Layout Scratch:"));
3292 for (int i = 0; i < 4; ++i) {
3293 ImGui::PushID(i);
3294 std::string slot_name = "S" + std::to_string(i + 1);
3295
3296 if (Button((slot_name + " Save").c_str(), ImVec2(70, 20))) {
3298 }
3299 SameLine();
3300
3301 bool can_load = layout_scratch_[i].in_use;
3302 if (!can_load) {
3303 ImGui::BeginDisabled();
3304 }
3305 if (Button((slot_name + " Load").c_str(), ImVec2(70, 20)) && can_load) {
3307 }
3308 if (!can_load) {
3309 ImGui::EndDisabled();
3310 }
3311 SameLine();
3312 TextDisabled("%s", layout_scratch_[i].name.c_str());
3313 ImGui::PopID();
3314 }
3315}
3316
3318 if (slot < 0 || slot >= 4) {
3319 return absl::InvalidArgumentError("Invalid scratch slot");
3320 }
3321
3323 if (total_tiles <= 0) {
3324 return absl::FailedPreconditionError("Tile16 blockset is not available");
3325 }
3326
3327 const int start_tile = std::clamp(current_tile16_, 0, total_tiles - 1);
3328 for (int y = 0; y < 8; ++y) {
3329 for (int x = 0; x < 8; ++x) {
3330 const int tile_id = start_tile + (y * 8) + x;
3331 layout_scratch_[slot].tile_layout[y][x] =
3332 (tile_id < total_tiles) ? tile_id : -1;
3333 }
3334 }
3335
3336 layout_scratch_[slot].in_use = true;
3337 layout_scratch_[slot].name =
3338 absl::StrFormat("From %03X", static_cast<uint16_t>(start_tile));
3339
3340 return absl::OkStatus();
3341}
3342
3344 if (slot < 0 || slot >= 4) {
3345 return absl::InvalidArgumentError("Invalid scratch slot");
3346 }
3347
3348 if (!layout_scratch_[slot].in_use) {
3349 return absl::FailedPreconditionError("Scratch slot is empty");
3350 }
3351
3352 const int first_tile = layout_scratch_[slot].tile_layout[0][0];
3353 if (first_tile < 0) {
3354 return absl::FailedPreconditionError("Scratch slot has no valid tile data");
3355 }
3356
3357 RETURN_IF_ERROR(SetCurrentTile(first_tile));
3359 scroll_to_current_ = true;
3360
3361 selected_tiles_.clear();
3362 selected_tiles_.reserve(64);
3363 for (int y = 0; y < 8; ++y) {
3364 for (int x = 0; x < 8; ++x) {
3365 const int tile_id = layout_scratch_[slot].tile_layout[y][x];
3366 if (tile_id >= 0) {
3367 selected_tiles_.push_back(tile_id);
3368 }
3369 }
3370 }
3371 selection_start_tile_ = first_tile;
3372
3373 return absl::OkStatus();
3374}
3375
3377 if (ImGui::BeginPopupModal("ManualTile8Editor", nullptr,
3378 ImGuiWindowFlags_AlwaysAutoResize)) {
3379 ImGui::Text(tr("Manual Tile8 Configuration for Tile16 %02X"),
3381 ImGui::Separator();
3382
3383 auto* tile_data = GetCurrentTile16Data();
3384 if (tile_data) {
3385 ImGui::Text(tr("Current Tile16 Staged Data:"));
3386
3387 auto stage_current_tile = [&]() -> absl::Status {
3388 SyncTilesInfoArray(tile_data);
3393 }
3395 return absl::OkStatus();
3396 };
3397
3398 // Display and edit each quadrant using TileInfo structure
3399 const char* quadrant_names[] = {"Top-Left", "Top-Right", "Bottom-Left",
3400 "Bottom-Right"};
3401
3402 for (int q = 0; q < 4; q++) {
3403 ImGui::Text(tr("%s Quadrant:"), quadrant_names[q]);
3404 ImGui::TextDisabled(tr("Tile Palette metadata + Tile8/flip flags"));
3405
3406 // Get the current TileInfo for this quadrant
3407 gfx::TileInfo* tile_info = nullptr;
3408 switch (q) {
3409 case 0:
3410 tile_info = &tile_data->tile0_;
3411 break;
3412 case 1:
3413 tile_info = &tile_data->tile1_;
3414 break;
3415 case 2:
3416 tile_info = &tile_data->tile2_;
3417 break;
3418 case 3:
3419 tile_info = &tile_data->tile3_;
3420 break;
3421 }
3422
3423 if (tile_info) {
3424 // Editable inputs for TileInfo components
3425 ImGui::PushID(q);
3426
3427 int tile_id_int = static_cast<int>(tile_info->id_);
3428 if (ImGui::InputInt(tr("Tile8 ID"), &tile_id_int, 1, 10)) {
3429 tile_info->id_ =
3430 static_cast<uint16_t>(std::max(0, std::min(tile_id_int, 1023)));
3431 }
3432
3433 int palette_int = static_cast<int>(tile_info->palette_);
3434 if (ImGui::SliderInt(tr("Tile Palette"), &palette_int, 0, 7)) {
3435 tile_info->palette_ = static_cast<uint8_t>(palette_int);
3436 }
3437
3438 ImGui::Checkbox(tr("X Flip"), &tile_info->horizontal_mirror_);
3439 ImGui::SameLine();
3440 ImGui::Checkbox(tr("Y Flip"), &tile_info->vertical_mirror_);
3441 ImGui::SameLine();
3442 ImGui::Checkbox(tr("Priority"), &tile_info->over_);
3443
3444 if (ImGui::Button(tr("Stage Quadrant Edit"))) {
3445 auto stage_result = stage_current_tile();
3446 if (!stage_result.ok()) {
3447 ImGui::Text(tr("Stage Error: %s"), stage_result.message().data());
3448 }
3449 }
3450
3451 ImGui::PopID();
3452 }
3453
3454 if (q < 3)
3455 ImGui::Separator();
3456 }
3457
3458 ImGui::Separator();
3459 if (ImGui::Button(tr("Stage All Edits"))) {
3460 auto stage_result = stage_current_tile();
3461 if (!stage_result.ok()) {
3462 ImGui::Text(tr("Stage Error: %s"), stage_result.message().data());
3463 }
3464 }
3465 ImGui::SameLine();
3466 if (ImGui::Button(tr("Write Pending to ROM"))) {
3467 auto write_result = CommitAllChanges();
3468 if (!write_result.ok()) {
3469 ImGui::Text(tr("Write Error: %s"), write_result.message().data());
3470 }
3471 }
3472 ImGui::SameLine();
3473 if (ImGui::Button(tr("Refresh Display"))) {
3474 auto refresh_result = SetCurrentTile(current_tile16_);
3475 if (!refresh_result.ok()) {
3476 ImGui::Text(tr("Refresh Error: %s"), refresh_result.message().data());
3477 }
3478 }
3479
3480 } else {
3481 ImGui::Text(tr("Tile16 data not accessible"));
3482 ImGui::Text(tr("Current tile16: %d"), current_tile16_);
3483 if (rom_) {
3484 ImGui::Text(tr("Valid range: 0-4095 (4096 total tiles)"));
3485 }
3486 }
3487
3488 ImGui::Separator();
3489 if (ImGui::Button(tr("Close"))) {
3490 ImGui::CloseCurrentPopup();
3491 }
3492
3493 ImGui::EndPopup();
3494 }
3495}
3496
3498 // Skip if live preview is disabled
3499 if (!live_preview_enabled_) {
3500 return absl::OkStatus();
3501 }
3502
3503 // Check if preview needs updating
3504 if (!preview_dirty_) {
3505 return absl::OkStatus();
3506 }
3507
3508 // Ensure we have valid tile data
3510 preview_dirty_ = false;
3511 return absl::OkStatus();
3512 }
3513
3514 // Update the preview bitmap from current tile16
3515 if (!preview_tile16_.is_active()) {
3517 } else {
3518 // Recreate with updated data
3520 }
3521
3522 // Apply the current palette
3523 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
3524 if (display_palette && !display_palette->empty()) {
3525 const bool use_sub_palette_view =
3527 if (use_sub_palette_view) {
3528 const int sheet_index = GetSheetIndexForTile8(current_tile8_);
3529 const int palette_slot =
3530 GetActualPaletteSlot(static_cast<int>(current_palette_), sheet_index);
3531 if (palette_slot >= 0 &&
3532 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
3534 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
3535 } else {
3536 preview_tile16_.SetPaletteWithTransparent(*display_palette, 1, 15);
3537 }
3538 } else {
3539 preview_tile16_.SetPalette(*display_palette);
3540 }
3541 }
3542
3543 // Queue texture update
3546
3547 // Clear the dirty flag
3548 preview_dirty_ = false;
3549
3550 return absl::OkStatus();
3551}
3552
3554 if (!ImGui::IsAnyItemActive()) {
3555 const ImGuiIO& io = ImGui::GetIO();
3556#if defined(__APPLE__)
3557 const bool platform_primary_held = io.KeyCtrl || io.KeySuper;
3558#else
3559 const bool platform_primary_held = io.KeyCtrl;
3560#endif
3561 const bool ctrl_held = platform_primary_held ||
3562 ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
3563 ImGui::IsKeyDown(ImGuiKey_RightCtrl);
3564
3565 // Editing shortcuts (only fire without Ctrl to avoid conflicts)
3566 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
3567 status_ = ClearTile16();
3568 }
3569 if (ImGui::IsKeyPressed(ImGuiKey_H) && !ctrl_held) {
3571 }
3572 if (ImGui::IsKeyPressed(ImGuiKey_V) && !ctrl_held) {
3574 }
3575 if (ImGui::IsKeyPressed(ImGuiKey_R) && !ctrl_held) {
3577 }
3578 if (ImGui::IsKeyPressed(ImGuiKey_F) && !ctrl_held) {
3579 if (current_tile8_ >= 0 &&
3580 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
3582 }
3583 }
3584
3585 if (!ctrl_held) {
3586 if (ImGui::IsKeyPressed(ImGuiKey_P)) {
3588 }
3589 if (ImGui::IsKeyPressed(ImGuiKey_I)) {
3591 }
3592 if (ImGui::IsKeyPressed(ImGuiKey_U)) {
3594 }
3595 }
3596
3597 // Palette shortcuts
3598 if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
3599 status_ = CyclePalette(false);
3600 }
3601 if (ImGui::IsKeyPressed(ImGuiKey_E)) {
3602 status_ = CyclePalette(true);
3603 }
3604
3605 // Numeric shortcuts:
3606 // - 1..4 focus tile16 quadrants
3607 // - Ctrl+1..8 switch brush palette rows
3608 for (int i = 0; i < 8; ++i) {
3609 if (!ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_1 + i))) {
3610 continue;
3611 }
3612 const Tile16NumericShortcutResult shortcut =
3613 ResolveTile16NumericShortcut(ctrl_held, i);
3614 if (shortcut.quadrant_focus.has_value()) {
3615 active_quadrant_ = *shortcut.quadrant_focus;
3616 }
3617 if (shortcut.palette_id.has_value()) {
3618 current_palette_ = *shortcut.palette_id;
3620 }
3621 }
3622
3623 // Ctrl-modified shortcuts
3624 if (ctrl_held) {
3625 if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
3626 status_ = Undo();
3627 }
3628 if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
3629 status_ = Redo();
3630 }
3631 if (ImGui::IsKeyPressed(ImGuiKey_C)) {
3633 }
3634 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
3636 }
3637 if (ImGui::IsKeyPressed(ImGuiKey_S)) {
3638 if (ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
3639 ImGui::IsKeyDown(ImGuiKey_RightShift)) {
3641 } else {
3643 }
3644 }
3645 }
3646 }
3647}
3648
3662
3663} // namespace editor
3664} // namespace yaze
absl::StatusOr< gfx::Tile16 > ReadTile16(uint32_t tile16_id, uint32_t tile16_ptr)
Definition rom.cc:554
absl::Status WriteTile16(int tile16_id, uint32_t tile16_ptr, const gfx::Tile16 &tile)
Definition rom.cc:573
void set_dirty(bool dirty)
Definition rom.h:146
absl::Status SaveTile16ToScratchSpace(int slot)
const gfx::SnesPalette * ResolveDisplayPalette() const
std::function< absl::Status(const std::vector< Tile16Commit > &) on_changes_committed_)
std::map< int, gfx::Tile16 > pending_tile16_changes_
absl::Status LoadLayoutFromScratch(int slot)
static ImVec2 Tile16PreviewDisplayPixelToTilePosition(const ImVec2 &display_position)
zelda3::GameData * game_data() const
void FinalizePendingUndo()
Finalize any pending undo snapshot by capturing current state as "after" and pushing a Tile16EditActi...
std::chrono::steady_clock::time_point last_edit_time_
void DrawContextMenu()
Draw context menu with editor actions.
absl::Status CyclePalette(bool forward=true)
absl::Status HandleTile16CanvasClick(const ImVec2 &tile_position, bool left_click, bool right_click)
std::chrono::steady_clock::time_point last_rom_write_time_
absl::Status FillTile16WithTile8(int tile8_id)
gfx::Tilemap * tile16_blockset_
bool BitmapHasEncodedPaletteRows(const gfx::Bitmap &bitmap) const
absl::Status CommitAllChanges()
Write all pending changes to ROM and notify parent.
std::map< int, gfx::Bitmap > pending_tile16_bitmaps_
std::array< Tile16ScratchData, 4 > scratch_space_
absl::Status SaveTile16ToROM()
Write current tile16 data directly to ROM (bypasses pending system)
void RestoreFromSnapshot(const Tile16Snapshot &snapshot)
Restore editor state from a Tile16Snapshot (used by undo actions).
absl::Status ApplyPaletteToAll(uint8_t palette_id)
void DiscardAllChanges()
Discard all pending changes (revert to ROM state)
int GetActualPaletteSlotForCurrentTile16() const
Get the palette slot for the current tile being edited.
std::array< uint8_t, 0x200 > all_tiles_types_
absl::Status RegenerateTile16BitmapFromROM()
std::function< void(int)> on_current_tile_changed_
absl::Status DiscardChanges()
Discard current tile's changes (single tile)
absl::Status HandleTile8SourceSelection(bool right_clicked, float display_scale)
absl::Status PasteTile16FromClipboard()
gui::TileSelectorWidget blockset_selector_
int pending_changes_count() const
Get count of tiles with pending changes.
absl::Status SaveLayoutToScratch(int slot)
absl::Status LoadTile16FromScratchSpace(int slot)
gfx::SnesPalette CreateRemappedPaletteForViewing(const gfx::SnesPalette &source, int target_row) const
Create a remapped palette for viewing with user-selected palette.
int GetActualPaletteSlot(int palette_button, int sheet_index) const
Calculate actual palette slot from button.
absl::Status FlipTile16Horizontal()
gfx::Bitmap * tile16_blockset_bmp_
std::vector< zelda3::Tile8PixelData > current_gfx_individual_
gfx::SnesPalette overworld_palette_
static constexpr int kTilesPerPage
absl::Status DrawBrushAndTilePaletteControls(bool show_debug_info)
int GetEncodedPaletteRow(uint8_t pixel_value) const
Get the encoded palette row for a pixel value.
static constexpr int kTilesPerRow
void EnableLivePreview(bool enable)
absl::Status DrawTile8SourcePanel(float preferred_height=0.0f)
absl::Status SetCurrentTile(int id)
absl::Status UpdateTile8Palette(int tile8_id)
Update palette for a specific tile8.
zelda3::Tile8UsageIndex tile8_usage_cache_
absl::Status UpdateAsPanel()
Update the editor content without MenuBar (for WindowContent usage)
absl::Status RefreshTile16Blockset()
Tile16ClipboardData clipboard_tile16_
absl::Status RefreshAllPalettes()
Refresh all tile8 palettes after a palette change.
bool HasTile16BlocksetBitmap() const
void DrawPaletteSettings()
Draw palette settings UI.
absl::Status ApplyPaletteToQuadrant(int quadrant, uint8_t palette_id)
absl::Status UpdateOverworldTilemap()
Update the overworld tilemap to reflect tile changes.
absl::Status CommitChangesToBlockset()
Commit pending changes to the blockset atlas.
std::array< LayoutScratch, 4 > layout_scratch_
absl::Status UpdateROMTile16Data()
gfx::SnesPalette CreateRemappedPaletteForTile8(const gfx::SnesPalette &source, int target_row, int tile8_id) const
std::optional< Tile16Snapshot > pending_undo_before_
bool IsTile16Valid(int tile_id) const
gfx::Tile16 * GetCurrentTile16Data()
void MarkCurrentTileModified()
Mark the current tile as having pending changes.
absl::Status RebuildTile8UsageCache()
bool has_pending_changes() const
Check if any tiles have uncommitted changes.
absl::Status CommitChangesToOverworld()
Single-tile commit: ROM + blockset + parent refresh callback. Prefer CommitAllChanges() from the main...
absl::Status UpdateBlocksetBitmap()
void DiscardCurrentTileChanges()
Discard only the current tile's pending changes.
void RequestTileSwitch(int target_tile_id)
absl::Status DrawToCurrentTile16(ImVec2 pos, const gfx::Bitmap *source_tile=nullptr)
absl::Status Initialize(gfx::Bitmap &tile16_blockset_bmp, gfx::Bitmap &current_gfx_bmp, std::array< uint8_t, 0x200 > &all_tiles_types)
absl::Status DrawCompactActionStatusRow(bool has_pending, bool current_tile_pending, int pending_count, bool *show_debug_info, bool *show_advanced_controls)
void CopyTileBitmapToBlockset(int tile_id, const gfx::Bitmap &tile_bitmap)
absl::Status PreviewPaletteChange(uint8_t palette_id)
absl::Status CopyTile16ToClipboard(int tile_id)
absl::Status DrawPrimaryActionControls()
absl::Status ClearScratchSpace(int slot)
bool is_tile_modified(int tile_id) const
Check if a specific tile has pending changes.
absl::Status BuildTile16BitmapFromData(const gfx::Tile16 &tile_data, gfx::Bitmap *output_bitmap) const
int GetSheetIndexForTile8(int tile8_id) const
Determine which graphics sheet contains a tile8.
absl::Status PickTile8FromTile16(const ImVec2 &position)
std::vector< int > selected_tiles_
void CopyTile16ToAtlas(int tile_id)
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo()
Redo the top action. Returns error if stack is empty.
absl::Status Undo()
Undo the top action. Returns error if stack is empty.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const uint8_t * data() const
Definition bitmap.h:398
const SnesPalette & palette() const
Definition bitmap.h:389
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
TextureHandle texture() const
Definition bitmap.h:401
const std::vector< uint8_t > & vector() const
Definition bitmap.h:402
auto size() const
Definition bitmap.h:397
void UpdateSurfacePixels()
Update SDL surface with current pixel data from data_ vector Call this after modifying pixel data via...
Definition bitmap.cc:369
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
int depth() const
Definition bitmap.h:396
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
Definition bitmap.cc:456
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:399
bool modified() const
Definition bitmap.h:404
RAII timer for automatic timing management.
SNES Color container.
Definition snes_color.h:110
constexpr void set_transparent(bool t)
Definition snes_color.h:197
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void AddColor(const SnesColor &color)
static constexpr size_t kMaxColors
Tile composition of four 8x8 tiles.
Definition snes_tile.h:142
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1162
void ShowScalingControls()
Definition canvas.cc:1698
void ShowAdvancedCanvasProperties()
Definition canvas.cc:1573
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1098
bool DrawTilePainter(const Bitmap &bitmap, int size, float scale=1.0f)
Definition canvas.cc:939
auto custom_labels_enabled()
Definition canvas.h:398
void SetCanvasSize(ImVec2 canvas_size)
Definition canvas.h:372
auto mutable_labels(int i)
Definition canvas.h:441
void InitializePaletteEditor(Rom *rom)
Definition canvas.cc:338
void set_draggable(bool draggable)
Definition canvas.h:360
float GetGlobalScale() const
Definition canvas.h:376
auto zero_point() const
Definition canvas.h:348
void ShowPaletteEditor()
Definition canvas.cc:354
void InitializeDefaults()
Definition canvas.cc:178
void SetAutoResize(bool auto_resize)
Definition canvas.h:275
bool ApplyROMPalette(int group_index, int palette_index)
Definition canvas.cc:376
void ShowColorAnalysis()
Definition canvas.cc:366
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
RenderResult Render(gfx::Bitmap &atlas, bool atlas_ready)
void ScrollToTile(int tile_id, bool use_imgui_scroll=true)
#define ICON_MD_MENU
Definition icons.h:1196
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
#define HOVER_HINT(string)
Definition macro.h:24
Definition input.cc:23
const char * EditModeLabel(Tile16EditMode mode)
gfx::TileInfo & TileInfoForQuadrant(gfx::Tile16 *tile, int quadrant)
constexpr int kTile16PixelCount
float ComputeTile8SourcePanelHeight(float available_height_px, int source_bitmap_height_px, float display_scale, float preferred_height_px=0.0f, float min_height_px=220.0f, float max_height_px=520.0f)
constexpr int kTile8PixelCount
int ComputeTile16CompactActionColumnCount(float available_width_px)
constexpr float kTile8DisplayScale
float ComputeTile16PaletteButtonSize(float available_width_px)
Tile16ActionControlState ComputeTile16ActionControlState(bool has_pending, bool current_tile_pending, bool can_undo, bool can_redo)
bool ComputeTile8UsageHighlight(bool source_hovered, bool right_mouse_down)
int ComputeTile16ActionRowCount(int action_count, int column_count)
constexpr int kTile8Size
constexpr int kTile16Size
constexpr int kNumScratchSlots
int ComputeTile8IndexFromCanvasMouse(float mouse_x, float mouse_y, int source_bitmap_width_px, int max_tile_count, float display_scale)
float ComputeTile8SourceDisplayScale(float available_width_px, int source_bitmap_width_px, float min_scale=1.0f, float max_scale=4.0f)
Tile16NumericShortcutResult ResolveTile16NumericShortcut(bool ctrl_held, int number_index)
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
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
bool SuccessButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a success action button (green color).
void AddTableColumn(Table &table, const std::string &label, GuiElement element)
Definition input.cc:637
bool DangerButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a danger action button (error color).
void EndPadding()
Definition style.cc:281
ButtonColorSet GetSuccessButtonColors()
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:293
Color GetThemeColor(const std::string &color_name)
void ThemedText(const char *text, SemanticColor color)
ImVec4 GetAccentColor()
Definition ui_helpers.cc:69
ButtonColorSet GetPrimaryButtonColors()
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115
gfx::Tile16 HorizontalFlipTile16(gfx::Tile16 tile)
gfx::TileInfo & MutableTile16QuadrantInfo(gfx::Tile16 &tile, int quadrant)
void SetTile16AllQuadrantPalettes(gfx::Tile16 *tile, uint8_t palette_id)
absl::Status RenderTile16BitmapFromMetadata(const gfx::Tile16 &tile_data, const std::vector< Tile8PixelData > &tile8_pixels, gfx::Bitmap *output_bitmap)
void SyncTile16TilesInfo(gfx::Tile16 *tile)
constexpr int kNumTile16Individual
Definition overworld.h:241
gfx::Tile16 VerticalFlipTile16(gfx::Tile16 tile)
gfx::Tile16 RotateTile16Clockwise(gfx::Tile16 tile)
constexpr uint32_t kTile16Ptr
Definition game_data.h:59
int ComputeTile16Count(const gfx::Tilemap *tile16_blockset)
absl::StatusOr< std::vector< Tile16StampMutation > > BuildTile16StampMutations(const Tile16StampRequest &request)
const gfx::TileInfo & Tile16QuadrantInfo(const gfx::Tile16 &tile, int quadrant)
absl::Status BuildTile8UsageIndex(int total_tiles, const std::function< absl::StatusOr< gfx::Tile16 >(int)> &tile_provider, Tile8UsageIndex *usage_index)
void BlitTile16BitmapToAtlas(gfx::Bitmap *destination, int tile_id, const gfx::Bitmap &source_bitmap)
bool SetTile16QuadrantPalette(gfx::Tile16 *tile, int quadrant, uint8_t palette_id)
constexpr int kMaxTile8UsageId
std::array< uint8_t, 64 > Tile8PixelData
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Snapshot of a Tile16's editable state for undo/redo.
std::vector< uint8_t > bitmap_data
const SnesPalette & palette_ref(int i) const
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119
std::optional< float > grid_step
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92