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
3#include <array>
4#include <memory>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
12#include "app/gfx/core/bitmap.h"
17#include "app/gui/core/input.h"
18#include "app/gui/core/style.h"
21#include "imgui/imgui.h"
22#include "rom/rom.h"
23#include "util/hex.h"
24#include "util/log.h"
25#include "util/macro.h"
26#include "zelda3/game_data.h"
32
33namespace yaze {
34namespace editor {
35
36using namespace ImGui;
37
38// Display scales used for the tile8 source/preview rendering.
39constexpr float kTile8DisplayScale = 4.0f;
40
41namespace {
42
44
46 std::vector<gfx::SnesColor> grayscale;
47 grayscale.reserve(gfx::SnesPalette::kMaxColors);
48 for (int i = 0; i < static_cast<int>(gfx::SnesPalette::kMaxColors); ++i) {
49 const float value = static_cast<float>(i) / 255.0f;
50 grayscale.emplace_back(ImVec4(value, value, value, 1.0f));
51 }
52 if (!grayscale.empty()) {
53 grayscale[0].set_transparent(true);
54 }
55 return gfx::SnesPalette(grayscale);
56}
57
59 return zelda3::MutableTile16QuadrantInfo(*tile, quadrant);
60}
61
63 int quadrant) {
64 return zelda3::Tile16QuadrantInfo(tile, quadrant);
65}
66
70
71const char* EditModeLabel(Tile16EditMode mode) {
72 switch (mode) {
74 return "Paint";
76 return "Pick";
78 return "Usage Probe";
79 default:
80 return "Paint";
81 }
82}
83
84} // namespace
85
87 gfx::Bitmap& tile16_blockset_bmp, gfx::Bitmap& current_gfx_bmp,
88 std::array<uint8_t, 0x200>& all_tiles_types) {
89 all_tiles_types_ = all_tiles_types;
90 tile16_blockset_bmp_ = &tile16_blockset_bmp;
91 current_gfx_bmp_ = &current_gfx_bmp;
92
93 // Note: LoadTile8() will be called after palette is set by overworld editor
94 // This ensures proper palette coordination from the start
95
96 // Initialize current tile16 bitmap - this will be set by SetCurrentTile
98 std::vector<uint8_t>(kTile16PixelCount, 0));
99 current_tile16_bmp_.SetPalette(tile16_blockset_bmp.palette());
100 // Queue texture for later rendering.
103
104 // Initialize enhanced canvas features with proper sizing
107
108 // Attach blockset canvas to the selector widget
110 blockset_selector_.SetTileCount(kTile16Count);
111
112 // Configure canvases with proper initialization
115
116 // Initialize enhanced palette editors if ROM is available
117 if (rom_) {
120 }
121
122 // Initialize the current tile16 properly from the blockset
123 if (tile16_blockset_) {
124 RETURN_IF_ERROR(SetCurrentTile(0)); // Start with tile 0
125 }
126
128
129 // Setup collision type labels for tile8 canvas
130 ImVector<std::string> tile16_names;
131 for (int i = 0; i < 0x200; ++i) {
132 std::string str = util::HexByte(all_tiles_types_[i]);
133 tile16_names.push_back(str);
134 }
135 *tile8_source_canvas_.mutable_labels(0) = tile16_names;
137
138 // Setup tile info table
140 [&]() { Text("Tile16: %02X", current_tile16_); });
142 [&]() { Text("Tile8: %02X", current_tile8_); });
143 gui::AddTableColumn(tile_edit_table_, "##tile16Flip", [&]() {
144 Checkbox("X Flip", &x_flip);
145 Checkbox("Y Flip", &y_flip);
146 Checkbox("Priority", &priority_tile);
147 });
148
149 return absl::OkStatus();
150}
151
152absl::Status Tile16Editor::Update() {
154 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
155 }
156
157 if (BeginMenuBar()) {
158 if (BeginMenu("View")) {
159 Checkbox("Show Collision Types",
161 EndMenu();
162 }
163
164 if (BeginMenu("Edit")) {
165 if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
167 }
168 if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
170 }
171 EndMenu();
172 }
173
174 if (BeginMenu("File")) {
175 if (MenuItem("Write Pending to ROM", "Ctrl+S")) {
177 }
178 if (MenuItem("Refresh Blockset Preview", "Ctrl+Shift+S")) {
180 }
181 Separator();
182 bool live_preview = live_preview_enabled_;
183 if (MenuItem("Live Preview", nullptr, &live_preview)) {
184 EnableLivePreview(live_preview);
185 }
186 EndMenu();
187 }
188
189 if (BeginMenu("Scratch Space")) {
190 for (int i = 0; i < 4; i++) {
191 std::string slot_name = "Slot " + std::to_string(i + 1);
192 if (scratch_space_[i].has_data) {
193 if (MenuItem((slot_name + " (Load)").c_str())) {
195 }
196 if (MenuItem((slot_name + " (Save)").c_str())) {
198 }
199 if (MenuItem((slot_name + " (Clear)").c_str())) {
201 }
202 } else {
203 if (MenuItem((slot_name + " (Save)").c_str())) {
205 }
206 }
207 if (i < 3)
208 Separator();
209 }
210 EndMenu();
211 }
212
213 EndMenuBar();
214 }
215
216 // About popup
217 if (BeginPopupModal("About Tile16 Editor", NULL,
218 ImGuiWindowFlags_AlwaysAutoResize)) {
219 Text("Tile16 Editor for Link to the Past");
220 Text("This editor allows you to edit 16x16 tiles used in the game.");
221 Text("Features:");
222 BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
223 BulletText("Copy and paste Tile16 graphics");
224 BulletText("Save and load Tile16 graphics to/from scratch space");
225 BulletText("Preview Tile16 graphics at a larger size");
226 Separator();
227 if (Button("Close")) {
228 CloseCurrentPopup();
229 }
230 EndPopup();
231 }
232
233 // Unsaved changes confirmation dialog
235 OpenPopup("Unsaved Changes##Tile16Editor");
236 }
237 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
238 ImGuiWindowFlags_AlwaysAutoResize)) {
239 Text("Tile %d has staged changes.", current_tile16_);
240 Text("What would you like to do?");
241 Separator();
242
243 if (Button("Keep Staged & Continue", ImVec2(220, 0))) {
244 if (IsItemHovered()) {
245 SetTooltip(
246 "Switch to the requested tile now. Other tiles with staged edits "
247 "stay in the write queue until you use Write Pending or Discard.");
248 }
251 if (!status.ok()) {
252 util::logf("Failed to switch to tile %d: %s",
253 pending_tile_switch_target_, status.message().data());
254 }
255 }
258 CloseCurrentPopup();
259 }
260
261 if (gui::SuccessButton("Write Pending & Continue", ImVec2(220, 0))) {
262 auto status = CommitAllChanges();
263 if (status.ok() && pending_tile_switch_target_ >= 0) {
265 }
266 if (!status.ok()) {
267 util::logf("Failed to write/switch pending changes: %s",
268 status.message().data());
269 }
272 CloseCurrentPopup();
273 }
274
275 if (gui::DangerButton("Discard Current & Continue", ImVec2(220, 0))) {
280 if (!status.ok()) {
281 util::logf("Failed to switch to tile %d: %s",
282 pending_tile_switch_target_, status.message().data());
283 }
284 }
287 CloseCurrentPopup();
288 }
289
290 if (Button("Cancel", ImVec2(220, 0))) {
293 CloseCurrentPopup();
294 }
295
296 EndPopup();
297 }
298
299 // Handle keyboard shortcuts (shared implementation)
301
303
304 // Draw palette settings popup if enabled
306
307 // Update live preview if dirty
309
310 return absl::OkStatus();
311}
312
314 // REFACTORED: Single unified table layout in UpdateTile16Edit
316}
317
320 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
321 }
322
323 // Menu button for context menu
324 if (Button(ICON_MD_MENU " Menu")) {
325 OpenPopup("##Tile16EditorContextMenu");
326 }
327 SameLine();
328 TextDisabled("Right-click for more options");
329
330 // Context menu
332
333 // About popup
334 if (BeginPopupModal("About Tile16 Editor", NULL,
335 ImGuiWindowFlags_AlwaysAutoResize)) {
336 Text("Tile16 Editor for Link to the Past");
337 Text("This editor allows you to edit 16x16 tiles used in the game.");
338 Text("Features:");
339 BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
340 BulletText("Copy and paste Tile16 graphics");
341 BulletText("Save and load Tile16 graphics to/from scratch space");
342 BulletText("Preview Tile16 graphics at a larger size");
343 Separator();
344 if (Button("Close")) {
345 CloseCurrentPopup();
346 }
347 EndPopup();
348 }
349
350 // Unsaved changes confirmation dialog
352 OpenPopup("Unsaved Changes##Tile16Editor");
353 }
354 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
355 ImGuiWindowFlags_AlwaysAutoResize)) {
356 Text("Tile %d has staged changes.", current_tile16_);
357 Text("What would you like to do?");
358 Separator();
359
360 if (Button("Keep Staged & Continue", ImVec2(220, 0))) {
361 if (IsItemHovered()) {
362 SetTooltip(
363 "Switch to the requested tile now. Other tiles with staged edits "
364 "stay in the write queue until you use Write Pending or Discard.");
365 }
368 if (!status.ok()) {
369 util::logf("Failed to switch to tile %d: %s",
370 pending_tile_switch_target_, status.message().data());
371 }
372 }
375 CloseCurrentPopup();
376 }
377
378 if (gui::SuccessButton("Write Pending & Continue", ImVec2(220, 0))) {
379 auto status = CommitAllChanges();
380 if (status.ok() && pending_tile_switch_target_ >= 0) {
382 }
383 if (!status.ok()) {
384 util::logf("Failed to write/switch pending changes: %s",
385 status.message().data());
386 }
389 CloseCurrentPopup();
390 }
391
392 if (gui::DangerButton("Discard Current & Continue", ImVec2(220, 0))) {
397 if (!status.ok()) {
398 util::logf("Failed to switch to tile %d: %s",
399 pending_tile_switch_target_, status.message().data());
400 }
401 }
404 CloseCurrentPopup();
405 }
406
407 if (Button("Cancel", ImVec2(220, 0))) {
410 CloseCurrentPopup();
411 }
412
413 EndPopup();
414 }
415
416 // Handle keyboard shortcuts (shared implementation)
418
422
423 return absl::OkStatus();
424}
425
427 if (BeginPopup("##Tile16EditorContextMenu")) {
428 if (BeginMenu("View")) {
429 Checkbox("Show Collision Types",
431 EndMenu();
432 }
433
434 if (BeginMenu("Edit")) {
435 if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
437 }
438 if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
440 }
441 Separator();
442 if (MenuItem("Flip Horizontal", "H")) {
444 }
445 if (MenuItem("Flip Vertical", "V")) {
447 }
448 if (MenuItem("Rotate", "R")) {
450 }
451 if (MenuItem("Clear", "Delete")) {
453 }
454 EndMenu();
455 }
456
457 if (BeginMenu("File")) {
458 if (MenuItem("Write Pending to ROM", "Ctrl+S")) {
460 }
461 if (MenuItem("Refresh Blockset Preview", "Ctrl+Shift+S")) {
463 }
464 Separator();
465 bool live_preview = live_preview_enabled_;
466 if (MenuItem("Live Preview", nullptr, &live_preview)) {
467 EnableLivePreview(live_preview);
468 }
469 EndMenu();
470 }
471
472 if (BeginMenu("Scratch Space")) {
473 for (int i = 0; i < 4; i++) {
474 std::string slot_name = "Slot " + std::to_string(i + 1);
475 if (scratch_space_[i].has_data) {
476 if (MenuItem((slot_name + " (Load)").c_str())) {
478 }
479 if (MenuItem((slot_name + " (Save)").c_str())) {
481 }
482 if (MenuItem((slot_name + " (Clear)").c_str())) {
484 }
485 } else {
486 if (MenuItem((slot_name + " (Save)").c_str())) {
488 }
489 }
490 if (i < 3)
491 Separator();
492 }
493 EndMenu();
494 }
495
496 EndPopup();
497 }
498}
499
502 gui::BeginChildWithScrollbar("##Tile16EditorBlocksetScrollRegion");
503
504 // Tile ID search/jump bar
507 }
508
509 // Configure canvas frame options for blockset view
510 gui::CanvasFrameOptions frame_opts;
511 frame_opts.draw_grid = true;
512 frame_opts.grid_step = 32.0f; // Tile16 grid
513 frame_opts.draw_context_menu = true;
514 frame_opts.draw_overlay = true;
515 frame_opts.render_popups = true;
516 frame_opts.use_child_window = false;
517
518 auto canvas_rt = gui::BeginCanvas(blockset_canvas_, frame_opts);
520
521 // Ensure selector is synced with current selection
524 }
525
526 if (tile16_blockset_bmp_ == nullptr) {
527 return absl::FailedPreconditionError(
528 "Tile16 blockset bitmap not initialized");
529 }
530
531 // Render the selector widget (handles bitmap, grid, highlights, interaction)
532 auto result = blockset_selector_.Render(*tile16_blockset_bmp_, true);
533
534 if (result.selection_changed) {
535 // Use RequestTileSwitch to handle pending changes confirmation
536 RequestTileSwitch(result.selected_tile);
537 util::logf("Selected Tile16 from blockset: %d", result.selected_tile);
538 }
539
540 gui::EndCanvas(blockset_canvas_, canvas_rt, frame_opts);
541 EndChild();
542
543 return absl::OkStatus();
544}
545
546// ROM data access methods
548 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
549 return nullptr;
550 }
551 return &current_tile16_data_;
552}
553
555 auto* tile_data = GetCurrentTile16Data();
556 if (!tile_data) {
557 return absl::FailedPreconditionError("Cannot access current tile16 data");
558 }
559
560 // Write the modified tile16 data back to ROM
563
564 util::logf("ROM Tile16 data written for tile %d", current_tile16_);
565 return absl::OkStatus();
566}
567
569 if (!tile16_blockset_) {
570 return absl::FailedPreconditionError("Tile16 blockset not available");
571 }
572
573 // CRITICAL FIX: Force regeneration without using problematic tile cache
574 // Directly mark atlas as modified to trigger regeneration from ROM data
575
576 // Mark atlas as modified to trigger regeneration
578
579 // Queue texture update via Arena's deferred system
582
583 util::logf("Tile16 blockset refreshed and regenerated");
584 return absl::OkStatus();
585}
586
588 const gfx::Tile16& tile_data, gfx::Bitmap* output_bitmap) const {
590 tile_data, current_gfx_individual_, output_bitmap);
591}
592
594 const gfx::Bitmap& tile_bitmap) {
595 if (!tile_bitmap.is_active()) {
596 return;
597 }
598
599 if (tile16_blockset_bmp_ != nullptr) {
601 }
604 }
605
608 tile_bitmap);
610 }
611}
612
614 gfx::ScopedTimer timer("tile16_blockset_update");
615
616 if (!tile16_blockset_) {
617 return absl::FailedPreconditionError("Tile16 blockset not initialized");
618 }
619
620 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
621 return absl::OutOfRangeError("Current tile16 ID out of range");
622 }
623
625 return absl::FailedPreconditionError("Current tile16 bitmap is not active");
626 }
627
629
633 }
637 }
638
639 return absl::OkStatus();
640}
641
643 // Rebuild preview from `current_tile16_data_` (SetCurrentTile prefers pending
644 // maps over ROM). Name is historical.
645 auto* tile_data = GetCurrentTile16Data();
646 if (!tile_data) {
647 return absl::FailedPreconditionError("Cannot access current tile16 data");
648 }
649
650 // Tests and some initialization paths reach regeneration before tile8 previews
651 // are built; lazily populate them so metadata->bitmap rendering can proceed.
652 if (current_gfx_individual_.empty()) {
653 if (!HasCurrentGfxBitmap()) {
654 return absl::FailedPreconditionError("Tile8 source bitmap not active");
655 }
657 }
658
659 // Shared render path used by regeneration, stamping, and multi-tile updates.
661
662 // Set the appropriate palette using the same system as overworld
664
665 // Queue texture creation via Arena's deferred system
668
669 util::logf("Regenerated Tile16 bitmap for tile %d from ROM data",
671 return absl::OkStatus();
672}
673
674absl::Status Tile16Editor::DrawToCurrentTile16(ImVec2 pos,
675 const gfx::Bitmap* source_tile) {
676 constexpr int kTile8Size = 8;
677 (void)source_tile;
678
679 // Save undo state before making changes
680 auto now = std::chrono::steady_clock::now();
681 auto time_since_last_edit =
682 std::chrono::duration_cast<std::chrono::milliseconds>(now -
684 .count();
685
686 if (time_since_last_edit > 100) { // 100ms threshold
688 last_edit_time_ = now;
689 }
690
691 // Validate inputs
692 if (current_tile8_ < 0 ||
693 current_tile8_ >= static_cast<int>(current_gfx_individual_.size())) {
694 return absl::OutOfRangeError(
695 absl::StrFormat("Invalid tile8 index: %d", current_tile8_));
696 }
697
699 return absl::FailedPreconditionError("Target tile16 bitmap not active");
700 }
701
702 const int tile8_count =
703 static_cast<int>(std::min<size_t>(current_gfx_individual_.size(), 1024));
704 const int max_tile8_id = std::max(0, tile8_count - 1);
705 const int tile8_row_stride =
706 std::max(1, current_gfx_bmp_->width() / kTile8Size);
707 const int quadrant_x = (pos.x >= kTile8Size) ? 1 : 0;
708 const int quadrant_y = (pos.y >= kTile8Size) ? 1 : 0;
709 const int quadrant_index = quadrant_x + (quadrant_y * 2);
710 active_quadrant_ = std::clamp(quadrant_index, 0, 3);
711
712 zelda3::Tile16StampRequest stamp_request;
713 stamp_request.current_tile16 = current_tile16_data_;
714 stamp_request.current_tile16_id = current_tile16_;
715 stamp_request.selected_tile8_id = current_tile8_;
716 stamp_request.stamp_size = tile8_stamp_size_;
717 stamp_request.quadrant_index = quadrant_index;
718 stamp_request.palette_id = current_palette_;
719 stamp_request.x_flip = x_flip;
720 stamp_request.y_flip = y_flip;
721 stamp_request.priority = priority_tile;
722 stamp_request.tile8_row_stride = tile8_row_stride;
723 stamp_request.tile16_row_stride = kTilesPerRow;
724 stamp_request.max_tile8_id = max_tile8_id;
725 stamp_request.max_tile16_id = kTile16Count - 1;
726
727 ASSIGN_OR_RETURN(auto staged_tiles,
728 zelda3::BuildTile16StampMutations(stamp_request));
729
730 for (const auto& mutation : staged_tiles) {
731 const int tile16_id = mutation.tile16_id;
732 const gfx::Tile16& tile_data = mutation.tile_data;
733 gfx::Bitmap staged_bitmap;
734 RETURN_IF_ERROR(BuildTile16BitmapFromData(tile_data, &staged_bitmap));
735 if (current_tile16_bmp_.palette().size() > 0) {
736 staged_bitmap.SetPalette(current_tile16_bmp_.palette());
737 }
738
739 if (tile16_id == current_tile16_) {
740 current_tile16_data_ = tile_data;
741 SyncTilesInfoArray(&current_tile16_data_);
743 staged_bitmap.vector());
749 } else {
750 pending_tile16_changes_[tile16_id] = tile_data;
751 pending_tile16_bitmaps_[tile16_id] = staged_bitmap;
752 preview_dirty_ = true;
753 }
754
755 CopyTileBitmapToBlockset(tile16_id, staged_bitmap);
756 }
757
761 }
765 }
766
768
771 }
772
774 "Local tile16 stamp staged (size=%dx, tiles=%zu). Use 'Write Pending' to "
775 "commit.",
776 tile8_stamp_size_, staged_tiles.size());
777
778 return absl::OkStatus();
779}
780
781absl::Status Tile16Editor::HandleTile16CanvasClick(const ImVec2& tile_position,
782 bool left_click,
783 bool right_click) {
784 if (!left_click && !right_click) {
785 return absl::OkStatus();
786 }
787
788 if (right_click) {
789 RETURN_IF_ERROR(PickTile8FromTile16(tile_position));
790 util::logf("Picked tile8 from tile16 at (%d, %d)",
791 static_cast<int>(tile_position.x),
792 static_cast<int>(tile_position.y));
793 return absl::OkStatus();
794 }
795
796 switch (edit_mode_) {
798 // Pass nullptr to let DrawToCurrentTile16 handle flipping and store
799 // correct TileInfo metadata. The preview bitmap is pre-flipped for
800 // display only.
801 RETURN_IF_ERROR(DrawToCurrentTile16(tile_position, nullptr));
802 break;
805 RETURN_IF_ERROR(PickTile8FromTile16(tile_position));
806 break;
807 }
808
809 return absl::OkStatus();
810}
811
813 static bool show_advanced_controls = false;
814 static bool show_debug_info = false;
815
816 // Modern header with improved styling
817 gui::StyleVarGuard header_var_guard(
818 {{ImGuiStyleVar_FramePadding, ImVec2(8, 4)},
819 {ImGuiStyleVar_ItemSpacing, ImVec2(8, 4)}});
820
821 const bool has_pending = has_pending_changes();
822 const bool current_tile_pending = is_tile_modified(current_tile16_);
823 const int pending_count = pending_changes_count();
824
825 DrawEditorHeader(show_debug_info);
826 DrawEditorHeaderToggles(&show_debug_info, &show_advanced_controls);
827 DrawStagedStateBar(has_pending, current_tile_pending, pending_count);
828
829 ImGui::Separator();
830
831 // REFACTORED: Improved 3-column layout with better space utilization
832 if (ImGui::BeginTable("##Tile16EditLayout", 3,
833 ImGuiTableFlags_Resizable |
834 ImGuiTableFlags_BordersInnerV |
835 ImGuiTableFlags_SizingStretchProp)) {
836 ImGui::TableSetupColumn("Tile16 Blockset",
837 ImGuiTableColumnFlags_WidthStretch, 0.35f);
838 ImGui::TableSetupColumn("Tile8 Source", ImGuiTableColumnFlags_WidthStretch,
839 0.35f);
840 ImGui::TableSetupColumn("Editor & Controls",
841 ImGuiTableColumnFlags_WidthStretch, 0.30f);
842
843 ImGui::TableHeadersRow();
844 ImGui::TableNextRow();
845
846 // ========== COLUMN 1: Tile16 Blockset ==========
847 ImGui::TableNextColumn();
848 ImGui::BeginGroup();
849
850 // Navigation header with tile info
851 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile16 Blockset");
852 ImGui::SameLine();
853
854 // Show current tile and total tiles
856 ImGui::TextDisabled("(%d / %d)", current_tile16_, total_tiles);
857
858 // Navigation controls row
859 {
860 gui::StyleVarGuard nav_spacing_guard(ImGuiStyleVar_ItemSpacing,
861 ImVec2(4, 4));
862
863 // Jump to Tile ID input - live navigation as user types
864 ImGui::SetNextItemWidth(80);
865 if (ImGui::InputInt("##JumpToTile", &jump_to_tile_id_, 0, 0)) {
866 // Clamp to valid range
867 jump_to_tile_id_ = std::clamp(jump_to_tile_id_, 0, total_tiles - 1);
870 scroll_to_current_ = true;
871 }
872 }
873 if (ImGui::IsItemHovered()) {
874 ImGui::SetTooltip("Tile ID (0-%d) - navigates as you type",
875 total_tiles - 1);
876 }
877
878 ImGui::SameLine();
879 ImGui::TextDisabled("|");
880 ImGui::SameLine();
881
882 // Page navigation
883 int total_pages = (total_tiles + kTilesPerPage - 1) / kTilesPerPage;
885
886 if (ImGui::Button("<<")) {
888 scroll_to_current_ = true;
889 }
890 if (ImGui::IsItemHovered())
891 ImGui::SetTooltip("First page");
892
893 ImGui::SameLine();
894 if (ImGui::Button("<")) {
895 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
896 RequestTileSwitch(new_tile);
897 scroll_to_current_ = true;
898 }
899 if (ImGui::IsItemHovered())
900 ImGui::SetTooltip("Previous page (PageUp)");
901
902 ImGui::SameLine();
903 ImGui::TextDisabled("Page %d/%d", current_page_ + 1, total_pages);
904
905 ImGui::SameLine();
906 if (ImGui::Button(">")) {
907 int new_tile =
908 std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
909 RequestTileSwitch(new_tile);
910 scroll_to_current_ = true;
911 }
912 if (ImGui::IsItemHovered())
913 ImGui::SetTooltip("Next page (PageDown)");
914
915 ImGui::SameLine();
916 if (ImGui::Button(">>")) {
917 RequestTileSwitch(total_tiles - 1);
918 scroll_to_current_ = true;
919 }
920 if (ImGui::IsItemHovered())
921 ImGui::SetTooltip("Last page");
922
923 // Display current tile info (sheet and palette)
924 ImGui::SameLine();
925 ImGui::TextDisabled("|");
926 ImGui::SameLine();
927 int sheet_idx = GetSheetIndexForTile8(current_tile8_);
928 ImGui::Text("Sheet: %d | Palette: %d", sheet_idx, current_palette_);
929
930 // Handle keyboard shortcuts for page navigation
931 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
932 if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
933 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
934 RequestTileSwitch(new_tile);
935 scroll_to_current_ = true;
936 }
937 if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
938 int new_tile =
939 std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
940 RequestTileSwitch(new_tile);
941 scroll_to_current_ = true;
942 }
943 if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
945 scroll_to_current_ = true;
946 }
947 if (ImGui::IsKeyPressed(ImGuiKey_End)) {
948 RequestTileSwitch(total_tiles - 1);
949 scroll_to_current_ = true;
950 }
951
952 // Arrow keys for single-tile navigation (when Ctrl not held)
953 if (!ImGui::GetIO().KeyCtrl) {
954 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
955 if (current_tile16_ > 0) {
957 scroll_to_current_ = true;
958 }
959 }
960 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
961 if (current_tile16_ < total_tiles - 1) {
963 scroll_to_current_ = true;
964 }
965 }
966 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
969 scroll_to_current_ = true;
970 }
971 }
972 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
973 if (current_tile16_ + kTilesPerRow < total_tiles) {
975 scroll_to_current_ = true;
976 }
977 }
978 }
979 }
980
981 } // nav_spacing_guard scope
982
983 // Blockset canvas with scrolling
984 if (BeginChild("##BlocksetScrollable",
985 ImVec2(0, ImGui::GetContentRegionAvail().y), true,
986 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
987 // Handle scroll-to-current request
988 if (scroll_to_current_) {
989 int tile_row = current_tile16_ / kTilesPerRow;
990 float tile_y = tile_row * 32.0f * blockset_canvas_.GetGlobalScale();
991 ImGui::SetScrollY(tile_y);
992 scroll_to_current_ = false;
993 }
994
995 // Configure canvas frame options for blockset
996 gui::CanvasFrameOptions blockset_frame_opts;
997 blockset_frame_opts.draw_grid = true;
998 blockset_frame_opts.grid_step = 32.0f;
999 blockset_frame_opts.draw_context_menu = true;
1000 blockset_frame_opts.draw_overlay = true;
1001 blockset_frame_opts.render_popups = true;
1002 blockset_frame_opts.use_child_window = false;
1003
1004 auto blockset_rt =
1005 gui::BeginCanvas(blockset_canvas_, blockset_frame_opts);
1006
1007 // Handle tile selection from blockset
1008 bool tile_selected = false;
1010
1011 if (ImGui::IsItemClicked(ImGuiMouseButton_Left) &&
1013 tile_selected = true;
1014 }
1015
1016 if (tile_selected) {
1017 const ImGuiIO& io = ImGui::GetIO();
1018 ImVec2 canvas_pos = blockset_canvas_.zero_point();
1019 ImVec2 mouse_pos =
1020 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1021
1022 int grid_x = static_cast<int>(mouse_pos.x /
1024 int grid_y = static_cast<int>(mouse_pos.y /
1026 int selected_tile = grid_x + grid_y * 8;
1027
1028 if (selected_tile != current_tile16_ && selected_tile >= 0) {
1029 // Use RequestTileSwitch to handle pending changes confirmation
1030 RequestTileSwitch(selected_tile);
1031 util::logf("Selected Tile16 from blockset: %d", selected_tile);
1032 }
1033 }
1034
1035 if (tile16_blockset_bmp_ != nullptr) {
1037 }
1039
1040 gui::EndCanvas(blockset_canvas_, blockset_rt, blockset_frame_opts);
1041 }
1042 EndChild();
1043 ImGui::EndGroup();
1044
1045 // ========== COLUMN 2: Tile8 Source ==========
1046 ImGui::TableNextColumn();
1047 ImGui::BeginGroup();
1049 ImGui::EndGroup();
1050
1051 // ========== COLUMN 3: Tile16 Editor + Controls ==========
1052 TableNextColumn();
1053 ImGui::BeginGroup();
1054
1055 // Fixed size container to prevent canvas expansion
1056 if (ImGui::BeginChild("##Tile16FixedCanvas", ImVec2(90, 90), true,
1057 ImGuiWindowFlags_NoScrollbar |
1058 ImGuiWindowFlags_NoScrollWithMouse)) {
1059 // Configure canvas frame options for tile16 editor
1060 gui::CanvasFrameOptions tile16_edit_frame_opts;
1061 tile16_edit_frame_opts.canvas_size = ImVec2(64, 64);
1062 tile16_edit_frame_opts.draw_grid = true;
1063 tile16_edit_frame_opts.grid_step = 8.0f; // 8x8 grid for tile8 placement
1064 tile16_edit_frame_opts.draw_context_menu = true;
1065 tile16_edit_frame_opts.draw_overlay = true;
1066 tile16_edit_frame_opts.render_popups = true;
1067 tile16_edit_frame_opts.use_child_window = false;
1068
1069 auto tile16_edit_rt =
1070 gui::BeginCanvas(tile16_edit_canvas_, tile16_edit_frame_opts);
1071
1072 // Draw current tile16 bitmap with dynamic zoom
1075 }
1076
1077 // Handle tile8 painting with improved hover preview
1078 if (current_tile8_ >= 0 &&
1079 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
1080 // Create a display tile that shows the current palette selection
1082 tile8_preview_bmp_.Create(8, 8, 8,
1083 std::vector<uint8_t>(kTile8PixelCount, 0));
1084 }
1085
1086 // Get the original pixel data (already has sheet offsets from
1087 // ProcessGraphicsBuffer)
1088 auto& preview_data = tile8_preview_bmp_.mutable_data();
1089 std::copy(current_gfx_individual_[current_tile8_].begin(),
1091 preview_data.begin());
1092
1093 // Apply the correct sheet-aware palette slice for the preview
1094 const gfx::SnesPalette* display_palette = nullptr;
1095 if (overworld_palette_.size() >= 256) {
1096 display_palette = &overworld_palette_;
1097 } else if (palette_.size() >= 256) {
1098 display_palette = &palette_;
1099 } else {
1100 display_palette =
1102 }
1103
1104 if (display_palette && !display_palette->empty()) {
1106 // Calculate palette slot for the selected tile8
1107 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1108 int palette_slot =
1110
1111 // SNES palette offset fix: pixel value N maps to sub-palette color N
1112 // Color 0 is handled by SetPaletteWithTransparent (transparent)
1113 // Colors 1-15 need to come from palette[slot+1] through palette[slot+15]
1114 if (palette_slot >= 0 && static_cast<size_t>(palette_slot + 16) <=
1115 display_palette->size()) {
1117 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
1118 } else {
1120 15);
1121 }
1122 } else {
1123 tile8_preview_bmp_.SetPalette(*display_palette);
1124 }
1125 }
1126
1127 // Apply flips if needed
1128 if (x_flip || y_flip) {
1129 auto& data = tile8_preview_bmp_.mutable_data();
1130
1131 if (x_flip) {
1132 for (int y = 0; y < 8; ++y) {
1133 for (int x = 0; x < 4; ++x) {
1134 std::swap(data[y * 8 + x], data[y * 8 + (7 - x)]);
1135 }
1136 }
1137 }
1138
1139 if (y_flip) {
1140 for (int y = 0; y < 4; ++y) {
1141 for (int x = 0; x < 8; ++x) {
1142 std::swap(data[y * 8 + x], data[(7 - y) * 8 + x]);
1143 }
1144 }
1145 }
1146 }
1147
1148 // Push pixel changes to the existing surface before queuing texture work
1150
1151 // Queue texture creation/update on the persistent preview bitmap to
1152 // avoid dangling stack pointers in the arena queue
1153 const auto preview_command =
1157 gfx::Arena::Get().QueueTextureCommand(preview_command,
1159
1160 // CRITICAL FIX: Handle tile painting with simple click instead of
1161 // click+drag Draw the preview first
1164
1165 const bool left_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
1166 const bool right_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
1167
1168 if (left_clicked || right_clicked) {
1169 const ImGuiIO& io = ImGui::GetIO();
1170 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
1171 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
1172 io.MousePos.y - canvas_pos.y);
1173
1174 // Convert canvas coordinates to tile16 coordinates
1175 // Account for bitmap offset (2,2) and scale (4x)
1176 constexpr float kBitmapOffset = 2.0f;
1177 constexpr float kBitmapScale = 4.0f;
1178 int tile_x =
1179 static_cast<int>((mouse_pos.x - kBitmapOffset) / kBitmapScale);
1180 int tile_y =
1181 static_cast<int>((mouse_pos.y - kBitmapOffset) / kBitmapScale);
1182
1183 // Clamp to valid range (0-15 for 16x16 tile)
1184 tile_x = std::max(0, std::min(15, tile_x));
1185 tile_y = std::max(0, std::min(15, tile_y));
1186
1187 util::logf(
1188 "Tile16 canvas click: (%.2f, %.2f) -> Tile16: (%d, %d), mode=%s",
1189 mouse_pos.x, mouse_pos.y, tile_x, tile_y,
1190 EditModeLabel(edit_mode_));
1191
1192 RETURN_IF_ERROR(HandleTile16CanvasClick(ImVec2(tile_x, tile_y),
1193 left_clicked, right_clicked));
1194 }
1195 }
1196
1197 gui::EndCanvas(tile16_edit_canvas_, tile16_edit_rt,
1198 tile16_edit_frame_opts);
1199 }
1200 ImGui::EndChild();
1201
1202 Separator();
1203
1204 // === Compact Controls Section ===
1205
1206 // Tile8 info and preview
1207 if (current_tile8_ >= 0 &&
1208 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
1209 Text("Tile8: %02X", current_tile8_);
1210 SameLine();
1211 auto* tile8_texture = tile8_preview_bmp_.texture();
1212 if (tile8_texture) {
1213 ImGui::Image((ImTextureID)(intptr_t)tile8_texture, ImVec2(24, 24));
1214 }
1215
1216 // Show encoded palette row indicator
1217 // This shows which palette row the tile is encoded to use in the ROM
1218 int sheet_idx = GetSheetIndexForTile8(current_tile8_);
1219 int encoded_row = -1;
1220
1221 // Determine encoded row based on sheet and ProcessGraphicsBuffer behavior
1222 // Sheets 0, 3, 4, 5 have 0x88 added (row 8-9)
1223 // Other sheets have raw values (row 0)
1224 switch (sheet_idx) {
1225 case 0:
1226 case 3:
1227 case 4:
1228 case 5:
1229 encoded_row = 8; // 0x88 offset = row 8
1230 break;
1231 default:
1232 encoded_row = 0; // Raw values = row 0
1233 break;
1234 }
1235
1236 // Visual indicator showing sheet and encoded row
1237 ImGui::SameLine();
1238 ImGui::TextDisabled("S%d", sheet_idx);
1239 if (ImGui::IsItemHovered()) {
1240 ImGui::BeginTooltip();
1241 ImGui::Text("Sheet: %d", sheet_idx);
1242 ImGui::Text("Encoded Palette Row: %d", encoded_row);
1243 ImGui::Separator();
1244 ImGui::TextWrapped(
1245 "Graphics sheets have different palette encodings:\n"
1246 "- Sheets 0,3,4,5: Row 8 (offset 0x88)\n"
1247 "- Sheets 1,2,6,7: Row 0 (raw)");
1248 ImGui::EndTooltip();
1249 }
1250 }
1251
1252 // Tile8 transform options in compact form
1253 Checkbox("X Flip", &x_flip);
1254 SameLine();
1255 Checkbox("Y Flip", &y_flip);
1256 SameLine();
1257 Checkbox("Priority", &priority_tile);
1258
1259 Text("Stamp:");
1260 SameLine();
1261 if (ImGui::RadioButton("1x", tile8_stamp_size_ == 1)) {
1263 }
1264 SameLine();
1265 if (ImGui::RadioButton("2x", tile8_stamp_size_ == 2)) {
1267 }
1268 SameLine();
1269 if (ImGui::RadioButton("4x", tile8_stamp_size_ == 4)) {
1271 }
1272 HOVER_HINT(
1273 "1x: paint one quadrant\n2x: fill current tile16 from a 2x2 tile8 "
1274 "block\n4x: stamp a 2x2 tile16 patch from a 4x4 tile8 block");
1275
1276 Text("Edit Mode:");
1277 if (ImGui::RadioButton("Paint (P)", edit_mode_ == Tile16EditMode::kPaint)) {
1279 }
1280 SameLine();
1281 if (ImGui::RadioButton("Pick (I)", edit_mode_ == Tile16EditMode::kPick)) {
1283 }
1284 SameLine();
1285 if (ImGui::RadioButton("Usage (U)",
1288 }
1289 HOVER_HINT(
1290 "Paint: left-click places Tile8 into Tile16.\n"
1291 "Pick: left-click samples Tile8 from Tile16.\n"
1292 "Usage: keeps usage overlay visible and samples on click.\n"
1293 "Right-click on Tile16 preview always samples.");
1294
1295 Separator();
1296
1298
1299 Separator();
1300
1302
1303 // Advanced controls (collapsible)
1304 if (show_advanced_controls) {
1305 Separator();
1306 Text("Advanced:");
1307
1308 if (Button("Palette Settings", ImVec2(-1, 0))) {
1310 }
1311
1312 if (Button("Analyze Data", ImVec2(-1, 0))) {
1314 }
1315 HOVER_HINT("Analyze tile8 source data format and palette state");
1316
1317 if (Button("Manual Edit", ImVec2(-1, 0))) {
1318 ImGui::OpenPopup("ManualTile8Editor");
1319 }
1320
1321 if (Button("Refresh Blockset", ImVec2(-1, 0))) {
1323 }
1324
1325 // Scratch space in compact form
1326 Text("Scratch:");
1328
1329 // Manual tile8 editor popup
1331 }
1332
1333 // Compact debug information panel
1334 if (show_debug_info) {
1335 Separator();
1336 Text("Debug:");
1337 ImGui::TextDisabled("T16:%02X T8:%d Pal:%d", current_tile16_,
1339
1340 if (current_tile8_ >= 0) {
1341 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1342 int actual_slot = GetActualPaletteSlot(current_palette_, sheet_index);
1343 ImGui::TextDisabled("Sheet:%d Slot:%d", sheet_index, actual_slot);
1344 }
1345
1346 // Compact palette mapping table
1347 if (ImGui::CollapsingHeader("Palette Map",
1348 ImGuiTreeNodeFlags_DefaultOpen)) {
1349 ImGui::BeginChild("##PaletteMappingScroll", ImVec2(0, 120), true);
1350 if (ImGui::BeginTable("##PalMap", 3,
1351 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1352 ImGuiTableFlags_SizingFixedFit)) {
1353 ImGui::TableSetupColumn("Btn", ImGuiTableColumnFlags_WidthFixed, 30);
1354 ImGui::TableSetupColumn("S0,3-4", ImGuiTableColumnFlags_WidthFixed,
1355 50);
1356 ImGui::TableSetupColumn("S1-2", ImGuiTableColumnFlags_WidthFixed, 50);
1357 ImGui::TableHeadersRow();
1358
1359 for (int i = 0; i < 8; ++i) {
1360 ImGui::TableNextRow();
1361 ImGui::TableNextColumn();
1362 ImGui::Text("%d", i);
1363 ImGui::TableNextColumn();
1364 ImGui::Text("%d", GetActualPaletteSlot(i, 0));
1365 ImGui::TableNextColumn();
1366 ImGui::Text("%d", GetActualPaletteSlot(i, 1));
1367 }
1368 ImGui::EndTable();
1369 }
1370 ImGui::EndChild();
1371 }
1372
1373 // Color preview - compact
1374 if (ImGui::CollapsingHeader("Colors")) {
1375 if (overworld_palette_.size() >= 256) {
1376 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1377 ImGui::Text("Slot %d:", actual_slot);
1378
1379 for (int i = 0;
1380 i < 8 &&
1381 (actual_slot + i) < static_cast<int>(overworld_palette_.size());
1382 ++i) {
1383 int color_index = actual_slot + i;
1384 auto color = overworld_palette_[color_index];
1385 ImVec4 display_color = color.rgb();
1386
1387 ImGui::ColorButton(absl::StrFormat("##c%d", i).c_str(),
1388 display_color, ImGuiColorEditFlags_NoTooltip,
1389 ImVec2(20, 20));
1390 if (ImGui::IsItemHovered()) {
1391 ImGui::SetTooltip("%d:0x%04X", color_index, color.snes());
1392 }
1393
1394 if ((i + 1) % 4 != 0)
1395 ImGui::SameLine();
1396 }
1397 }
1398 }
1399 }
1400
1401 ImGui::EndGroup();
1402 EndTable();
1403 }
1404
1406 DrawBottomActionRail(has_pending, current_tile_pending, pending_count));
1407
1408 // Draw palette settings and canvas popups
1410
1411 // Show canvas popup windows if opened from context menu
1418
1419 return absl::OkStatus();
1420}
1421
1422void Tile16Editor::DrawEditorHeader(bool show_debug_info) {
1423 active_quadrant_ = std::clamp(active_quadrant_, 0, 3);
1424 static constexpr std::array<const char*, 4> kQuadrantLabels = {"TL", "TR",
1425 "BL", "BR"};
1426 int quadrant_palette_tl = -1;
1427 int quadrant_palette_tr = -1;
1428 int quadrant_palette_bl = -1;
1429 int quadrant_palette_br = -1;
1430 if (const auto* tile_data = GetCurrentTile16Data()) {
1431 quadrant_palette_tl = tile_data->tile0_.palette_;
1432 quadrant_palette_tr = tile_data->tile1_.palette_;
1433 quadrant_palette_bl = tile_data->tile2_.palette_;
1434 quadrant_palette_br = tile_data->tile3_.palette_;
1435 }
1436
1437 ImGui::BeginGroup();
1438 ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tile16 Editor");
1439 ImGui::SameLine();
1440 ImGui::TextDisabled("ID: %02X", current_tile16_);
1441 ImGui::SameLine();
1442 ImGui::TextDisabled("| Brush Palette: %d", current_palette_);
1443 ImGui::SameLine();
1444 ImGui::TextDisabled("| Active Quadrant: %s",
1445 kQuadrantLabels[active_quadrant_]);
1446 ImGui::SameLine();
1447 ImGui::TextDisabled("| Edit Mode: %s", EditModeLabel(edit_mode_));
1448
1449 if (quadrant_palette_tl >= 0) {
1450 ImGui::SameLine();
1451 ImGui::TextDisabled("| Tile Meta TL/TR/BL/BR: %d/%d/%d/%d",
1452 quadrant_palette_tl, quadrant_palette_tr,
1453 quadrant_palette_bl, quadrant_palette_br);
1454 }
1455
1456 ImGui::SameLine();
1457 const bool usage_mode_active = highlight_tile8_usage_;
1458 ImGui::TextColored(
1459 usage_mode_active ? ImVec4(0.55f, 0.90f, 0.60f, 1.0f)
1460 : ImVec4(0.95f, 0.86f, 0.46f, 1.0f),
1461 usage_mode_active ? "Usage Mode: ACTIVE" : "Usage Mode: Hold RMB or U");
1462 if (ImGui::IsItemHovered()) {
1463 ImGui::BeginTooltip();
1464 ImGui::Text("Usage Mode");
1465 ImGui::Separator();
1466 ImGui::TextDisabled("Current edit mode: %s", EditModeLabel(edit_mode_));
1467 ImGui::TextDisabled(
1468 "Press U to lock usage overlay and probe interactions.");
1469 ImGui::TextDisabled(
1470 "Hold RMB on the Tile8 Source panel for temporary usage highlighting.");
1471 ImGui::TextDisabled(
1472 "Pick mode (I) and Usage mode (U) sample on left-click.");
1473 ImGui::TextDisabled(
1474 "Paint mode (P) places Tile8 data and metadata on left-click.");
1475 ImGui::TextDisabled("Navigation shortcuts: PgUp/PgDn/Home/End + arrows.");
1476 ImGui::TextDisabled("Modes: P/I/U | Palette: Ctrl+1..8 | Quadrants: 1..4");
1477 ImGui::EndTooltip();
1478 }
1479
1480 if (show_debug_info) {
1481 ImGui::SameLine();
1482 const int actual_slot = GetActualPaletteSlotForCurrentTile16();
1483 ImGui::TextDisabled("(Slot: %d)", actual_slot);
1484 }
1485 ImGui::EndGroup();
1486}
1487
1488void Tile16Editor::DrawEditorHeaderToggles(bool* show_debug_info,
1489 bool* show_advanced_controls) {
1490 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 180);
1491 if (ImGui::Button("Debug Info", ImVec2(80, 0))) {
1492 *show_debug_info = !*show_debug_info;
1493 }
1494 ImGui::SameLine();
1495 if (ImGui::Button("Advanced", ImVec2(80, 0))) {
1496 *show_advanced_controls = !*show_advanced_controls;
1497 }
1498}
1499
1501 bool current_tile_pending,
1502 int pending_count) {
1503 const ImVec4 staged_bar_bg =
1504 current_tile_pending ? ImVec4(0.27f, 0.18f, 0.09f, 0.65f)
1505 : (has_pending ? ImVec4(0.16f, 0.15f, 0.18f, 0.65f)
1506 : ImVec4(0.12f, 0.17f, 0.13f, 0.65f));
1507 gui::StyleColorGuard staged_bar_guard(ImGuiCol_ChildBg, staged_bar_bg);
1508 if (ImGui::BeginChild(
1509 "##Tile16StagedStateBar", ImVec2(0, 58), true,
1510 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
1511 if (current_tile_pending) {
1512 ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.30f, 1.0f), "Tile %02X: STAGED",
1514 } else {
1515 ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.55f, 1.0f), "Tile %02X: CLEAN",
1517 }
1518
1519 ImGui::SameLine();
1520 ImGui::TextDisabled("| Queue: %d tile%s pending", pending_count,
1521 pending_count == 1 ? "" : "s");
1522
1524 const auto seconds_since_write =
1525 std::chrono::duration_cast<std::chrono::seconds>(
1526 std::chrono::steady_clock::now() - last_rom_write_time_)
1527 .count();
1528 ImGui::SameLine();
1529 ImGui::TextDisabled("| Last write: %d tile%s, %lds ago",
1531 last_rom_write_count_ == 1 ? "" : "s",
1532 static_cast<long>(seconds_since_write));
1533 }
1534 ImGui::TextDisabled(
1535 "Action rail at bottom: Write Pending / Discard / Undo");
1536 }
1537 ImGui::EndChild();
1538}
1539
1541 bool show_debug_info) {
1542 // Palette selector - this is the paint brush palette for new placements.
1543 Text("Brush Palette:");
1544 if (show_debug_info) {
1545 SameLine();
1546 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1547 ImGui::TextDisabled("(Slot %d)", actual_slot);
1548 }
1549 ImGui::TextDisabled("Used for new tile8 placements");
1550
1551 // Compact palette grid
1552 ImGui::BeginGroup();
1553 float available_width = ImGui::GetContentRegionAvail().x;
1554 float button_size = std::min(32.0f, (available_width - 16.0f) / 4.0f);
1555
1556 for (int row = 0; row < 2; ++row) {
1557 for (int col = 0; col < 4; ++col) {
1558 if (col > 0)
1559 ImGui::SameLine();
1560
1561 int i = row * 4 + col;
1562 bool is_current = (current_palette_ == i);
1563
1564 // Modern button styling with better visual hierarchy
1565 ImGui::PushID(i);
1566
1567 gui::StyleColorGuard palette_btn_colors(
1568 {{ImGuiCol_Button, is_current ? ImVec4(0.2f, 0.7f, 0.3f, 1.0f)
1569 : ImVec4(0.3f, 0.3f, 0.35f, 1.0f)},
1570 {ImGuiCol_ButtonHovered, is_current
1571 ? ImVec4(0.3f, 0.8f, 0.4f, 1.0f)
1572 : ImVec4(0.4f, 0.4f, 0.45f, 1.0f)},
1573 {ImGuiCol_ButtonActive, is_current
1574 ? ImVec4(0.1f, 0.6f, 0.2f, 1.0f)
1575 : ImVec4(0.25f, 0.25f, 0.3f, 1.0f)},
1576 {ImGuiCol_Border, is_current ? ImVec4(0.4f, 0.9f, 0.5f, 1.0f)
1577 : ImVec4(0.5f, 0.5f, 0.5f, 0.3f)}});
1578 gui::StyleVarGuard palette_btn_border(ImGuiStyleVar_FrameBorderSize,
1579 1.0f);
1580
1581 if (ImGui::Button(absl::StrFormat("%d", i).c_str(),
1582 ImVec2(button_size, button_size))) {
1583 if (current_palette_ != i) {
1584 current_palette_ = i;
1585 auto status = RefreshAllPalettes();
1586 if (!status.ok()) {
1587 util::logf("Failed to refresh palettes: %s",
1588 status.message().data());
1589 } else {
1590 util::logf("Palette successfully changed to %d", current_palette_);
1591 }
1592 }
1593 }
1594
1595 ImGui::PopID();
1596
1597 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
1598 current_palette_ = static_cast<uint8_t>(i);
1600 }
1601
1602 // Tooltip with palette info
1603 if (ImGui::IsItemHovered()) {
1604 ImGui::BeginTooltip();
1605 if (show_debug_info) {
1606 ImGui::Text("Palette %d -> Slots:", i);
1607 ImGui::Text(" S0,3,4: %d", GetActualPaletteSlot(i, 0));
1608 ImGui::Text(" S1,2: %d", GetActualPaletteSlot(i, 1));
1609 ImGui::Text(" S5,6: %d", GetActualPaletteSlot(i, 5));
1610 ImGui::Text(" S7: %d", GetActualPaletteSlot(i, 7));
1611 } else {
1612 ImGui::Text("Brush Palette %d", i);
1613 ImGui::TextDisabled("Applied to new tile8 placements");
1614 ImGui::TextDisabled("RMB: apply to all tile quadrants");
1615 ImGui::TextDisabled("Quadrant metadata is shown in strip below");
1616 ImGui::TextDisabled(
1617 "Hotkeys: Ctrl+1..8 palette, 1..4 quadrant focus");
1618 if (is_current) {
1619 ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Active");
1620 }
1621 }
1622 ImGui::EndTooltip();
1623 }
1624 }
1625 }
1626 ImGui::EndGroup();
1627
1628 if (auto* tile_data = GetCurrentTile16Data(); tile_data != nullptr) {
1629 active_quadrant_ = std::clamp(active_quadrant_, 0, 3);
1630 Text("Quadrant Focus:");
1631 SameLine();
1632 ImGui::TextDisabled("1-4");
1633 static constexpr std::array<const char*, 4> kQuadrantLabels = {"TL", "TR",
1634 "BL", "BR"};
1635 const float quadrant_button_width = std::max(58.0f, button_size + 24.0f);
1636 for (int q = 0; q < 4; ++q) {
1637 if (q > 0) {
1638 SameLine();
1639 }
1640
1641 const gfx::TileInfo& info = TileInfoForQuadrant(*tile_data, q);
1642 const uint8_t quadrant_palette = info.palette_;
1643 const bool is_active_quadrant = (active_quadrant_ == q);
1644 const bool matches_brush = (quadrant_palette == current_palette_);
1645
1646 ImGui::PushID(100 + q);
1647 gui::StyleColorGuard quadrant_btn_colors(
1648 {{ImGuiCol_Button,
1649 is_active_quadrant
1650 ? ImVec4(0.16f, 0.48f, 0.72f, 1.0f)
1651 : (matches_brush ? ImVec4(0.23f, 0.35f, 0.50f, 1.0f)
1652 : ImVec4(0.28f, 0.28f, 0.32f, 1.0f))},
1653 {ImGuiCol_ButtonHovered,
1654 is_active_quadrant
1655 ? ImVec4(0.20f, 0.56f, 0.82f, 1.0f)
1656 : (matches_brush ? ImVec4(0.28f, 0.43f, 0.60f, 1.0f)
1657 : ImVec4(0.38f, 0.38f, 0.42f, 1.0f))},
1658 {ImGuiCol_ButtonActive,
1659 is_active_quadrant
1660 ? ImVec4(0.12f, 0.40f, 0.62f, 1.0f)
1661 : (matches_brush ? ImVec4(0.18f, 0.30f, 0.44f, 1.0f)
1662 : ImVec4(0.24f, 0.24f, 0.28f, 1.0f))},
1663 {ImGuiCol_Border, is_active_quadrant
1664 ? ImVec4(0.45f, 0.78f, 1.0f, 1.0f)
1665 : ImVec4(0.4f, 0.4f, 0.4f, 0.4f)}});
1666 gui::StyleVarGuard quadrant_btn_border(ImGuiStyleVar_FrameBorderSize,
1667 is_active_quadrant ? 2.0f : 1.0f);
1668
1669 if (ImGui::Button(absl::StrFormat("%d %s:%d", q + 1, kQuadrantLabels[q],
1670 quadrant_palette)
1671 .c_str(),
1672 ImVec2(quadrant_button_width, 0))) {
1673 active_quadrant_ = q;
1674 if (current_palette_ != quadrant_palette) {
1675 current_palette_ = quadrant_palette;
1676 auto status = RefreshAllPalettes();
1677 if (!status.ok()) {
1678 util::logf("Failed to refresh palettes: %s",
1679 status.message().data());
1680 }
1681 }
1682 }
1683
1684 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
1685 active_quadrant_ = q;
1687 }
1688
1689 if (ImGui::IsItemHovered()) {
1690 ImGui::BeginTooltip();
1691 ImGui::Text("Quadrant %s metadata", kQuadrantLabels[q]);
1692 ImGui::Separator();
1693 ImGui::Text("Tile8: %02X", info.id_);
1694 ImGui::Text("Palette: %d", quadrant_palette);
1695 ImGui::Text("Flip: H:%s V:%s", info.horizontal_mirror_ ? "Y" : "N",
1696 info.vertical_mirror_ ? "Y" : "N");
1697 ImGui::Text("Priority: %s", info.over_ ? "Y" : "N");
1698 ImGui::TextDisabled("LMB: set brush palette from this quadrant");
1699 ImGui::TextDisabled(
1700 "RMB: apply current brush palette to this quadrant");
1701 ImGui::TextDisabled("Keys 1..4: focus TL/TR/BL/BR");
1702 ImGui::EndTooltip();
1703 }
1704
1705 ImGui::PopID();
1706 }
1707
1708 const gfx::TileInfo& active_info =
1709 TileInfoForQuadrant(*tile_data, active_quadrant_);
1710 ImGui::TextDisabled("Active %s: Tile8 %02X | P%d | H:%s V:%s | Pri:%s",
1711 kQuadrantLabels[active_quadrant_], active_info.id_,
1712 active_info.palette_,
1713 active_info.horizontal_mirror_ ? "Y" : "N",
1714 active_info.vertical_mirror_ ? "Y" : "N",
1715 active_info.over_ ? "Y" : "N");
1716
1717 if (Button("Apply Brush to Active Quadrant", ImVec2(-1, 0))) {
1720 }
1721 HOVER_HINT(
1722 "Copy the Brush Palette into the selected quadrant metadata.\n"
1723 "Use keys 1..4 to change active quadrant quickly.");
1724 }
1725
1726 // Copy the current brush palette into all stored quadrant palette fields.
1727 if (Button("Apply Brush to All Quadrants", ImVec2(-1, 0))) {
1729 }
1730 HOVER_HINT(
1731 "Copy the Brush Palette into Tile Palette metadata for all 4 "
1732 "quadrants.\n"
1733 "Tip: right-click any brush palette button above for a one-step apply.");
1734 return absl::OkStatus();
1735}
1736
1738 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile8 Source");
1739
1741
1742 if (BeginChild("##Tile8SourceScrollable", ImVec2(0, 0), true,
1743 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1744 gui::CanvasFrameOptions tile8_frame_opts;
1745 tile8_frame_opts.draw_grid = true;
1746 tile8_frame_opts.grid_step = 32.0f; // Tile8 grid (8px * 4 scale)
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) {
1767 }
1768
1769 if (current_gfx_bmp_ != nullptr) {
1772 }
1773
1774 gui::EndCanvas(tile8_source_canvas_, tile8_rt, tile8_frame_opts);
1775 }
1776 EndChild();
1777
1778 return absl::OkStatus();
1779}
1780
1781absl::Status Tile16Editor::HandleTile8SourceSelection(bool right_clicked) {
1782 const ImGuiIO& io = ImGui::GetIO();
1783 ImVec2 canvas_pos = tile8_source_canvas_.zero_point();
1784 ImVec2 mouse_pos =
1785 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1786
1787 const int new_tile8 = ComputeTile8IndexFromCanvasMouse(
1788 mouse_pos.x, mouse_pos.y, current_gfx_bmp_->width(),
1789 static_cast<int>(current_gfx_individual_.size()), kTile8DisplayScale);
1790 if (new_tile8 < 0 || new_tile8 == current_tile8_) {
1791 return absl::OkStatus();
1792 }
1793
1794 current_tile8_ = new_tile8;
1796 if (right_clicked) {
1798 }
1799 util::logf("Selected Tile8: %d", current_tile8_);
1800
1801 return absl::OkStatus();
1802}
1803
1805 // Local tile-shaping actions stay in the right column.
1806 if (Button("Clear", ImVec2(-1, 0))) {
1808 }
1809
1810 if (Button("Copy", ImVec2(-1, 0))) {
1812 }
1813
1814 if (Button("Paste", ImVec2(-1, 0))) {
1816 }
1817
1818 return absl::OkStatus();
1819}
1820
1821absl::Status Tile16Editor::DrawBottomActionRail(bool has_pending,
1822 bool current_tile_pending,
1823 int pending_count) {
1825 has_pending, current_tile_pending, undo_manager_.CanUndo());
1826
1827 gui::StyleColorGuard rail_bg(
1828 {{ImGuiCol_ChildBg, ImVec4(0.10f, 0.13f, 0.16f, 0.85f)}});
1829 if (ImGui::BeginChild(
1830 "##Tile16BottomActionRail", ImVec2(0, 64), true,
1831 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
1832 ImGui::TextDisabled("Queue: %d tile%s staged", pending_count,
1833 pending_count == 1 ? "" : "s");
1834 ImGui::SameLine();
1835 ImGui::TextDisabled("| Mode: %s", EditModeLabel(edit_mode_));
1836
1837 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1838 const float avail = ImGui::GetContentRegionAvail().x;
1839 const float button_width =
1840 std::max(110.0f, (avail - spacing * 3.0f) / 4.0f);
1841
1842 if (!action_state.can_write_pending) {
1843 ImGui::BeginDisabled();
1844 }
1845 if (gui::SuccessButton("Write Pending", ImVec2(button_width, 0))) {
1847 }
1848 if (!action_state.can_write_pending) {
1849 ImGui::EndDisabled();
1850 }
1851 if (ImGui::IsItemHovered()) {
1852 ImGui::SetTooltip("Write all %d pending tile16 edits to ROM",
1853 pending_count);
1854 }
1855
1856 ImGui::SameLine();
1857 if (!action_state.can_discard_current) {
1858 ImGui::BeginDisabled();
1859 }
1860 if (ImGui::Button("Discard Current", ImVec2(button_width, 0))) {
1862 }
1863 if (!action_state.can_discard_current) {
1864 ImGui::EndDisabled();
1865 }
1866
1867 ImGui::SameLine();
1868 if (!action_state.can_write_pending) {
1869 ImGui::BeginDisabled();
1870 }
1871 if (gui::DangerButton("Discard All", ImVec2(button_width, 0))) {
1873 }
1874 if (!action_state.can_write_pending) {
1875 ImGui::EndDisabled();
1876 }
1877
1878 ImGui::SameLine();
1879 if (!action_state.can_undo) {
1880 ImGui::BeginDisabled();
1881 }
1882 if (ImGui::Button("Undo", ImVec2(button_width, 0))) {
1884 }
1885 if (!action_state.can_undo) {
1886 ImGui::EndDisabled();
1887 }
1888 }
1889 ImGui::EndChild();
1890 return absl::OkStatus();
1891}
1892
1894 if (!HasCurrentGfxBitmap() || current_gfx_bmp_->data() == nullptr) {
1895 return absl::FailedPreconditionError(
1896 "Current graphics bitmap not initialized");
1897 }
1898
1900
1901 // Calculate how many 8x8 tiles we can fit based on the current graphics
1902 // bitmap size SNES graphics are typically 128 pixels wide (16 tiles of 8
1903 // pixels each)
1904 const int tiles_per_row = current_gfx_bmp_->width() / 8;
1905 const int total_rows = current_gfx_bmp_->height() / 8;
1906 const int total_tiles = tiles_per_row * total_rows;
1907
1908 current_gfx_individual_.reserve(total_tiles);
1909
1910 // Extract individual 8x8 tiles from the graphics bitmap
1911 for (int tile_y = 0; tile_y < total_rows; ++tile_y) {
1912 for (int tile_x = 0; tile_x < tiles_per_row; ++tile_x) {
1913 zelda3::Tile8PixelData tile_data{};
1914
1915 // Extract tile data from the main graphics bitmap.
1916 // Preserve encoded palette offsets unless normalization is enabled.
1917 for (int py = 0; py < 8; ++py) {
1918 for (int px = 0; px < 8; ++px) {
1919 int src_x = tile_x * 8 + px;
1920 int src_y = tile_y * 8 + py;
1921 int src_index = src_y * current_gfx_bmp_->width() + src_x;
1922 int dst_index = py * 8 + px;
1923
1924 if (src_index < static_cast<int>(current_gfx_bmp_->size()) &&
1925 dst_index < 64) {
1926 uint8_t pixel_value = current_gfx_bmp_->data()[src_index];
1927
1929 pixel_value &= palette_normalization_mask_;
1930 }
1931
1932 tile_data[dst_index] = pixel_value;
1933 }
1934 }
1935 }
1936
1937 current_gfx_individual_.push_back(tile_data);
1938 }
1939 }
1940
1941 // Apply current palette settings to all tiles when a display palette is ready.
1942 // Some integration/headless initialization paths populate tile graphics before
1943 // the overworld palette is available; defer palette refresh in that case.
1944 if (rom_) {
1945 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
1946 if (display_palette && !display_palette->empty()) {
1948 } else {
1949 util::logf(
1950 "LoadTile8: display palette not available yet; deferring refresh");
1951 }
1952 }
1953
1954 // Ensure canvas scroll size matches the full tilesheet at preview scale
1958
1959 util::logf("Loaded %zu individual tile8 graphics",
1961 return absl::OkStatus();
1962}
1963
1964absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
1965 if (tile_id < 0 || tile_id >= kTile16Count) {
1966 return absl::OutOfRangeError(
1967 absl::StrFormat("Invalid tile16 id: %d", tile_id));
1968 }
1969
1970 if (!tile16_blockset_ || !rom_) {
1971 return absl::FailedPreconditionError(
1972 "Tile16 blockset or ROM not initialized");
1973 }
1974
1975 // Commit any in-progress edits before switching the current tile selection so
1976 // undo/redo captures the correct "after" state.
1978
1979 current_tile16_ = tile_id;
1980 jump_to_tile_id_ = tile_id; // Sync input field with current tile
1981
1982 // Load editable tile16 metadata from pending state first, then ROM.
1983 auto pending_it = pending_tile16_changes_.find(current_tile16_);
1984 if (pending_it != pending_tile16_changes_.end()) {
1985 current_tile16_data_ = pending_it->second;
1986 } else {
1989 }
1990 SyncTilesInfoArray(&current_tile16_data_);
1991
1992 bool bitmap_loaded = false;
1993
1994 auto pending_bitmap_it = pending_tile16_bitmaps_.find(current_tile16_);
1995 if (pending_bitmap_it != pending_tile16_bitmaps_.end() &&
1996 pending_bitmap_it->second.is_active()) {
1998 pending_bitmap_it->second.vector());
1999 current_tile16_bmp_.SetPalette(pending_bitmap_it->second.palette());
2000 bitmap_loaded = true;
2001 } else {
2002 auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
2003 if (!tile_data.empty()) {
2004 for (auto& pixel : tile_data) {
2007 }
2008 }
2010 bitmap_loaded = true;
2011 }
2012 }
2013
2014 if (!bitmap_loaded) {
2016 } else {
2021 }
2022 }
2023
2024 util::logf("SetCurrentTile: loaded tile %d successfully", tile_id);
2025
2028 }
2029 return absl::OkStatus();
2030}
2031
2032void Tile16Editor::RequestTileSwitch(int target_tile_id) {
2033 // Validate that the tile16 editor is properly initialized
2034 if (!tile16_blockset_ || !rom_) {
2035 util::logf(
2036 "RequestTileSwitch: Editor not initialized (blockset=%p, rom=%p)",
2038 return;
2039 }
2040
2041 // Validate target tile ID
2042 if (target_tile_id < 0 || target_tile_id >= kTile16Count) {
2043 util::logf("RequestTileSwitch: Invalid target tile ID %d", target_tile_id);
2044 return;
2045 }
2046
2047 // Check if we're already on this tile
2048 if (target_tile_id == current_tile16_) {
2049 return;
2050 }
2051
2052 // Check if current tile has pending changes
2054 // Store target and show dialog
2055 pending_tile_switch_target_ = target_tile_id;
2057 util::logf("Tile %d has pending changes, showing confirmation dialog",
2059 } else {
2060 // No pending changes, switch directly
2061 auto status = SetCurrentTile(target_tile_id);
2062 if (!status.ok()) {
2063 util::logf("Failed to switch to tile %d: %s", target_tile_id,
2064 status.message().data());
2065 }
2066 }
2067}
2068
2069absl::Status Tile16Editor::CopyTile16ToClipboard(int tile_id) {
2070 if (tile_id < 0 || tile_id >= kTile16Count) {
2071 return absl::InvalidArgumentError("Invalid tile ID");
2072 }
2073 if (!rom_) {
2074 return absl::FailedPreconditionError("ROM not available");
2075 }
2076
2077 auto pending_tile_it = pending_tile16_changes_.find(tile_id);
2078 if (tile_id == current_tile16_) {
2080 } else if (pending_tile_it != pending_tile16_changes_.end()) {
2081 clipboard_tile16_.tile_data = pending_tile_it->second;
2082 } else {
2085 }
2086 SyncTilesInfoArray(&clipboard_tile16_.tile_data);
2087
2088 bool bitmap_copied = false;
2089 auto pending_bitmap_it = pending_tile16_bitmaps_.find(tile_id);
2090 if (tile_id == current_tile16_ && current_tile16_bmp_.is_active()) {
2094 bitmap_copied = true;
2095 } else if (pending_bitmap_it != pending_tile16_bitmaps_.end() &&
2096 pending_bitmap_it->second.is_active()) {
2098 pending_bitmap_it->second.vector());
2099 clipboard_tile16_.bitmap.SetPalette(pending_bitmap_it->second.palette());
2100 bitmap_copied = true;
2101 } else if (tile16_blockset_) {
2102 auto tile_pixels = gfx::GetTilemapData(*tile16_blockset_, tile_id);
2103 if (!tile_pixels.empty()) {
2106 bitmap_copied = true;
2107 }
2108 }
2109
2110 if (bitmap_copied) {
2113 }
2114
2116 return absl::OkStatus();
2117}
2118
2147
2149 if (slot < 0 || slot >= kNumScratchSlots) {
2150 return absl::InvalidArgumentError("Invalid scratch space slot");
2151 }
2153 return absl::FailedPreconditionError("No active tile16 to save");
2154 }
2155
2156 scratch_space_[slot].tile_data = current_tile16_data_;
2157 SyncTilesInfoArray(&scratch_space_[slot].tile_data);
2158 scratch_space_[slot].bitmap.Create(kTile16Size, kTile16Size, 8,
2160 scratch_space_[slot].bitmap.SetPalette(current_tile16_bmp_.palette());
2161 // Queue texture creation via Arena's deferred system
2163 &scratch_space_[slot].bitmap);
2164
2165 scratch_space_[slot].has_data = true;
2166 return absl::OkStatus();
2167}
2168
2170 if (slot < 0 || slot >= kNumScratchSlots) {
2171 return absl::InvalidArgumentError("Invalid scratch space slot");
2172 }
2173
2174 if (!scratch_space_[slot].has_data) {
2175 return absl::FailedPreconditionError("Scratch space slot is empty");
2176 }
2177
2178 SaveUndoState();
2179
2180 current_tile16_data_ = scratch_space_[slot].tile_data;
2181 SyncTilesInfoArray(&current_tile16_data_);
2182
2183 if (scratch_space_[slot].bitmap.is_active()) {
2185 scratch_space_[slot].bitmap.vector());
2186 current_tile16_bmp_.SetPalette(scratch_space_[slot].bitmap.palette());
2190 } else {
2192 }
2193
2197 }
2199 return absl::OkStatus();
2200}
2201
2202absl::Status Tile16Editor::ClearScratchSpace(int slot) {
2203 if (slot < 0 || slot >= kNumScratchSlots) {
2204 return absl::InvalidArgumentError("Invalid scratch space slot");
2205 }
2206
2207 scratch_space_[slot].has_data = false;
2208 return absl::OkStatus();
2209}
2210
2211// Advanced editing features
2214 return absl::FailedPreconditionError("No active tile16 to flip");
2215 }
2216
2217 SaveUndoState();
2218
2220
2225 }
2226
2227 // Track this tile as having pending changes
2229
2230 return absl::OkStatus();
2231}
2232
2235 return absl::FailedPreconditionError("No active tile16 to flip");
2236 }
2237
2238 SaveUndoState();
2239
2241
2246 }
2247
2248 // Track this tile as having pending changes
2250
2251 return absl::OkStatus();
2252}
2253
2256 return absl::FailedPreconditionError("No active tile16 to rotate");
2257 }
2258
2259 SaveUndoState();
2260
2261 // Tile16 metadata does not support arbitrary 8x8 rotation flags.
2262 // Rotate the 2x2 quadrant layout in a persistable way.
2264
2269 }
2270
2271 // Track this tile as having pending changes
2273
2274 return absl::OkStatus();
2275}
2276
2277absl::Status Tile16Editor::FillTile16WithTile8(int tile8_id) {
2278 if (current_gfx_individual_.empty()) {
2279 if (!HasCurrentGfxBitmap()) {
2280 return absl::FailedPreconditionError("Source tile8 bitmap not active");
2281 }
2283 }
2284
2285 if (tile8_id < 0 ||
2286 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2287 return absl::InvalidArgumentError("Invalid tile8 ID");
2288 }
2289
2290 SaveUndoState();
2291
2292 const gfx::TileInfo fill_info(static_cast<uint16_t>(tile8_id),
2295 for (int quadrant = 0; quadrant < 4; ++quadrant) {
2296 TileInfoForQuadrant(&current_tile16_data_, quadrant) = fill_info;
2297 }
2298 SyncTilesInfoArray(&current_tile16_data_);
2299
2304 }
2305
2306 // Track this tile as having pending changes
2308
2309 return absl::OkStatus();
2310}
2311
2314 return absl::FailedPreconditionError("No active tile16 to clear");
2315 }
2316
2317 SaveUndoState();
2318
2319 const gfx::TileInfo clear_info(0, current_palette_, false, false, false);
2320 for (int quadrant = 0; quadrant < 4; ++quadrant) {
2321 TileInfoForQuadrant(&current_tile16_data_, quadrant) = clear_info;
2322 }
2323 SyncTilesInfoArray(&current_tile16_data_);
2324
2329 }
2330
2331 // Track this tile as having pending changes
2333
2334 return absl::OkStatus();
2335}
2336
2337// Palette management
2338absl::Status Tile16Editor::CyclePalette(bool forward) {
2339 uint8_t new_palette = current_palette_;
2340
2341 if (forward) {
2342 new_palette = (new_palette + 1) % 8;
2343 } else {
2344 new_palette = (new_palette == 0) ? 7 : new_palette - 1;
2345 }
2346
2347 current_palette_ = new_palette;
2348
2349 // Use the RefreshAllPalettes method which handles all the coordination
2351
2352 util::logf("Cycled to palette slot %d", current_palette_);
2353 return absl::OkStatus();
2354}
2355
2356absl::Status Tile16Editor::PreviewPaletteChange(uint8_t palette_id) {
2357 if (!show_palette_preview_) {
2358 return absl::OkStatus();
2359 }
2360
2361 if (palette_id >= 8) {
2362 return absl::InvalidArgumentError("Invalid palette ID");
2363 }
2364
2365 // Create a preview bitmap with the new palette
2366 if (!preview_tile16_.is_active()) {
2368 } else {
2369 // Recreate the preview bitmap with new data
2371 }
2372
2373 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
2374 if (!display_palette || display_palette->empty()) {
2375 return absl::OkStatus();
2376 }
2377
2378 const bool use_sub_palette_view =
2380 if (use_sub_palette_view) {
2381 const int sheet_index = GetSheetIndexForTile8(current_tile8_);
2382 const int palette_slot =
2383 GetActualPaletteSlot(static_cast<int>(palette_id), sheet_index);
2384 if (palette_slot >= 0 &&
2385 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
2387 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
2388 } else {
2389 preview_tile16_.SetPaletteWithTransparent(*display_palette, 1, 15);
2390 }
2391 } else {
2392 preview_tile16_.SetPalette(*display_palette);
2393 }
2394
2395 // Queue texture update via Arena's deferred system
2398 preview_dirty_ = true;
2399
2400 return absl::OkStatus();
2401}
2402
2403absl::Status Tile16Editor::ApplyPaletteToAll(uint8_t palette_id) {
2404 if (palette_id >= 8) {
2405 return absl::InvalidArgumentError("Invalid palette ID");
2406 }
2407
2408 auto* tile_data = GetCurrentTile16Data();
2409 if (!tile_data) {
2410 return absl::FailedPreconditionError("No current tile16 data");
2411 }
2412
2413 SaveUndoState();
2414 zelda3::SetTile16AllQuadrantPalettes(tile_data, palette_id);
2415
2416 // Update current palette to match
2417 current_palette_ = palette_id;
2418
2419 // Regenerate bitmap with new per-quadrant palette metadata
2421
2422 // Keep blockset/editor previews in sync with other tile16 edit operations.
2426 }
2427
2428 // Mark as modified
2430
2431 util::logf("Applied palette %d to all quadrants of tile %d", palette_id,
2433 return absl::OkStatus();
2434}
2435
2437 uint8_t palette_id) {
2438 if (palette_id >= 8) {
2439 return absl::InvalidArgumentError("Invalid palette ID");
2440 }
2441 if (quadrant < 0 || quadrant > 3) {
2442 return absl::InvalidArgumentError("Invalid quadrant index");
2443 }
2444
2445 auto* tile_data = GetCurrentTile16Data();
2446 if (!tile_data) {
2447 return absl::FailedPreconditionError("No current tile16 data");
2448 }
2449
2450 SaveUndoState();
2451 if (!zelda3::SetTile16QuadrantPalette(tile_data, quadrant, palette_id)) {
2452 return absl::InvalidArgumentError("Invalid quadrant index");
2453 }
2454 current_palette_ = palette_id;
2455
2460 }
2461
2463 util::logf("Applied palette %d to quadrant %d of tile %d", palette_id,
2464 quadrant, current_tile16_);
2465 return absl::OkStatus();
2466}
2467
2468// Undo/Redo system (unified UndoManager framework)
2469
2485
2487 if (!pending_undo_before_.has_value())
2488 return;
2490 pending_undo_before_.reset();
2491 return;
2492 }
2493
2494 // Capture the current (post-edit) state as the "after" snapshot
2495 Tile16Snapshot after;
2496 after.tile_id = current_tile16_;
2500 after.palette = current_palette_;
2501 after.x_flip = x_flip;
2502 after.y_flip = y_flip;
2503 after.priority = priority_tile;
2504
2505 // Build the restore callback that captures `this`
2506 auto restore_fn = [this](const Tile16Snapshot& snap) {
2507 RestoreFromSnapshot(snap);
2508 };
2509
2510 undo_manager_.Push(std::make_unique<Tile16EditAction>(
2511 std::move(*pending_undo_before_), std::move(after), restore_fn));
2512
2513 pending_undo_before_.reset();
2514}
2515
2518 return;
2519 }
2520
2521 // Finalize any previously pending snapshot before starting a new one
2523
2524 Tile16Snapshot before;
2525 before.tile_id = current_tile16_;
2529 before.palette = current_palette_;
2530 before.x_flip = x_flip;
2531 before.y_flip = y_flip;
2532 before.priority = priority_tile;
2533
2534 pending_undo_before_ = std::move(before);
2535}
2536
2537absl::Status Tile16Editor::Undo() {
2539 return undo_manager_.Undo();
2540}
2541
2542absl::Status Tile16Editor::Redo() {
2543 return undo_manager_.Redo();
2544}
2545
2547 if (!tile16_blockset_) {
2548 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2549 }
2550
2551 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2552 return absl::OutOfRangeError("Current tile16 ID out of range");
2553 }
2554
2555 if (current_palette_ >= 8) {
2556 return absl::OutOfRangeError("Current palette ID out of range");
2557 }
2558
2559 return absl::OkStatus();
2560}
2561
2562bool Tile16Editor::IsTile16Valid(int tile_id) const {
2563 return tile16_blockset_ != nullptr && tile_id >= 0 && tile_id < kTile16Count;
2564}
2565
2566// Integration with overworld system
2568 if (!rom_) {
2569 return absl::FailedPreconditionError("ROM not available");
2570 }
2571
2573 return absl::FailedPreconditionError("No active tile16 to save");
2574 }
2575
2576 // Write the tile16 data to ROM first
2578
2579 // Update the tile16 blockset with current changes
2581
2582 // Commit changes to the tile16 blockset
2584
2587
2588 // Mark ROM as dirty so changes persist when saving
2589 rom_->set_dirty(true);
2592 last_rom_write_time_ = std::chrono::steady_clock::now();
2594
2595 util::logf("Tile16 %d saved to ROM", current_tile16_);
2596 return absl::OkStatus();
2597}
2598
2600 if (!tile16_blockset_) {
2601 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2602 }
2603
2604 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2605 return absl::OutOfRangeError("Current tile16 ID out of range");
2606 }
2607
2608 // Update atlas directly instead of using problematic tile cache
2610
2611 return absl::OkStatus();
2612}
2613
2615 if (!tile16_blockset_) {
2616 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2617 }
2618
2619 // Regenerate the tilemap data if needed
2621 // Queue texture update via Arena's deferred system
2624 }
2625
2626 // Update individual cached tiles
2627 // Note: With the new tile cache system, tiles are automatically managed
2628 // and don't need manual modification tracking like the old system
2629 // The cache handles LRU eviction and automatic updates
2630
2631 return absl::OkStatus();
2632}
2633
2635 // Step 1: Update ROM data with current tile16 changes
2637
2638 // Step 2: Update the local blockset to reflect changes
2640
2641 // Step 3: Update the atlas directly
2643
2644 // Step 4: Notify the parent editor (overworld editor) to regenerate its
2645 // blockset
2648 }
2649
2654 last_rom_write_time_ = std::chrono::steady_clock::now();
2656
2657 util::logf("Committed Tile16 %d changes to overworld system",
2659 return absl::OkStatus();
2660}
2661
2663 // Drop the current tile's staged copy first; SetCurrentTile consults pending
2664 // state before ROM state.
2668
2669 // Reload the current tile16 from ROM to discard any local changes
2671
2672 util::logf("Discarded Tile16 changes for tile %d", current_tile16_);
2673 return absl::OkStatus();
2674}
2675
2677 if (pending_tile16_changes_.empty()) {
2678 return absl::OkStatus(); // Nothing to commit
2679 }
2680
2681 const int written_count = static_cast<int>(pending_tile16_changes_.size());
2682 util::logf("Committing %zu pending tile16 changes to ROM",
2684
2685 // Write all pending changes to ROM
2686 for (const auto& [tile_id, tile_data] : pending_tile16_changes_) {
2687 auto status = rom_->WriteTile16(tile_id, zelda3::kTile16Ptr, tile_data);
2688 if (!status.ok()) {
2689 util::logf("Failed to write tile16 %d: %s", tile_id,
2690 status.message().data());
2691 return status;
2692 }
2693 }
2694
2695 // Clear pending changes before parent refresh (overworld reads committed ROM).
2698
2699 // Local atlas hint; full rebuild is typically done in overworld callback.
2701
2702 // Notify parent editor to refresh overworld display
2705 }
2706
2707 rom_->set_dirty(true);
2709 last_rom_write_count_ = written_count;
2710 last_rom_write_time_ = std::chrono::steady_clock::now();
2712 util::logf("All pending tile16 changes committed successfully");
2713 return absl::OkStatus();
2714}
2715
2717 if (pending_tile16_changes_.empty()) {
2718 return;
2719 }
2720
2721 util::logf("Discarding %zu pending tile16 changes",
2723
2727
2728 // Reload current tile to restore original state
2729 auto status = SetCurrentTile(current_tile16_);
2730 if (!status.ok()) {
2731 util::logf("Failed to reload tile after discard: %s",
2732 status.message().data());
2733 }
2734}
2735
2738 if (it != pending_tile16_changes_.end()) {
2739 pending_tile16_changes_.erase(it);
2742 util::logf("Discarded pending changes for tile %d", current_tile16_);
2743 }
2744
2745 // Reload tile from ROM
2746 auto status = SetCurrentTile(current_tile16_);
2747 if (!status.ok()) {
2748 util::logf("Failed to reload tile after discard: %s",
2749 status.message().data());
2750 }
2751}
2752
2754 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2755 return;
2756 }
2757
2758 SyncTilesInfoArray(&current_tile16_data_);
2761 preview_dirty_ = true;
2763
2764 util::logf("Marked tile %d as modified (total pending: %zu)", current_tile16_,
2766}
2767
2769 if (!rom_) {
2770 return absl::FailedPreconditionError("ROM not available for usage cache");
2771 }
2772
2773 const int total_tiles = zelda3::ComputeTile16Count(tile16_blockset_);
2774 auto tile_provider = [this](int tile_id) -> absl::StatusOr<gfx::Tile16> {
2775 auto pending_it = pending_tile16_changes_.find(tile_id);
2776 if (pending_it != pending_tile16_changes_.end()) {
2777 return pending_it->second;
2778 }
2779 return rom_->ReadTile16(tile_id, zelda3::kTile16Ptr);
2780 };
2781
2782 RETURN_IF_ERROR(zelda3::BuildTile8UsageIndex(total_tiles, tile_provider,
2784
2786 return absl::OkStatus();
2787}
2788
2792 return;
2793 }
2794
2796 auto cache_status = RebuildTile8UsageCache();
2797 if (!cache_status.ok()) {
2798 util::logf("Tile8 usage cache rebuild failed: %s",
2799 cache_status.message().data());
2800 return;
2801 }
2802 }
2803
2804 const auto& hits = tile8_usage_cache_[current_tile8_];
2805 if (hits.empty()) {
2806 return;
2807 }
2808
2809 ImDrawList* draw_list = ImGui::GetWindowDrawList();
2810 const ImVec2 canvas_pos = blockset_canvas_.zero_point();
2811 const float scale = blockset_canvas_.GetGlobalScale();
2812 const float tile16_display = 32.0f * scale;
2813 const float quadrant_display = 16.0f * scale;
2814 const int tiles_per_row =
2815 std::max(1, tile16_blockset_bmp_->width() / kTile16Size);
2816
2817 for (const auto& hit : hits) {
2818 const int tile_x = hit.tile16_id % tiles_per_row;
2819 const int tile_y = hit.tile16_id / tiles_per_row;
2820 const int quad_x = hit.quadrant % 2;
2821 const int quad_y = hit.quadrant / 2;
2822
2823 const ImVec2 min(
2824 canvas_pos.x + (tile_x * tile16_display) + (quad_x * quadrant_display),
2825 canvas_pos.y + (tile_y * tile16_display) + (quad_y * quadrant_display));
2826 const ImVec2 max(min.x + quadrant_display, min.y + quadrant_display);
2827
2828 // Mirrors ZScream's right-click usage tint (purple, transparent).
2829 draw_list->AddRectFilled(min, max, IM_COL32(150, 0, 210, 80));
2830 draw_list->AddRect(min, max, IM_COL32(215, 170, 255, 180));
2831 }
2832}
2833
2834absl::Status Tile16Editor::PickTile8FromTile16(const ImVec2& position) {
2835 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2836 return absl::InvalidArgumentError("Invalid tile16 or ROM not set");
2837 }
2838
2839 // Determine which quadrant of the tile16 was clicked
2840 int quad_x = (position.x < 8) ? 0 : 1; // Left or right half
2841 int quad_y = (position.y < 8) ? 0 : 1; // Top or bottom half
2842 int quadrant = quad_x + (quad_y * 2); // 0=TL, 1=TR, 2=BL, 3=BR
2843 active_quadrant_ = std::clamp(quadrant, 0, 3);
2844
2845 // Get the tile16 data structure
2846 auto* tile16_data = GetCurrentTile16Data();
2847 if (!tile16_data) {
2848 return absl::FailedPreconditionError("Failed to get tile16 data");
2849 }
2850
2851 // Extract tile metadata from the clicked quadrant.
2852 gfx::TileInfo tile_info = TileInfoForQuadrant(*tile16_data, quadrant);
2853
2854 // Set the current tile8 and palette
2855 current_tile8_ = tile_info.id_;
2856 current_palette_ = tile_info.palette_;
2857
2858 // Update the flip states based on the tile info
2859 x_flip = tile_info.horizontal_mirror_;
2860 y_flip = tile_info.vertical_mirror_;
2861 priority_tile = tile_info.over_;
2862
2863 // Refresh the palette to match the picked tile
2866
2867 util::logf("Picked tile8 %d with palette %d from quadrant %d of tile16 %d",
2869
2870 return absl::OkStatus();
2871}
2872
2873// Get the appropriate palette slot for current graphics sheet
2874int Tile16Editor::GetPaletteSlotForSheet(int sheet_index) const {
2875 // Based on ProcessGraphicsBuffer logic and overworld palette coordination:
2876 // Sheets 0,3-6: Use AUX palettes (slots 10-15 in 256-color palette)
2877 // Sheets 1-2: Use MAIN palette (slots 2-6 in 256-color palette)
2878 // Sheet 7: Use ANIMATED palette (slot 7 in 256-color palette)
2879
2880 switch (sheet_index) {
2881 case 0:
2882 return 10; // Main blockset -> AUX1 palette region
2883 case 1:
2884 return 2; // Main graphics -> MAIN palette region
2885 case 2:
2886 return 3; // Main graphics -> MAIN palette region
2887 case 3:
2888 return 11; // Area graphics -> AUX1 palette region
2889 case 4:
2890 return 12; // Area graphics -> AUX1 palette region
2891 case 5:
2892 return 13; // Area graphics -> AUX2 palette region
2893 case 6:
2894 return 14; // Area graphics -> AUX2 palette region
2895 case 7:
2896 return 7; // Animated tiles -> ANIMATED palette region
2897 default:
2898 return static_cast<int>(
2899 current_palette_); // Use current selection for other sheets
2900 }
2901}
2902
2903// NEW: Get the actual palette slot for a given palette button and sheet index
2904// This uses row-based addressing to match the overworld's approach:
2905// The 256-color palette is organized as 16 rows of 16 colors each.
2906// Palette buttons 0-7 map to CGRAM rows starting at the sheet's base row,
2907// skipping HUD rows for overworld visuals.
2909 int sheet_index) const {
2910 const int clamped_button = std::clamp(palette_button, 0, 7);
2911 const int base_row = GetPaletteBaseForSheet(sheet_index);
2912 const int actual_row = std::clamp(base_row + clamped_button, 0, 15);
2913
2914 // Palette buttons map to CGRAM rows starting from the sheet base.
2915 return actual_row * 16;
2916}
2917
2918// NEW: Get the sheet index for a given tile8 ID
2920 // Determine which graphics sheet a tile8 belongs to based on its position
2921 // This is based on the 256-tile per sheet organization
2922
2923 constexpr int kTilesPerSheet = 256; // 16x16 tiles per sheet
2924 int sheet_index = tile8_id / kTilesPerSheet;
2925
2926 // Clamp to valid sheet range (0-7)
2927 return std::min(7, std::max(0, sheet_index));
2928}
2929
2930// NEW: Get the actual palette slot for the current tile16 being edited
2932 // For the current tile16, we need to determine which sheet the tile8s belong
2933 // to and use the most appropriate palette region
2934
2935 if (current_tile8_ >= 0 &&
2936 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
2937 int sheet_index = GetSheetIndexForTile8(current_tile8_);
2938 return GetActualPaletteSlot(current_palette_, sheet_index);
2939 }
2940
2941 // Default to sheet 0 (main blockset) if no tile8 selected
2943}
2944
2945int Tile16Editor::GetPaletteBaseForSheet(int sheet_index) const {
2946 // Based on overworld palette structure and how ProcessGraphicsBuffer assigns
2947 // colors: The 256-color palette is organized as 16 rows of 16 colors each.
2948 // Different graphics sheets map to different palette regions:
2949 //
2950 // Row 0: Transparent/system colors
2951 // Row 1: HUD colors (palette index 0x10-0x1F)
2952 // Rows 2-4: MAIN/AUX1 palette region for main graphics
2953 // Rows 5-7: AUX2 palette region for area-specific graphics
2954 // Row 7: ANIMATED palette for animated tiles
2955 //
2956 // The palette_button (0-7) selects within the region.
2957 switch (sheet_index) {
2958 case 0: // Main blockset
2959 case 3: // Area graphics set 1
2960 case 4: // Area graphics set 2
2961 return 2; // AUX1 palette region starts at row 2
2962 case 5: // Area graphics set 3
2963 case 6: // Area graphics set 4
2964 return 5; // AUX2 palette region starts at row 5
2965 case 1: // Main graphics
2966 case 2: // Main graphics
2967 return 2; // MAIN palette region starts at row 2
2968 case 7: // Animated tiles
2969 return 7; // ANIMATED palette region at row 7
2970 default:
2971 return 2; // Default to MAIN region
2972 }
2973}
2974
2976 const gfx::SnesPalette& source, int target_row) const {
2977 // Create a remapped 256-color palette where all pixel values (0-255)
2978 // are mapped to the target palette row based on their low nibble.
2979 //
2980 // This allows the source bitmap (which has pre-encoded palette offsets)
2981 // to be viewed with the user-selected palette row.
2982 //
2983 // For each palette index i:
2984 // - Extract the color index: low_nibble = i & 0x0F
2985 // - Map to target row: (base_row + target_row) * 16 + low_nibble
2986 // - Copy the color from source palette at that position
2987
2988 gfx::SnesPalette remapped;
2989
2990 // Map palette buttons to actual CGRAM rows based on the current sheet.
2991 int sheet_index = 0;
2992 if (current_tile8_ >= 0 &&
2993 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
2995 }
2996 const int base_row = GetPaletteBaseForSheet(sheet_index);
2997 const int actual_target_row = std::clamp(base_row + target_row, 0, 15);
2998
2999 for (int i = 0; i < 256; ++i) {
3000 int low_nibble = i & 0x0F;
3001 int target_index = (actual_target_row * 16) + low_nibble;
3002
3003 // Make color 0 of each row transparent
3004 if (low_nibble == 0) {
3005 // Use transparent color (alpha = 0)
3006 remapped.AddColor(gfx::SnesColor(0));
3007 } else if (target_index < static_cast<int>(source.size())) {
3008 remapped.AddColor(source[target_index]);
3009 } else {
3010 // Fallback to black if out of bounds
3011 remapped.AddColor(gfx::SnesColor(0));
3012 }
3013 }
3014
3015 return remapped;
3016}
3017
3018int Tile16Editor::GetEncodedPaletteRow(uint8_t pixel_value) const {
3019 // Determine which palette row a pixel value encodes
3020 // ProcessGraphicsBuffer adds 0x88 (136) to sheets 0, 3, 4, 5
3021 // So pixel values map to rows as follows:
3022 // 0x00-0x0F (0-15): Row 0
3023 // 0x10-0x1F (16-31): Row 1
3024 // ...
3025 // 0x80-0x8F (128-143): Row 8
3026 // 0x90-0x9F (144-159): Row 9
3027 // etc.
3028 return pixel_value / 16;
3029}
3030
3032 if (overworld_palette_.size() >= 256) {
3033 return &overworld_palette_;
3034 }
3035 if (palette_.size() >= 256) {
3036 return &palette_;
3037 }
3038 if (game_data() && !game_data()->palette_groups.overworld_main.empty()) {
3040 }
3041 return nullptr;
3042}
3043
3045 const gfx::Bitmap& bitmap) const {
3046 if (!bitmap.is_active() || bitmap.data() == nullptr) {
3047 return false;
3048 }
3049
3050 for (size_t i = 0; i < bitmap.size(); ++i) {
3051 if ((bitmap.data()[i] & 0xF0) != 0) {
3052 return true;
3053 }
3054 }
3055 return false;
3056}
3057
3060 return;
3061 }
3062
3063 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
3064 gfx::SnesPalette fallback_palette;
3065 if (!display_palette || display_palette->empty()) {
3066 fallback_palette = BuildFallbackDisplayPalette();
3067 display_palette = &fallback_palette;
3068 }
3069
3070 // Most tile16 edit paths now encode the palette row directly in pixel indices
3071 // via (pixel & 0x0F) + (palette * 0x10). In that case, apply the full palette.
3072 // If normalization produced low-nibble-only pixels, keep the legacy sub-palette
3073 // view path so the advanced normalization workflow still renders correctly.
3076 const int palette_slot = GetActualPaletteSlotForCurrentTile16();
3077 if (palette_slot >= 0 &&
3078 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
3080 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
3081 } else {
3082 current_tile16_bmp_.SetPaletteWithTransparent(*display_palette, 1, 15);
3083 }
3084 } else {
3085 current_tile16_bmp_.SetPalette(*display_palette);
3086 }
3087
3091}
3092
3093// Helper methods for palette management
3094absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) {
3095 if (tile8_id < 0 ||
3096 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
3097 return absl::InvalidArgumentError("Invalid tile8 ID");
3098 }
3099
3100 if (!rom_) {
3101 return absl::FailedPreconditionError("ROM not set");
3102 }
3103
3104 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
3105 if (!display_palette || display_palette->empty()) {
3106 return absl::FailedPreconditionError("No overworld palette available");
3107 }
3108
3109 // Validate current_palette_ index
3110 if (current_palette_ < 0 || current_palette_ >= 8) {
3111 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
3112 current_palette_ = 0;
3113 }
3114
3115 const int sheet_index = GetSheetIndexForTile8(tile8_id);
3116 const int palette_slot =
3117 GetActualPaletteSlot(static_cast<int>(current_palette_), sheet_index);
3118
3119 util::logf("Updated tile8 %d with palette slot %d (palette size: %zu colors)",
3120 tile8_id, current_palette_, display_palette->size());
3121
3122 return absl::OkStatus();
3123}
3124
3126 if (!rom_) {
3127 return absl::FailedPreconditionError("ROM not set");
3128 }
3129
3130 // Validate current_palette_ index
3131 if (current_palette_ < 0 || current_palette_ >= 8) {
3132 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
3133 current_palette_ = 0;
3134 }
3135
3136 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
3137 gfx::SnesPalette fallback_palette;
3138 if (!display_palette || display_palette->empty()) {
3139 fallback_palette = BuildFallbackDisplayPalette();
3140 display_palette = &fallback_palette;
3141 util::logf("Display palette unavailable; using fallback grayscale palette");
3142 }
3143 util::logf("Using resolved display palette with %zu colors",
3144 display_palette->size());
3145
3146 // The source bitmap (current_gfx_bmp_) contains 8bpp indexed pixel data.
3147 // If palette offsets are preserved, apply the full CGRAM palette. Otherwise,
3148 // remap the palette to the user-selected row.
3149 if (HasCurrentGfxBitmap()) {
3151 gfx::SnesPalette remapped_palette =
3153 current_gfx_bmp_->SetPalette(remapped_palette);
3154 util::logf("Applied remapped palette (button %d) to source bitmap",
3156 } else {
3157 current_gfx_bmp_->SetPalette(*display_palette);
3158 util::logf("Applied full CGRAM palette to source bitmap");
3159 }
3160
3162 // Queue texture update via Arena's deferred system
3165 }
3166
3167 // Update current tile16 being edited - regenerate from ROM so per-quadrant
3168 // palette metadata is applied via the pixel transform
3170 auto regen_status = RegenerateTile16BitmapFromROM();
3171 if (!regen_status.ok()) {
3172 // Fallback: just apply palette directly
3173 current_tile16_bmp_.SetPalette(*display_palette);
3177 }
3178 }
3179
3180 util::logf(
3181 "Successfully refreshed all palettes in tile16 editor with palette %d",
3183 return absl::OkStatus();
3184}
3185
3187 util::logf("=== TILE8 SOURCE DATA ANALYSIS ===");
3188
3189 // Analyze current_gfx_bmp_
3190 util::logf("current_gfx_bmp_:");
3191 util::logf(" - Active: %s", HasCurrentGfxBitmap() ? "yes" : "no");
3192 util::logf(" - Size: %dx%d", current_gfx_bmp_->width(),
3194 util::logf(" - Depth: %d bpp", current_gfx_bmp_->depth());
3195 util::logf(" - Data size: %zu bytes", current_gfx_bmp_->size());
3196 util::logf(" - Palette size: %zu colors",
3198
3199 // Analyze pixel value distribution in first 64 pixels (first tile8)
3200 if (current_gfx_bmp_->data() && current_gfx_bmp_->size() >= 64) {
3201 std::map<uint8_t, int> pixel_counts;
3202 for (size_t i = 0; i < 64; ++i) {
3203 uint8_t val = current_gfx_bmp_->data()[i];
3204 pixel_counts[val]++;
3205 }
3206 util::logf(" - First tile8 (Sheet 0) pixel distribution:");
3207 for (const auto& [val, count] : pixel_counts) {
3208 int row = GetEncodedPaletteRow(val);
3209 int col = val & 0x0F;
3210 util::logf(" Value 0x%02X (%3d) = Row %d, Col %d: %d pixels", val, val,
3211 row, col, count);
3212 }
3213
3214 // Check if values are in expected 4bpp range
3215 bool all_4bpp = true;
3216 for (const auto& [val, count] : pixel_counts) {
3217 if (val > 15) {
3218 all_4bpp = false;
3219 break;
3220 }
3221 }
3222 util::logf(" - Values in raw 4bpp range (0-15): %s",
3223 all_4bpp ? "yes" : "NO (pre-encoded)");
3224
3225 // Show what the remapping does
3226 util::logf(" - Palette remapping for viewing:");
3227 util::logf(" Selected palette: %d (row %d)", current_palette_,
3229 util::logf(" Pixels are remapped: (value & 0x0F) + (selected_row * 16)");
3230 }
3231
3232 // Analyze current_gfx_individual_
3233 util::logf("current_gfx_individual_:");
3234 util::logf(" - Count: %zu tiles", current_gfx_individual_.size());
3235
3236 if (!current_gfx_individual_.empty()) {
3237 const auto& first_tile = current_gfx_individual_[0];
3238 util::logf(" - First tile:");
3239 util::logf(" - Size: 8x8");
3240 util::logf(" - Depth: 8 bpp");
3241 std::map<uint8_t, int> pixel_counts;
3242 for (uint8_t val : first_tile) {
3243 pixel_counts[val]++;
3244 }
3245 util::logf(" - Pixel distribution:");
3246 for (const auto& [val, count] : pixel_counts) {
3247 util::logf(" Value 0x%02X (%3d): %d pixels", val, val, count);
3248 }
3249 }
3250
3251 // Analyze palette state
3252 util::logf("Palette state:");
3253 util::logf(" - current_palette_: %d", current_palette_);
3254 util::logf(" - overworld_palette_ size: %zu", overworld_palette_.size());
3255 util::logf(" - palette_ size: %zu", palette_.size());
3256
3257 // Calculate expected palette slot
3258 int palette_slot = GetActualPaletteSlot(current_palette_, 0);
3259 util::logf(" - GetActualPaletteSlot(%d, 0) = %d", current_palette_,
3260 palette_slot);
3261 util::logf(" - Expected palette offset for SetPaletteWithTransparent: %d",
3262 palette_slot + 1);
3263
3264 // Show first 16 colors of the overworld palette
3265 if (overworld_palette_.size() >= 16) {
3266 util::logf(" - First 16 palette colors (row 0):");
3267 for (int i = 0; i < 16; ++i) {
3268 auto color = overworld_palette_[i];
3269 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3270 static_cast<int>(color.rgb().x),
3271 static_cast<int>(color.rgb().y),
3272 static_cast<int>(color.rgb().z));
3273 }
3274 }
3275
3276 // Show colors at the selected palette slot
3277 if (overworld_palette_.size() >= static_cast<size_t>(palette_slot + 16)) {
3278 util::logf(" - Colors at palette slot %d (row %d):", palette_slot,
3279 palette_slot / 16);
3280 for (int i = 0; i < 16; ++i) {
3281 auto color = overworld_palette_[palette_slot + i];
3282 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3283 static_cast<int>(color.rgb().x),
3284 static_cast<int>(color.rgb().y),
3285 static_cast<int>(color.rgb().z));
3286 }
3287 }
3288
3289 util::logf("=== END ANALYSIS ===");
3290}
3291
3294 if (Begin("Advanced Palette Settings", &show_palette_settings_)) {
3295 Text("Pixel Normalization & Color Correction:");
3296
3297 int mask_value = static_cast<int>(palette_normalization_mask_);
3298 if (SliderInt("Normalization Mask", &mask_value, 1, 255, "0x%02X")) {
3299 palette_normalization_mask_ = static_cast<uint8_t>(mask_value);
3300 }
3301
3302 Checkbox("Auto Normalize Pixels", &auto_normalize_pixels_);
3303
3304 if (Button("Apply to All Graphics")) {
3305 auto reload_result = LoadTile8();
3306 if (!reload_result.ok()) {
3307 Text("Error: %s", reload_result.message().data());
3308 }
3309 }
3310
3311 SameLine();
3312 if (Button("Reset Defaults")) {
3315 auto reload_result = LoadTile8();
3316 (void)reload_result; // Suppress warning
3317 }
3318
3319 Separator();
3320 Text("Current State:");
3321 static constexpr std::array<const char*, 7> palette_group_names = {
3322 "OW Main", "OW Aux", "OW Anim", "Dungeon",
3323 "Sprites", "Armor", "Sword"};
3324 Text("Palette Group: %d (%s)", current_palette_group_,
3326 ? palette_group_names[current_palette_group_]
3327 : "Unknown");
3328 Text("Current Palette: %d", current_palette_);
3329
3330 Separator();
3331 Text("Sheet-Specific Fixes:");
3332
3333 // Sheet-specific palette fixes
3334 static bool fix_sheet_0 = true;
3335 static bool fix_sprite_sheets = true;
3336 static bool use_transparent_for_terrain = false;
3337
3338 if (Checkbox("Fix Sheet 0 (Trees)", &fix_sheet_0)) {
3339 auto reload_result = LoadTile8();
3340 if (!reload_result.ok()) {
3341 Text("Error reloading: %s", reload_result.message().data());
3342 }
3343 }
3344 HOVER_HINT(
3345 "Use direct palette for sheet 0 instead of transparent palette");
3346
3347 if (Checkbox("Fix Sprite Sheets", &fix_sprite_sheets)) {
3348 auto reload_result = LoadTile8();
3349 if (!reload_result.ok()) {
3350 Text("Error reloading: %s", reload_result.message().data());
3351 }
3352 }
3353 HOVER_HINT("Use direct palette for sprite graphics sheets");
3354
3355 if (Checkbox("Transparent for Terrain", &use_transparent_for_terrain)) {
3356 auto reload_result = LoadTile8();
3357 if (!reload_result.ok()) {
3358 Text("Error reloading: %s", reload_result.message().data());
3359 }
3360 }
3361 HOVER_HINT("Force transparent palette for terrain graphics");
3362
3363 Separator();
3364 Text("Color Analysis:");
3365 if (current_tile8_ >= 0 &&
3366 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
3367 Text("Selected Tile8 Analysis:");
3368 const auto& tile_data = current_gfx_individual_[current_tile8_];
3369 std::map<uint8_t, int> pixel_counts;
3370 for (uint8_t pixel : tile_data) {
3371 pixel_counts[pixel & 0x0F]++; // Normalize to 4-bit
3372 }
3373
3374 Text("Pixel Value Distribution:");
3375 for (const auto& pair : pixel_counts) {
3376 int value = pair.first;
3377 int count = pair.second;
3378 Text(" Value %d (0x%X): %d pixels", value, value, count);
3379 }
3380
3381 Text("Palette Colors Used:");
3382 const gfx::SnesPalette* analysis_palette = ResolveDisplayPalette();
3383 if (analysis_palette == nullptr || analysis_palette->empty()) {
3384 analysis_palette =
3386 }
3387 for (const auto& pair : pixel_counts) {
3388 int value = pair.first;
3389 int count = pair.second;
3390 if (value < static_cast<int>(analysis_palette->size())) {
3391 auto color = (*analysis_palette)[value];
3392 ImVec4 display_color = color.rgb();
3393 ImGui::ColorButton(("##analysis" + std::to_string(value)).c_str(),
3394 display_color, ImGuiColorEditFlags_NoTooltip,
3395 ImVec2(16, 16));
3396 if (ImGui::IsItemHovered()) {
3397 ImGui::SetTooltip("Index %d: 0x%04X (%d pixels)", value,
3398 color.snes(), count);
3399 }
3400 if (value % 8 != 7)
3401 ImGui::SameLine();
3402 }
3403 }
3404 }
3405
3406 // Enhanced ROM Palette Management Section
3407 Separator();
3408 if (CollapsingHeader("ROM Palette Manager") && rom_) {
3409 Text("Experimental ROM Palette Selection:");
3410 HOVER_HINT(
3411 "Use ROM palettes to experiment with different color schemes");
3412
3413 if (Button("Open Enhanced Palette Editor")) {
3415 }
3416 SameLine();
3417 if (Button("Show Color Analysis")) {
3419 }
3420
3421 // Quick palette application
3422 static int quick_group = 0;
3423 static int quick_index = 0;
3424
3425 SliderInt("ROM Group", &quick_group, 0, 6);
3426 SliderInt("Palette Index", &quick_index, 0, 7);
3427
3428 if (Button("Apply to Tile8 Source")) {
3429 if (tile8_source_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3430 util::logf("Applied ROM palette group %d, index %d to Tile8 source",
3431 quick_group, quick_index);
3432 }
3433 }
3434 SameLine();
3435 if (Button("Apply to Tile16 Editor")) {
3436 if (tile16_edit_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3437 util::logf(
3438 "Applied ROM palette group %d, index %d to Tile16 editor",
3439 quick_group, quick_index);
3440 }
3441 }
3442 }
3443 }
3444 End();
3445 }
3446}
3447
3449 Text("Layout Scratch:");
3450 for (int i = 0; i < 4; ++i) {
3451 ImGui::PushID(i);
3452 std::string slot_name = "S" + std::to_string(i + 1);
3453
3454 if (Button((slot_name + " Save").c_str(), ImVec2(70, 20))) {
3456 }
3457 SameLine();
3458
3459 bool can_load = layout_scratch_[i].in_use;
3460 if (!can_load) {
3461 ImGui::BeginDisabled();
3462 }
3463 if (Button((slot_name + " Load").c_str(), ImVec2(70, 20)) && can_load) {
3465 }
3466 if (!can_load) {
3467 ImGui::EndDisabled();
3468 }
3469 SameLine();
3470 TextDisabled("%s", layout_scratch_[i].name.c_str());
3471 ImGui::PopID();
3472 }
3473}
3474
3476 if (slot < 0 || slot >= 4) {
3477 return absl::InvalidArgumentError("Invalid scratch slot");
3478 }
3479
3481 if (total_tiles <= 0) {
3482 return absl::FailedPreconditionError("Tile16 blockset is not available");
3483 }
3484
3485 const int start_tile = std::clamp(current_tile16_, 0, total_tiles - 1);
3486 for (int y = 0; y < 8; ++y) {
3487 for (int x = 0; x < 8; ++x) {
3488 const int tile_id = start_tile + (y * 8) + x;
3489 layout_scratch_[slot].tile_layout[y][x] =
3490 (tile_id < total_tiles) ? tile_id : -1;
3491 }
3492 }
3493
3494 layout_scratch_[slot].in_use = true;
3495 layout_scratch_[slot].name =
3496 absl::StrFormat("From %03X", static_cast<uint16_t>(start_tile));
3497
3498 return absl::OkStatus();
3499}
3500
3502 if (slot < 0 || slot >= 4) {
3503 return absl::InvalidArgumentError("Invalid scratch slot");
3504 }
3505
3506 if (!layout_scratch_[slot].in_use) {
3507 return absl::FailedPreconditionError("Scratch slot is empty");
3508 }
3509
3510 const int first_tile = layout_scratch_[slot].tile_layout[0][0];
3511 if (first_tile < 0) {
3512 return absl::FailedPreconditionError("Scratch slot has no valid tile data");
3513 }
3514
3515 RETURN_IF_ERROR(SetCurrentTile(first_tile));
3517 scroll_to_current_ = true;
3518
3519 selected_tiles_.clear();
3520 selected_tiles_.reserve(64);
3521 for (int y = 0; y < 8; ++y) {
3522 for (int x = 0; x < 8; ++x) {
3523 const int tile_id = layout_scratch_[slot].tile_layout[y][x];
3524 if (tile_id >= 0) {
3525 selected_tiles_.push_back(tile_id);
3526 }
3527 }
3528 }
3529 selection_start_tile_ = first_tile;
3530
3531 return absl::OkStatus();
3532}
3533
3535 if (ImGui::BeginPopupModal("ManualTile8Editor", nullptr,
3536 ImGuiWindowFlags_AlwaysAutoResize)) {
3537 ImGui::Text("Manual Tile8 Configuration for Tile16 %02X", current_tile16_);
3538 ImGui::Separator();
3539
3540 auto* tile_data = GetCurrentTile16Data();
3541 if (tile_data) {
3542 ImGui::Text("Current Tile16 Staged Data:");
3543
3544 auto stage_current_tile = [&]() -> absl::Status {
3545 SyncTilesInfoArray(tile_data);
3550 }
3552 return absl::OkStatus();
3553 };
3554
3555 // Display and edit each quadrant using TileInfo structure
3556 const char* quadrant_names[] = {"Top-Left", "Top-Right", "Bottom-Left",
3557 "Bottom-Right"};
3558
3559 for (int q = 0; q < 4; q++) {
3560 ImGui::Text("%s Quadrant:", quadrant_names[q]);
3561 ImGui::TextDisabled("Tile Palette metadata + Tile8/flip flags");
3562
3563 // Get the current TileInfo for this quadrant
3564 gfx::TileInfo* tile_info = nullptr;
3565 switch (q) {
3566 case 0:
3567 tile_info = &tile_data->tile0_;
3568 break;
3569 case 1:
3570 tile_info = &tile_data->tile1_;
3571 break;
3572 case 2:
3573 tile_info = &tile_data->tile2_;
3574 break;
3575 case 3:
3576 tile_info = &tile_data->tile3_;
3577 break;
3578 }
3579
3580 if (tile_info) {
3581 // Editable inputs for TileInfo components
3582 ImGui::PushID(q);
3583
3584 int tile_id_int = static_cast<int>(tile_info->id_);
3585 if (ImGui::InputInt("Tile8 ID", &tile_id_int, 1, 10)) {
3586 tile_info->id_ =
3587 static_cast<uint16_t>(std::max(0, std::min(tile_id_int, 1023)));
3588 }
3589
3590 int palette_int = static_cast<int>(tile_info->palette_);
3591 if (ImGui::SliderInt("Tile Palette", &palette_int, 0, 7)) {
3592 tile_info->palette_ = static_cast<uint8_t>(palette_int);
3593 }
3594
3595 ImGui::Checkbox("X Flip", &tile_info->horizontal_mirror_);
3596 ImGui::SameLine();
3597 ImGui::Checkbox("Y Flip", &tile_info->vertical_mirror_);
3598 ImGui::SameLine();
3599 ImGui::Checkbox("Priority", &tile_info->over_);
3600
3601 if (ImGui::Button("Stage Quadrant Edit")) {
3602 auto stage_result = stage_current_tile();
3603 if (!stage_result.ok()) {
3604 ImGui::Text("Stage Error: %s", stage_result.message().data());
3605 }
3606 }
3607
3608 ImGui::PopID();
3609 }
3610
3611 if (q < 3)
3612 ImGui::Separator();
3613 }
3614
3615 ImGui::Separator();
3616 if (ImGui::Button("Stage All Edits")) {
3617 auto stage_result = stage_current_tile();
3618 if (!stage_result.ok()) {
3619 ImGui::Text("Stage Error: %s", stage_result.message().data());
3620 }
3621 }
3622 ImGui::SameLine();
3623 if (ImGui::Button("Write Pending to ROM")) {
3624 auto write_result = CommitAllChanges();
3625 if (!write_result.ok()) {
3626 ImGui::Text("Write Error: %s", write_result.message().data());
3627 }
3628 }
3629 ImGui::SameLine();
3630 if (ImGui::Button("Refresh Display")) {
3631 auto refresh_result = SetCurrentTile(current_tile16_);
3632 if (!refresh_result.ok()) {
3633 ImGui::Text("Refresh Error: %s", refresh_result.message().data());
3634 }
3635 }
3636
3637 } else {
3638 ImGui::Text("Tile16 data not accessible");
3639 ImGui::Text("Current tile16: %d", current_tile16_);
3640 if (rom_) {
3641 ImGui::Text("Valid range: 0-4095 (4096 total tiles)");
3642 }
3643 }
3644
3645 ImGui::Separator();
3646 if (ImGui::Button("Close")) {
3647 ImGui::CloseCurrentPopup();
3648 }
3649
3650 ImGui::EndPopup();
3651 }
3652}
3653
3655 // Skip if live preview is disabled
3656 if (!live_preview_enabled_) {
3657 return absl::OkStatus();
3658 }
3659
3660 // Check if preview needs updating
3661 if (!preview_dirty_) {
3662 return absl::OkStatus();
3663 }
3664
3665 // Ensure we have valid tile data
3667 preview_dirty_ = false;
3668 return absl::OkStatus();
3669 }
3670
3671 // Update the preview bitmap from current tile16
3672 if (!preview_tile16_.is_active()) {
3674 } else {
3675 // Recreate with updated data
3677 }
3678
3679 // Apply the current palette
3680 const gfx::SnesPalette* display_palette = ResolveDisplayPalette();
3681 if (display_palette && !display_palette->empty()) {
3682 const bool use_sub_palette_view =
3684 if (use_sub_palette_view) {
3685 const int sheet_index = GetSheetIndexForTile8(current_tile8_);
3686 const int palette_slot =
3687 GetActualPaletteSlot(static_cast<int>(current_palette_), sheet_index);
3688 if (palette_slot >= 0 &&
3689 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
3691 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
3692 } else {
3693 preview_tile16_.SetPaletteWithTransparent(*display_palette, 1, 15);
3694 }
3695 } else {
3696 preview_tile16_.SetPalette(*display_palette);
3697 }
3698 }
3699
3700 // Queue texture update
3703
3704 // Clear the dirty flag
3705 preview_dirty_ = false;
3706
3707 return absl::OkStatus();
3708}
3709
3711 if (!ImGui::IsAnyItemActive()) {
3712 const ImGuiIO& io = ImGui::GetIO();
3713#if defined(__APPLE__)
3714 const bool platform_primary_held = io.KeyCtrl || io.KeySuper;
3715#else
3716 const bool platform_primary_held = io.KeyCtrl;
3717#endif
3718 const bool ctrl_held = platform_primary_held ||
3719 ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
3720 ImGui::IsKeyDown(ImGuiKey_RightCtrl);
3721
3722 // Editing shortcuts (only fire without Ctrl to avoid conflicts)
3723 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
3724 status_ = ClearTile16();
3725 }
3726 if (ImGui::IsKeyPressed(ImGuiKey_H) && !ctrl_held) {
3728 }
3729 if (ImGui::IsKeyPressed(ImGuiKey_V) && !ctrl_held) {
3731 }
3732 if (ImGui::IsKeyPressed(ImGuiKey_R) && !ctrl_held) {
3734 }
3735 if (ImGui::IsKeyPressed(ImGuiKey_F) && !ctrl_held) {
3736 if (current_tile8_ >= 0 &&
3737 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
3739 }
3740 }
3741
3742 if (!ctrl_held) {
3743 if (ImGui::IsKeyPressed(ImGuiKey_P)) {
3745 }
3746 if (ImGui::IsKeyPressed(ImGuiKey_I)) {
3748 }
3749 if (ImGui::IsKeyPressed(ImGuiKey_U)) {
3751 }
3752 }
3753
3754 // Palette shortcuts
3755 if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
3756 status_ = CyclePalette(false);
3757 }
3758 if (ImGui::IsKeyPressed(ImGuiKey_E)) {
3759 status_ = CyclePalette(true);
3760 }
3761
3762 // Numeric shortcuts:
3763 // - 1..4 focus tile16 quadrants
3764 // - Ctrl+1..8 switch brush palette rows
3765 for (int i = 0; i < 8; ++i) {
3766 if (!ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_1 + i))) {
3767 continue;
3768 }
3769 const Tile16NumericShortcutResult shortcut =
3770 ResolveTile16NumericShortcut(ctrl_held, i);
3771 if (shortcut.quadrant_focus.has_value()) {
3772 active_quadrant_ = *shortcut.quadrant_focus;
3773 }
3774 if (shortcut.palette_id.has_value()) {
3775 current_palette_ = *shortcut.palette_id;
3777 }
3778 }
3779
3780 // Ctrl-modified shortcuts
3781 if (ctrl_held) {
3782 if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
3783 status_ = Undo();
3784 }
3785 if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
3786 status_ = Redo();
3787 }
3788 if (ImGui::IsKeyPressed(ImGuiKey_C)) {
3790 }
3791 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
3793 }
3794 if (ImGui::IsKeyPressed(ImGuiKey_S)) {
3795 if (ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
3796 ImGui::IsKeyDown(ImGuiKey_RightShift)) {
3798 } else {
3800 }
3801 }
3802 }
3803 }
3804}
3805
3819
3820} // namespace editor
3821} // namespace yaze
absl::StatusOr< gfx::Tile16 > ReadTile16(uint32_t tile16_id, uint32_t tile16_ptr)
Definition rom.cc:444
absl::Status WriteTile16(int tile16_id, uint32_t tile16_ptr, const gfx::Tile16 &tile)
Definition rom.cc:463
void set_dirty(bool dirty)
Definition rom.h:134
absl::Status DrawTile8SourcePanel()
absl::Status SaveTile16ToScratchSpace(int slot)
const gfx::SnesPalette * ResolveDisplayPalette() const
std::map< int, gfx::Tile16 > pending_tile16_changes_
absl::Status LoadLayoutFromScratch(int slot)
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 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)
int GetPaletteSlotForSheet(int sheet_index) const
Get base palette slot for a graphics sheet.
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 + sheet.
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 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()
std::optional< Tile16Snapshot > pending_undo_before_
void DrawStagedStateBar(bool has_pending, bool current_tile_pending, int pending_count)
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)
void DrawEditorHeaderToggles(bool *show_debug_info, bool *show_advanced_controls)
absl::Status DrawToCurrentTile16(ImVec2 pos, const gfx::Bitmap *source_tile=nullptr)
absl::Status HandleTile8SourceSelection(bool right_clicked)
absl::Status Initialize(gfx::Bitmap &tile16_blockset_bmp, gfx::Bitmap &current_gfx_bmp, std::array< uint8_t, 0x200 > &all_tiles_types)
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.
std::function< absl::Status()> on_changes_committed_
absl::Status BuildTile16BitmapFromData(const gfx::Tile16 &tile_data, gfx::Bitmap *output_bitmap) const
void DrawEditorHeader(bool show_debug_info)
int GetPaletteBaseForSheet(int sheet_index) const
Get palette base row for a graphics sheet.
int GetSheetIndexForTile8(int tile8_id) const
Determine which graphics sheet contains a tile8.
absl::Status DrawBottomActionRail(bool has_pending, bool current_tile_pending, int pending_count)
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:377
const SnesPalette & palette() const
Definition bitmap.h:368
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:380
const std::vector< uint8_t > & vector() const
Definition bitmap.h:381
auto size() const
Definition bitmap.h:376
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:384
void set_modified(bool modified)
Definition bitmap.h:388
int height() const
Definition bitmap.h:374
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:373
int depth() const
Definition bitmap.h:375
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:378
SDL_Surface * surface() const
Definition bitmap.h:379
bool modified() const
Definition bitmap.h:383
RAII timer for automatic timing management.
SNES Color container.
Definition snes_color.h:110
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:1157
void ShowScalingControls()
Definition canvas.cc:1934
void ShowAdvancedCanvasProperties()
Definition canvas.cc:1810
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1093
bool DrawTilePainter(const Bitmap &bitmap, int size, float scale=1.0f)
Definition canvas.cc:934
auto custom_labels_enabled()
Definition canvas.h:492
void SetCanvasSize(ImVec2 canvas_size)
Definition canvas.h:466
auto mutable_labels(int i)
Definition canvas.h:536
void InitializePaletteEditor(Rom *rom)
Definition canvas.cc:326
void set_draggable(bool draggable)
Definition canvas.h:454
float GetGlobalScale() const
Definition canvas.h:470
auto zero_point() const
Definition canvas.h:443
bool IsMouseHovering() const
Definition canvas.h:433
void ShowPaletteEditor()
Definition canvas.cc:342
void InitializeDefaults()
Definition canvas.cc:164
void SetAutoResize(bool auto_resize)
Definition canvas.h:370
bool ApplyROMPalette(int group_index, int palette_index)
Definition canvas.cc:364
void ShowColorAnalysis()
Definition canvas.cc:354
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)
#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:22
const char * EditModeLabel(Tile16EditMode mode)
gfx::TileInfo & TileInfoForQuadrant(gfx::Tile16 *tile, int quadrant)
constexpr int kTile16PixelCount
constexpr int kTile8PixelCount
constexpr float kTile8DisplayScale
bool ComputeTile8UsageHighlight(bool source_hovered, bool right_mouse_down)
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)
Tile16ActionControlState ComputeTile16ActionControlState(bool has_pending, bool current_tile_pending, bool can_undo)
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)
Definition canvas.cc:1591
void BeginPadding(int i)
Definition style.cc:274
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
Definition canvas.cc:1568
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:650
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:278
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:290
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:239
gfx::Tile16 VerticalFlipTile16(gfx::Tile16 tile)
gfx::Tile16 RotateTile16Clockwise(gfx::Tile16 tile)
constexpr uint32_t kTile16Ptr
Definition game_data.h:58
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
Definition canvas.h:70
gfx::PaletteGroupMap palette_groups
Definition game_data.h:91