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"
14#include "app/gui/core/input.h"
15#include "app/gui/core/style.h"
18#include "imgui/imgui.h"
19#include "rom/rom.h"
20#include "util/hex.h"
21#include "util/log.h"
22#include "util/macro.h"
23#include "zelda3/game_data.h"
25
26namespace yaze {
27namespace editor {
28
29using namespace ImGui;
30
31// Display scales used for the tile8 source/preview rendering.
32constexpr float kTile8DisplayScale = 4.0f;
33
34namespace {
35
37
39 switch (quadrant) {
40 case 0:
41 return tile->tile0_;
42 case 1:
43 return tile->tile1_;
44 case 2:
45 return tile->tile2_;
46 default:
47 return tile->tile3_;
48 }
49}
50
51const gfx::TileInfo& TileInfoForQuadrant(const gfx::Tile16& tile, int quadrant) {
52 switch (quadrant) {
53 case 0:
54 return tile.tile0_;
55 case 1:
56 return tile.tile1_;
57 case 2:
58 return tile.tile2_;
59 default:
60 return tile.tile3_;
61 }
62}
63
65 if (!tile) {
66 return;
67 }
68 tile->tiles_info[0] = tile->tile0_;
69 tile->tiles_info[1] = tile->tile1_;
70 tile->tiles_info[2] = tile->tile2_;
71 tile->tiles_info[3] = tile->tile3_;
72}
73
78
83
84int ComputeTile16Count(const gfx::Tilemap* tile16_blockset) {
85 if (!tile16_blockset || !tile16_blockset->atlas.is_active()) {
86 return kTile16Count;
87 }
88
89 const int tiles_per_row = std::max(1, tile16_blockset->atlas.width() / kTile16Size);
90 const int rows = std::max(1, tile16_blockset->atlas.height() / kTile16Size);
91 const int computed = tiles_per_row * rows;
92 return std::clamp(computed, 1, kTile16Count);
93}
94
95} // namespace
96
98 const gfx::Bitmap& tile16_blockset_bmp, const gfx::Bitmap& current_gfx_bmp,
99 std::array<uint8_t, 0x200>& all_tiles_types) {
100 all_tiles_types_ = all_tiles_types;
101
102 // Copy the graphics bitmap (palette will be set later by overworld editor)
103 current_gfx_bmp_.Create(current_gfx_bmp.width(), current_gfx_bmp.height(),
104 current_gfx_bmp.depth(), current_gfx_bmp.vector());
105 current_gfx_bmp_.SetPalette(current_gfx_bmp.palette()); // Temporary palette
106 // Queue texture for later rendering.
109
110 // Copy the tile16 blockset bitmap
112 tile16_blockset_bmp.width(), tile16_blockset_bmp.height(),
113 tile16_blockset_bmp.depth(), tile16_blockset_bmp.vector());
114 tile16_blockset_bmp_.SetPalette(tile16_blockset_bmp.palette());
115 // Queue texture for later rendering.
118
119 // Note: LoadTile8() will be called after palette is set by overworld editor
120 // This ensures proper palette coordination from the start
121
122 // Initialize current tile16 bitmap - this will be set by SetCurrentTile
124 std::vector<uint8_t>(kTile16PixelCount, 0));
125 current_tile16_bmp_.SetPalette(tile16_blockset_bmp.palette());
126 // Queue texture for later rendering.
129
130 // Initialize enhanced canvas features with proper sizing
133
134 // Attach blockset canvas to the selector widget
136 blockset_selector_.SetTileCount(kTile16Count);
137
138 // Configure canvases with proper initialization
141
142 // Initialize enhanced palette editors if ROM is available
143 if (rom_) {
146 }
147
148 // Initialize the current tile16 properly from the blockset
149 if (tile16_blockset_) {
150 RETURN_IF_ERROR(SetCurrentTile(0)); // Start with tile 0
151 }
152
154
155 // Setup collision type labels for tile8 canvas
156 ImVector<std::string> tile16_names;
157 for (int i = 0; i < 0x200; ++i) {
158 std::string str = util::HexByte(all_tiles_types_[i]);
159 tile16_names.push_back(str);
160 }
161 *tile8_source_canvas_.mutable_labels(0) = tile16_names;
163
164 // Setup tile info table
166 [&]() { Text("Tile16: %02X", current_tile16_); });
168 [&]() { Text("Tile8: %02X", current_tile8_); });
169 gui::AddTableColumn(tile_edit_table_, "##tile16Flip", [&]() {
170 Checkbox("X Flip", &x_flip);
171 Checkbox("Y Flip", &y_flip);
172 Checkbox("Priority", &priority_tile);
173 });
174
175 return absl::OkStatus();
176}
177
178absl::Status Tile16Editor::Update() {
180 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
181 }
182
183 if (BeginMenuBar()) {
184 if (BeginMenu("View")) {
185 Checkbox("Show Collision Types",
187 EndMenu();
188 }
189
190 if (BeginMenu("Edit")) {
191 if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
193 }
194 if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
196 }
197 EndMenu();
198 }
199
200 if (BeginMenu("File")) {
201 if (MenuItem("Save Changes to ROM", "Ctrl+S")) {
203 }
204 if (MenuItem("Commit to Blockset", "Ctrl+Shift+S")) {
206 }
207 Separator();
208 bool live_preview = live_preview_enabled_;
209 if (MenuItem("Live Preview", nullptr, &live_preview)) {
210 EnableLivePreview(live_preview);
211 }
212 EndMenu();
213 }
214
215 if (BeginMenu("Scratch Space")) {
216 for (int i = 0; i < 4; i++) {
217 std::string slot_name = "Slot " + std::to_string(i + 1);
218 if (scratch_space_[i].has_data) {
219 if (MenuItem((slot_name + " (Load)").c_str())) {
221 }
222 if (MenuItem((slot_name + " (Save)").c_str())) {
224 }
225 if (MenuItem((slot_name + " (Clear)").c_str())) {
227 }
228 } else {
229 if (MenuItem((slot_name + " (Save)").c_str())) {
231 }
232 }
233 if (i < 3)
234 Separator();
235 }
236 EndMenu();
237 }
238
239 EndMenuBar();
240 }
241
242 // About popup
243 if (BeginPopupModal("About Tile16 Editor", NULL,
244 ImGuiWindowFlags_AlwaysAutoResize)) {
245 Text("Tile16 Editor for Link to the Past");
246 Text("This editor allows you to edit 16x16 tiles used in the game.");
247 Text("Features:");
248 BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
249 BulletText("Copy and paste Tile16 graphics");
250 BulletText("Save and load Tile16 graphics to/from scratch space");
251 BulletText("Preview Tile16 graphics at a larger size");
252 Separator();
253 if (Button("Close")) {
254 CloseCurrentPopup();
255 }
256 EndPopup();
257 }
258
259 // Unsaved changes confirmation dialog
261 OpenPopup("Unsaved Changes##Tile16Editor");
262 }
263 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
264 ImGuiWindowFlags_AlwaysAutoResize)) {
265 Text("Tile %d has unsaved changes.", current_tile16_);
266 Text("What would you like to do?");
267 Separator();
268
269 // Save & Continue button (green)
270 if (gui::SuccessButton("Save & Continue", ImVec2(120, 0))) {
271 // Commit just the current tile change
272 if (auto* tile_data = GetCurrentTile16Data()) {
273 auto status =
275 if (status.ok()) {
276 // Remove from pending
279 // Refresh blockset
281 // Now switch to the target tile
284 }
285 }
286 }
289 CloseCurrentPopup();
290 }
291
292 SameLine();
293
294 // Discard & Continue button (red)
295 if (gui::DangerButton("Discard & Continue", ImVec2(130, 0))) {
296 // Remove pending changes for current tile
299 // Switch to target tile
302 }
305 CloseCurrentPopup();
306 }
307
308 SameLine();
309
310 // Cancel button
311 if (Button("Cancel", ImVec2(80, 0))) {
314 CloseCurrentPopup();
315 }
316
317 EndPopup();
318 }
319
320 // Handle keyboard shortcuts (shared implementation)
322
324
325 // Draw palette settings popup if enabled
327
328 // Update live preview if dirty
330
331 return absl::OkStatus();
332}
333
335 // REFACTORED: Single unified table layout in UpdateTile16Edit
337}
338
341 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
342 }
343
344 // Menu button for context menu
345 if (Button(ICON_MD_MENU " Menu")) {
346 OpenPopup("##Tile16EditorContextMenu");
347 }
348 SameLine();
349 TextDisabled("Right-click for more options");
350
351 // Context menu
353
354 // About popup
355 if (BeginPopupModal("About Tile16 Editor", NULL,
356 ImGuiWindowFlags_AlwaysAutoResize)) {
357 Text("Tile16 Editor for Link to the Past");
358 Text("This editor allows you to edit 16x16 tiles used in the game.");
359 Text("Features:");
360 BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
361 BulletText("Copy and paste Tile16 graphics");
362 BulletText("Save and load Tile16 graphics to/from scratch space");
363 BulletText("Preview Tile16 graphics at a larger size");
364 Separator();
365 if (Button("Close")) {
366 CloseCurrentPopup();
367 }
368 EndPopup();
369 }
370
371 // Unsaved changes confirmation dialog
373 OpenPopup("Unsaved Changes##Tile16Editor");
374 }
375 if (BeginPopupModal("Unsaved Changes##Tile16Editor", NULL,
376 ImGuiWindowFlags_AlwaysAutoResize)) {
377 Text("Tile %d has unsaved changes.", current_tile16_);
378 Text("What would you like to do?");
379 Separator();
380
381 if (gui::SuccessButton("Save & Continue", ImVec2(120, 0))) {
382 if (auto* tile_data = GetCurrentTile16Data()) {
383 auto status =
385 if (status.ok()) {
391 }
392 }
393 }
396 CloseCurrentPopup();
397 }
398
399 SameLine();
400
401 if (gui::DangerButton("Discard & Continue", ImVec2(130, 0))) {
406 }
409 CloseCurrentPopup();
410 }
411
412 SameLine();
413
414 if (Button("Cancel", ImVec2(80, 0))) {
417 CloseCurrentPopup();
418 }
419
420 EndPopup();
421 }
422
423 // Handle keyboard shortcuts (shared implementation)
425
429
430 return absl::OkStatus();
431}
432
434 if (BeginPopup("##Tile16EditorContextMenu")) {
435 if (BeginMenu("View")) {
436 Checkbox("Show Collision Types",
438 EndMenu();
439 }
440
441 if (BeginMenu("Edit")) {
442 if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
444 }
445 if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
447 }
448 Separator();
449 if (MenuItem("Flip Horizontal", "H")) {
451 }
452 if (MenuItem("Flip Vertical", "V")) {
454 }
455 if (MenuItem("Rotate", "R")) {
457 }
458 if (MenuItem("Clear", "Delete")) {
460 }
461 EndMenu();
462 }
463
464 if (BeginMenu("File")) {
465 if (MenuItem("Save Changes to ROM", "Ctrl+S")) {
467 }
468 if (MenuItem("Commit to Blockset", "Ctrl+Shift+S")) {
470 }
471 Separator();
472 bool live_preview = live_preview_enabled_;
473 if (MenuItem("Live Preview", nullptr, &live_preview)) {
474 EnableLivePreview(live_preview);
475 }
476 EndMenu();
477 }
478
479 if (BeginMenu("Scratch Space")) {
480 for (int i = 0; i < 4; i++) {
481 std::string slot_name = "Slot " + std::to_string(i + 1);
482 if (scratch_space_[i].has_data) {
483 if (MenuItem((slot_name + " (Load)").c_str())) {
485 }
486 if (MenuItem((slot_name + " (Save)").c_str())) {
488 }
489 if (MenuItem((slot_name + " (Clear)").c_str())) {
491 }
492 } else {
493 if (MenuItem((slot_name + " (Save)").c_str())) {
495 }
496 }
497 if (i < 3)
498 Separator();
499 }
500 EndMenu();
501 }
502
503 EndPopup();
504 }
505}
506
509 gui::BeginChildWithScrollbar("##Tile16EditorBlocksetScrollRegion");
510
511 // Tile ID search/jump bar
514 }
515
516 // Configure canvas frame options for blockset view
517 gui::CanvasFrameOptions frame_opts;
518 frame_opts.draw_grid = true;
519 frame_opts.grid_step = 32.0f; // Tile16 grid
520 frame_opts.draw_context_menu = true;
521 frame_opts.draw_overlay = true;
522 frame_opts.render_popups = true;
523 frame_opts.use_child_window = false;
524
525 auto canvas_rt = gui::BeginCanvas(blockset_canvas_, frame_opts);
527
528 // Ensure selector is synced with current selection
531 }
532
533 // Render the selector widget (handles bitmap, grid, highlights, interaction)
535
536 if (result.selection_changed) {
537 // Use RequestTileSwitch to handle pending changes confirmation
538 RequestTileSwitch(result.selected_tile);
539 util::logf("Selected Tile16 from blockset: %d", result.selected_tile);
540 }
541
542 gui::EndCanvas(blockset_canvas_, canvas_rt, frame_opts);
543 EndChild();
544
545 return absl::OkStatus();
546}
547
548// ROM data access methods
550 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
551 return nullptr;
552 }
553 return &current_tile16_data_;
554}
555
557 auto* tile_data = GetCurrentTile16Data();
558 if (!tile_data) {
559 return absl::FailedPreconditionError("Cannot access current tile16 data");
560 }
561
562 // Write the modified tile16 data back to ROM
565
566 util::logf("ROM Tile16 data written for tile %d", current_tile16_);
567 return absl::OkStatus();
568}
569
571 if (!tile16_blockset_) {
572 return absl::FailedPreconditionError("Tile16 blockset not available");
573 }
574
575 // CRITICAL FIX: Force regeneration without using problematic tile cache
576 // Directly mark atlas as modified to trigger regeneration from ROM data
577
578 // Mark atlas as modified to trigger regeneration
580
581 // Queue texture update via Arena's deferred system
584
585 util::logf("Tile16 blockset refreshed and regenerated");
586 return absl::OkStatus();
587}
588
590 gfx::ScopedTimer timer("tile16_blockset_update");
591
592 if (!tile16_blockset_) {
593 return absl::FailedPreconditionError("Tile16 blockset not initialized");
594 }
595
596 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
597 return absl::OutOfRangeError("Current tile16 ID out of range");
598 }
599
600 // Use optimized batch operations for better performance
602 // Calculate the position of this tile in the blockset bitmap
603 constexpr int kTilesPerRow =
604 8; // Standard SNES tile16 layout is 8 tiles per row
605 int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
606 int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
607
608 // Use dirty region tracking for efficient updates (region calculated but
609 // not used in current implementation)
610
611 // Copy pixel data from current tile to blockset bitmap using batch
612 // operations
613 for (int tile_y_offset = 0; tile_y_offset < kTile16Size; ++tile_y_offset) {
614 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
615 ++tile_x_offset) {
616 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
617 int dst_index =
618 (tile_y + tile_y_offset) * tile16_blockset_bmp_.width() +
619 (tile_x + tile_x_offset);
620
621 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
622 dst_index < static_cast<int>(tile16_blockset_bmp_.size())) {
623 uint8_t pixel_value = current_tile16_bmp_.data()[src_index];
624 tile16_blockset_bmp_.WriteToPixel(dst_index, pixel_value);
625 }
626 }
627 }
628
629 // Mark the blockset bitmap as modified and queue texture update
633
634 // Also update the tile16 blockset atlas if available
636 // Update the atlas with the new tile data
637 for (int tile_y_offset = 0; tile_y_offset < kTile16Size;
638 ++tile_y_offset) {
639 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
640 ++tile_x_offset) {
641 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
642 int dst_index =
643 (tile_y + tile_y_offset) * tile16_blockset_->atlas.width() +
644 (tile_x + tile_x_offset);
645
646 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
647 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
649 dst_index, current_tile16_bmp_.data()[src_index]);
650 }
651 }
652 }
653
657 }
658 }
659
660 return absl::OkStatus();
661}
662
664 // Get the current tile16 data from ROM
665 auto* tile_data = GetCurrentTile16Data();
666 if (!tile_data) {
667 return absl::FailedPreconditionError("Cannot access current tile16 data");
668 }
669
670 // Create a new 16x16 bitmap for the tile16
671 std::vector<uint8_t> tile16_pixels(kTile16PixelCount, 0);
672
673 // Process each quadrant (2x2 grid of 8x8 tiles)
674 for (int quadrant = 0; quadrant < 4; ++quadrant) {
675 gfx::TileInfo* tile_info = nullptr;
676 int quadrant_x = quadrant % 2;
677 int quadrant_y = quadrant / 2;
678
679 // Get the tile info for this quadrant
680 switch (quadrant) {
681 case 0:
682 tile_info = &tile_data->tile0_;
683 break;
684 case 1:
685 tile_info = &tile_data->tile1_;
686 break;
687 case 2:
688 tile_info = &tile_data->tile2_;
689 break;
690 case 3:
691 tile_info = &tile_data->tile3_;
692 break;
693 }
694
695 if (!tile_info)
696 continue;
697
698 // Get the tile8 ID and properties
699 int tile8_id = tile_info->id_;
700 bool x_flip = tile_info->horizontal_mirror_;
701 bool y_flip = tile_info->vertical_mirror_;
702 // Palette information stored in tile_info but applied via separate palette
703 // system
704
705 // Get the source tile8 bitmap
706 if (tile8_id >= 0 &&
707 tile8_id < static_cast<int>(current_gfx_individual_.size()) &&
708 current_gfx_individual_[tile8_id].is_active()) {
709 const auto& source_tile8 = current_gfx_individual_[tile8_id];
710
711 // Copy the 8x8 tile into the appropriate quadrant of the 16x16 tile
712 for (int ty = 0; ty < kTile8Size; ++ty) {
713 for (int tx = 0; tx < kTile8Size; ++tx) {
714 // Apply flip transformations
715 int src_x = x_flip ? (kTile8Size - 1 - tx) : tx;
716 int src_y = y_flip ? (kTile8Size - 1 - ty) : ty;
717 int src_index = src_y * kTile8Size + src_x;
718
719 // Calculate destination in tile16
720 int dst_x = (quadrant_x * kTile8Size) + tx;
721 int dst_y = (quadrant_y * kTile8Size) + ty;
722 int dst_index = dst_y * kTile16Size + dst_x;
723
724 // Copy pixel with bounds checking
725 if (src_index >= 0 &&
726 src_index < static_cast<int>(source_tile8.size()) &&
727 dst_index >= 0 && dst_index < kTile16PixelCount) {
728 uint8_t pixel = source_tile8.data()[src_index];
729 // Apply palette offset if needed
730 tile16_pixels[dst_index] = pixel;
731 }
732 }
733 }
734 }
735 }
736
737 // Update the current tile16 bitmap with the regenerated data
739
740 // Set the appropriate palette using the same system as overworld
742
743 // Queue texture creation via Arena's deferred system
746
747 util::logf("Regenerated Tile16 bitmap for tile %d from ROM data",
749 return absl::OkStatus();
750}
751
752absl::Status Tile16Editor::DrawToCurrentTile16(ImVec2 pos,
753 const gfx::Bitmap* source_tile) {
754 constexpr int kTile8Size = 8;
755 constexpr int kTile16Size = 16;
756
757 // Save undo state before making changes
758 auto now = std::chrono::steady_clock::now();
759 auto time_since_last_edit =
760 std::chrono::duration_cast<std::chrono::milliseconds>(now -
762 .count();
763
764 if (time_since_last_edit > 100) { // 100ms threshold
766 last_edit_time_ = now;
767 }
768
769 // Validate inputs
770 if (current_tile8_ < 0 ||
771 current_tile8_ >= static_cast<int>(current_gfx_individual_.size())) {
772 return absl::OutOfRangeError(
773 absl::StrFormat("Invalid tile8 index: %d", current_tile8_));
774 }
775
776 if (!current_gfx_individual_[current_tile8_].is_active()) {
777 return absl::FailedPreconditionError("Source tile8 bitmap not active");
778 }
779
781 return absl::FailedPreconditionError("Target tile16 bitmap not active");
782 }
783
784 // Determine which quadrant was clicked - handle the 8x scale factor properly
785 int quadrant_x = (pos.x >= kTile8Size) ? 1 : 0;
786 int quadrant_y = (pos.y >= kTile8Size) ? 1 : 0;
787
788 int start_x = quadrant_x * kTile8Size;
789 int start_y = quadrant_y * kTile8Size;
790
791 // Get source tile8 data - use provided tile if available, otherwise use
792 // current tile8
793 const gfx::Bitmap* tile_to_use =
794 source_tile ? source_tile : &current_gfx_individual_[current_tile8_];
795 if (tile_to_use->size() < 64) {
796 return absl::FailedPreconditionError("Source tile data too small");
797 }
798
799 // Copy tile8 into tile16 quadrant with proper transformations
800 for (int tile_y = 0; tile_y < kTile8Size; ++tile_y) {
801 for (int tile_x = 0; tile_x < kTile8Size; ++tile_x) {
802 // Apply flip transformations to source coordinates only if using original
803 // tile If a pre-flipped tile is provided, use direct coordinates
804 int src_x;
805 int src_y;
806 if (source_tile) {
807 // Pre-flipped tile provided, use direct coordinates
808 src_x = tile_x;
809 src_y = tile_y;
810 } else {
811 // Original tile, apply flip transformations
812 src_x = x_flip ? (kTile8Size - 1 - tile_x) : tile_x;
813 src_y = y_flip ? (kTile8Size - 1 - tile_y) : tile_y;
814 }
815 int src_index = src_y * kTile8Size + src_x;
816
817 // Calculate destination in tile16
818 int dst_x = start_x + tile_x;
819 int dst_y = start_y + tile_y;
820 int dst_index = dst_y * kTile16Size + dst_x;
821
822 // Bounds check and copy pixel
823 if (src_index >= 0 && src_index < static_cast<int>(tile_to_use->size()) &&
824 dst_index >= 0 &&
825 dst_index < static_cast<int>(current_tile16_bmp_.size())) {
826 uint8_t pixel_value = tile_to_use->data()[src_index];
827
828 // Keep original pixel values - palette selection is handled by TileInfo
829 // metadata not by modifying pixel data directly
830 current_tile16_bmp_.WriteToPixel(dst_index, pixel_value);
831 }
832 }
833 }
834
835 // Mark the bitmap as modified and queue texture update
839
840 // Update ROM data when painting to tile16
841 auto* tile_data = GetCurrentTile16Data();
842 if (tile_data) {
843 // Update the quadrant's TileInfo based on current settings
844 int quadrant_index = quadrant_x + (quadrant_y * 2);
845 if (quadrant_index >= 0 && quadrant_index < 4) {
846 // Create new TileInfo with current settings
847 gfx::TileInfo new_tile_info(static_cast<uint16_t>(current_tile8_),
850
851 // Get pointer to the correct quadrant TileInfo
852 gfx::TileInfo* quadrant_tile = nullptr;
853 switch (quadrant_index) {
854 case 0:
855 quadrant_tile = &tile_data->tile0_;
856 break;
857 case 1:
858 quadrant_tile = &tile_data->tile1_;
859 break;
860 case 2:
861 quadrant_tile = &tile_data->tile2_;
862 break;
863 case 3:
864 quadrant_tile = &tile_data->tile3_;
865 break;
866 }
867
868 if (quadrant_tile && !(*quadrant_tile == new_tile_info)) {
869 *quadrant_tile = new_tile_info;
870 SyncTilesInfoArray(tile_data);
871
873 "Updated ROM Tile16 %d, quadrant %d: Tile8=%d, Pal=%d, XFlip=%d, "
874 "YFlip=%d, Priority=%d",
877 }
878 }
879 }
880
881 // CRITICAL FIX: Don't write to ROM immediately - only update local data
882 // ROM will be updated when user explicitly clicks "Save to ROM"
883
884 // Update the blockset bitmap displayed in the editor (local preview only)
886
887 // Update live preview if enabled (but don't save to ROM)
890 }
891
892 // Track this tile as having pending changes
894
896 "Local tile16 changes made (not saved to ROM yet). Use 'Apply Changes' "
897 "to commit.");
898
899 return absl::OkStatus();
900}
901
903 static bool show_advanced_controls = false;
904 static bool show_debug_info = false;
905
906 // Modern header with improved styling
907 gui::StyleVarGuard header_var_guard(
908 {{ImGuiStyleVar_FramePadding, ImVec2(8, 4)},
909 {ImGuiStyleVar_ItemSpacing, ImVec2(8, 4)}});
910
911 // Header section with better visual hierarchy
912 ImGui::BeginGroup();
913 ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tile16 Editor");
914 ImGui::SameLine();
915 ImGui::TextDisabled("ID: %02X", current_tile16_);
916 ImGui::SameLine();
917 ImGui::TextDisabled("|");
918 ImGui::SameLine();
919 ImGui::TextDisabled("Palette: %d", current_palette_);
920
921 // Show pending changes indicator
922 if (has_pending_changes()) {
923 ImGui::SameLine();
924 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "| %d pending",
926 }
927
928 // Show actual palette slot for debugging
929 if (show_debug_info) {
930 ImGui::SameLine();
931 int actual_slot = GetActualPaletteSlotForCurrentTile16();
932 ImGui::TextDisabled("(Slot: %d)", actual_slot);
933 }
934
935 ImGui::EndGroup();
936
937 // Apply/Discard buttons (only shown when there are pending changes)
938 if (has_pending_changes()) {
939 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 340);
940 if (gui::SuccessButton("Apply All", ImVec2(70, 0))) {
941 auto status = CommitAllChanges();
942 if (!status.ok()) {
943 util::logf("Failed to commit changes: %s", status.message().data());
944 }
945 }
946 if (ImGui::IsItemHovered()) {
947 ImGui::SetTooltip("Commit all %d pending changes to ROM",
949 }
950
951 ImGui::SameLine();
952 if (gui::DangerButton("Discard All", ImVec2(70, 0))) {
954 }
955 if (ImGui::IsItemHovered()) {
956 ImGui::SetTooltip("Discard all %d pending changes",
958 }
959
960 ImGui::SameLine();
961 }
962
963 // Modern button styling for controls
964 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 180);
965 if (ImGui::Button("Debug Info", ImVec2(80, 0))) {
966 show_debug_info = !show_debug_info;
967 }
968 ImGui::SameLine();
969 if (ImGui::Button("Advanced", ImVec2(80, 0))) {
970 show_advanced_controls = !show_advanced_controls;
971 }
972
973 ImGui::Separator();
974
975 // REFACTORED: Improved 3-column layout with better space utilization
976 if (ImGui::BeginTable("##Tile16EditLayout", 3,
977 ImGuiTableFlags_Resizable |
978 ImGuiTableFlags_BordersInnerV |
979 ImGuiTableFlags_SizingStretchProp)) {
980 ImGui::TableSetupColumn("Tile16 Blockset",
981 ImGuiTableColumnFlags_WidthStretch, 0.35f);
982 ImGui::TableSetupColumn("Tile8 Source", ImGuiTableColumnFlags_WidthStretch,
983 0.35f);
984 ImGui::TableSetupColumn("Editor & Controls",
985 ImGuiTableColumnFlags_WidthStretch, 0.30f);
986
987 ImGui::TableHeadersRow();
988 ImGui::TableNextRow();
989
990 // ========== COLUMN 1: Tile16 Blockset ==========
991 ImGui::TableNextColumn();
992 ImGui::BeginGroup();
993
994 // Navigation header with tile info
995 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile16 Blockset");
996 ImGui::SameLine();
997
998 // Show current tile and total tiles
999 int total_tiles = ComputeTile16Count(tile16_blockset_);
1000 ImGui::TextDisabled("(%d / %d)", current_tile16_, total_tiles);
1001
1002 // Navigation controls row
1003 {
1004 gui::StyleVarGuard nav_spacing_guard(ImGuiStyleVar_ItemSpacing,
1005 ImVec2(4, 4));
1006
1007 // Jump to Tile ID input - live navigation as user types
1008 ImGui::SetNextItemWidth(80);
1009 if (ImGui::InputInt("##JumpToTile", &jump_to_tile_id_, 0, 0)) {
1010 // Clamp to valid range
1011 jump_to_tile_id_ = std::clamp(jump_to_tile_id_, 0, total_tiles - 1);
1014 scroll_to_current_ = true;
1015 }
1016 }
1017 if (ImGui::IsItemHovered()) {
1018 ImGui::SetTooltip("Tile ID (0-%d) - navigates as you type",
1019 total_tiles - 1);
1020 }
1021
1022 ImGui::SameLine();
1023 ImGui::TextDisabled("|");
1024 ImGui::SameLine();
1025
1026 // Page navigation
1027 int total_pages = (total_tiles + kTilesPerPage - 1) / kTilesPerPage;
1029
1030 if (ImGui::Button("<<")) {
1032 scroll_to_current_ = true;
1033 }
1034 if (ImGui::IsItemHovered())
1035 ImGui::SetTooltip("First page");
1036
1037 ImGui::SameLine();
1038 if (ImGui::Button("<")) {
1039 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
1040 RequestTileSwitch(new_tile);
1041 scroll_to_current_ = true;
1042 }
1043 if (ImGui::IsItemHovered())
1044 ImGui::SetTooltip("Previous page (PageUp)");
1045
1046 ImGui::SameLine();
1047 ImGui::TextDisabled("Page %d/%d", current_page_ + 1, total_pages);
1048
1049 ImGui::SameLine();
1050 if (ImGui::Button(">")) {
1051 int new_tile = std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
1052 RequestTileSwitch(new_tile);
1053 scroll_to_current_ = true;
1054 }
1055 if (ImGui::IsItemHovered())
1056 ImGui::SetTooltip("Next page (PageDown)");
1057
1058 ImGui::SameLine();
1059 if (ImGui::Button(">>")) {
1060 RequestTileSwitch(total_tiles - 1);
1061 scroll_to_current_ = true;
1062 }
1063 if (ImGui::IsItemHovered())
1064 ImGui::SetTooltip("Last page");
1065
1066 // Display current tile info (sheet and palette)
1067 ImGui::SameLine();
1068 ImGui::TextDisabled("|");
1069 ImGui::SameLine();
1070 int sheet_idx = GetSheetIndexForTile8(current_tile8_);
1071 ImGui::Text("Sheet: %d | Palette: %d", sheet_idx, current_palette_);
1072
1073 // Handle keyboard shortcuts for page navigation
1074 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
1075 if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
1076 int new_tile = std::max(0, current_tile16_ - kTilesPerPage);
1077 RequestTileSwitch(new_tile);
1078 scroll_to_current_ = true;
1079 }
1080 if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
1081 int new_tile =
1082 std::min(total_tiles - 1, current_tile16_ + kTilesPerPage);
1083 RequestTileSwitch(new_tile);
1084 scroll_to_current_ = true;
1085 }
1086 if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
1088 scroll_to_current_ = true;
1089 }
1090 if (ImGui::IsKeyPressed(ImGuiKey_End)) {
1091 RequestTileSwitch(total_tiles - 1);
1092 scroll_to_current_ = true;
1093 }
1094
1095 // Arrow keys for single-tile navigation (when Ctrl not held)
1096 if (!ImGui::GetIO().KeyCtrl) {
1097 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
1098 if (current_tile16_ > 0) {
1100 scroll_to_current_ = true;
1101 }
1102 }
1103 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
1104 if (current_tile16_ < total_tiles - 1) {
1106 scroll_to_current_ = true;
1107 }
1108 }
1109 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
1112 scroll_to_current_ = true;
1113 }
1114 }
1115 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
1116 if (current_tile16_ + kTilesPerRow < total_tiles) {
1118 scroll_to_current_ = true;
1119 }
1120 }
1121 }
1122 }
1123
1124 } // nav_spacing_guard scope
1125
1126 // Blockset canvas with scrolling
1127 if (BeginChild("##BlocksetScrollable",
1128 ImVec2(0, ImGui::GetContentRegionAvail().y), true,
1129 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1130 // Handle scroll-to-current request
1131 if (scroll_to_current_) {
1132 int tile_row = current_tile16_ / kTilesPerRow;
1133 float tile_y = tile_row * 32.0f * blockset_canvas_.GetGlobalScale();
1134 ImGui::SetScrollY(tile_y);
1135 scroll_to_current_ = false;
1136 }
1137
1138 // Configure canvas frame options for blockset
1139 gui::CanvasFrameOptions blockset_frame_opts;
1140 blockset_frame_opts.draw_grid = true;
1141 blockset_frame_opts.grid_step = 32.0f;
1142 blockset_frame_opts.draw_context_menu = true;
1143 blockset_frame_opts.draw_overlay = true;
1144 blockset_frame_opts.render_popups = true;
1145 blockset_frame_opts.use_child_window = false;
1146
1147 auto blockset_rt =
1148 gui::BeginCanvas(blockset_canvas_, blockset_frame_opts);
1149
1150 // Handle tile selection from blockset
1151 bool tile_selected = false;
1153
1154 if (ImGui::IsItemClicked(ImGuiMouseButton_Left) &&
1156 tile_selected = true;
1157 }
1158
1159 if (tile_selected) {
1160 const ImGuiIO& io = ImGui::GetIO();
1161 ImVec2 canvas_pos = blockset_canvas_.zero_point();
1162 ImVec2 mouse_pos =
1163 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1164
1165 int grid_x = static_cast<int>(mouse_pos.x /
1167 int grid_y = static_cast<int>(mouse_pos.y /
1169 int selected_tile = grid_x + grid_y * 8;
1170
1171 if (selected_tile != current_tile16_ && selected_tile >= 0) {
1172 // Use RequestTileSwitch to handle pending changes confirmation
1173 RequestTileSwitch(selected_tile);
1174 util::logf("Selected Tile16 from blockset: %d", selected_tile);
1175 }
1176 }
1177
1179
1180 gui::EndCanvas(blockset_canvas_, blockset_rt, blockset_frame_opts);
1181 }
1182 EndChild();
1183 ImGui::EndGroup();
1184
1185 // ========== COLUMN 2: Tile8 Source ==========
1186 ImGui::TableNextColumn();
1187 ImGui::BeginGroup();
1188
1189 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile8 Source");
1190
1192
1193 // Scrollable tile8 source
1194 if (BeginChild("##Tile8SourceScrollable", ImVec2(0, 0), true,
1195 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
1196 // Configure canvas frame options for tile8 source
1197 gui::CanvasFrameOptions tile8_frame_opts;
1198 tile8_frame_opts.draw_grid = true;
1199 tile8_frame_opts.grid_step = 32.0f; // Tile8 grid (8px * 4 scale)
1200 tile8_frame_opts.draw_context_menu = true;
1201 tile8_frame_opts.draw_overlay = true;
1202 tile8_frame_opts.render_popups = true;
1203 tile8_frame_opts.use_child_window = false;
1204
1205 auto tile8_rt = gui::BeginCanvas(tile8_source_canvas_, tile8_frame_opts);
1206
1207 // Tile8 selection with improved feedback
1208 bool tile8_selected = false;
1210
1211 // Check for clicks properly
1212 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
1213 tile8_selected = true;
1214 }
1215
1216 if (tile8_selected) {
1217 const ImGuiIO& io = ImGui::GetIO();
1218 ImVec2 canvas_pos = tile8_source_canvas_.zero_point();
1219 ImVec2 mouse_pos =
1220 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1221
1222 // Account for dynamic zoom when calculating tile position
1223 int tile_x = static_cast<int>(
1224 mouse_pos.x / (8 * kTile8DisplayScale)); // 8 pixel tile * scale
1225 int tile_y = static_cast<int>(mouse_pos.y / (8 * kTile8DisplayScale));
1226
1227 // Calculate tiles per row based on bitmap width
1228 int tiles_per_row = current_gfx_bmp_.width() / 8;
1229 int new_tile8 = tile_x + (tile_y * tiles_per_row);
1230
1231 if (new_tile8 != current_tile8_ && new_tile8 >= 0 &&
1232 new_tile8 < static_cast<int>(current_gfx_individual_.size()) &&
1233 current_gfx_individual_[new_tile8].is_active()) {
1234 current_tile8_ = new_tile8;
1236 util::logf("Selected Tile8: %d", current_tile8_);
1237 }
1238 }
1239
1242
1243 gui::EndCanvas(tile8_source_canvas_, tile8_rt, tile8_frame_opts);
1244 }
1245 EndChild();
1246 ImGui::EndGroup();
1247
1248 // ========== COLUMN 3: Tile16 Editor + Controls ==========
1249 TableNextColumn();
1250 ImGui::BeginGroup();
1251
1252 // Fixed size container to prevent canvas expansion
1253 if (ImGui::BeginChild("##Tile16FixedCanvas", ImVec2(90, 90), true,
1254 ImGuiWindowFlags_NoScrollbar |
1255 ImGuiWindowFlags_NoScrollWithMouse)) {
1256 // Configure canvas frame options for tile16 editor
1257 gui::CanvasFrameOptions tile16_edit_frame_opts;
1258 tile16_edit_frame_opts.canvas_size = ImVec2(64, 64);
1259 tile16_edit_frame_opts.draw_grid = true;
1260 tile16_edit_frame_opts.grid_step = 8.0f; // 8x8 grid for tile8 placement
1261 tile16_edit_frame_opts.draw_context_menu = true;
1262 tile16_edit_frame_opts.draw_overlay = true;
1263 tile16_edit_frame_opts.render_popups = true;
1264 tile16_edit_frame_opts.use_child_window = false;
1265
1266 auto tile16_edit_rt =
1267 gui::BeginCanvas(tile16_edit_canvas_, tile16_edit_frame_opts);
1268
1269 // Draw current tile16 bitmap with dynamic zoom
1272 }
1273
1274 // Handle tile8 painting with improved hover preview
1275 if (current_tile8_ >= 0 &&
1276 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
1278 // Create a display tile that shows the current palette selection
1280 tile8_preview_bmp_.Create(8, 8, 8,
1281 std::vector<uint8_t>(kTile8PixelCount, 0));
1282 }
1283
1284 // Get the original pixel data (already has sheet offsets from
1285 // ProcessGraphicsBuffer)
1288
1289 // Apply the correct sheet-aware palette slice for the preview
1290 const gfx::SnesPalette* display_palette = nullptr;
1291 if (overworld_palette_.size() >= 256) {
1292 display_palette = &overworld_palette_;
1293 } else if (palette_.size() >= 256) {
1294 display_palette = &palette_;
1295 } else {
1296 display_palette = &current_gfx_individual_[current_tile8_].palette();
1297 }
1298
1299 if (display_palette && !display_palette->empty()) {
1301 // Calculate palette slot for the selected tile8
1302 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1303 int palette_slot =
1305
1306 // SNES palette offset fix: pixel value N maps to sub-palette color N
1307 // Color 0 is handled by SetPaletteWithTransparent (transparent)
1308 // Colors 1-15 need to come from palette[slot+1] through palette[slot+15]
1309 if (palette_slot >= 0 && static_cast<size_t>(palette_slot + 16) <=
1310 display_palette->size()) {
1312 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
1313 } else {
1315 15);
1316 }
1317 } else {
1318 tile8_preview_bmp_.SetPalette(*display_palette);
1319 }
1320 }
1321
1322 // Apply flips if needed
1323 if (x_flip || y_flip) {
1324 auto& data = tile8_preview_bmp_.mutable_data();
1325
1326 if (x_flip) {
1327 for (int y = 0; y < 8; ++y) {
1328 for (int x = 0; x < 4; ++x) {
1329 std::swap(data[y * 8 + x], data[y * 8 + (7 - x)]);
1330 }
1331 }
1332 }
1333
1334 if (y_flip) {
1335 for (int y = 0; y < 4; ++y) {
1336 for (int x = 0; x < 8; ++x) {
1337 std::swap(data[y * 8 + x], data[(7 - y) * 8 + x]);
1338 }
1339 }
1340 }
1341 }
1342
1343 // Push pixel changes to the existing surface before queuing texture work
1345
1346 // Queue texture creation/update on the persistent preview bitmap to
1347 // avoid dangling stack pointers in the arena queue
1348 const auto preview_command =
1352 gfx::Arena::Get().QueueTextureCommand(preview_command,
1354
1355 // CRITICAL FIX: Handle tile painting with simple click instead of
1356 // click+drag Draw the preview first
1359
1360 // Check for simple click to paint tile8 to tile16
1361 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
1362 const ImGuiIO& io = ImGui::GetIO();
1363 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
1364 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
1365 io.MousePos.y - canvas_pos.y);
1366
1367 // Convert canvas coordinates to tile16 coordinates
1368 // Account for bitmap offset (2,2) and scale (4x)
1369 constexpr float kBitmapOffset = 2.0f;
1370 constexpr float kBitmapScale = 4.0f;
1371 int tile_x =
1372 static_cast<int>((mouse_pos.x - kBitmapOffset) / kBitmapScale);
1373 int tile_y =
1374 static_cast<int>((mouse_pos.y - kBitmapOffset) / kBitmapScale);
1375
1376 // Clamp to valid range (0-15 for 16x16 tile)
1377 tile_x = std::max(0, std::min(15, tile_x));
1378 tile_y = std::max(0, std::min(15, tile_y));
1379
1380 util::logf("Tile16 canvas click: (%.2f, %.2f) -> Tile16: (%d, %d)",
1381 mouse_pos.x, mouse_pos.y, tile_x, tile_y);
1382
1383 // Pass nullptr to let DrawToCurrentTile16 handle flipping and store
1384 // correct TileInfo metadata. The preview bitmap is pre-flipped for
1385 // display only.
1386 RETURN_IF_ERROR(DrawToCurrentTile16(ImVec2(tile_x, tile_y), nullptr));
1387 }
1388
1389 // Right-click to pick tile8 from tile16
1390 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
1391 const ImGuiIO& io = ImGui::GetIO();
1392 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
1393 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
1394 io.MousePos.y - canvas_pos.y);
1395
1396 // Convert canvas coordinates to tile16 coordinates
1397 // Account for bitmap offset (2,2) and scale (4x)
1398 constexpr float kBitmapOffset = 2.0f;
1399 constexpr float kBitmapScale = 4.0f;
1400 int tile_x =
1401 static_cast<int>((mouse_pos.x - kBitmapOffset) / kBitmapScale);
1402 int tile_y =
1403 static_cast<int>((mouse_pos.y - kBitmapOffset) / kBitmapScale);
1404
1405 // Clamp to valid range (0-15 for 16x16 tile)
1406 tile_x = std::max(0, std::min(15, tile_x));
1407 tile_y = std::max(0, std::min(15, tile_y));
1408
1409 RETURN_IF_ERROR(PickTile8FromTile16(ImVec2(tile_x, tile_y)));
1410 util::logf("Right-clicked to pick tile8 from tile16 at (%d, %d)",
1411 tile_x, tile_y);
1412 }
1413 }
1414
1415 gui::EndCanvas(tile16_edit_canvas_, tile16_edit_rt,
1416 tile16_edit_frame_opts);
1417 }
1418 ImGui::EndChild();
1419
1420 Separator();
1421
1422 // === Compact Controls Section ===
1423
1424 // Tile8 info and preview
1425 if (current_tile8_ >= 0 &&
1426 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
1428 Text("Tile8: %02X", current_tile8_);
1429 SameLine();
1430 auto* tile8_texture = current_gfx_individual_[current_tile8_].texture();
1431 if (tile8_texture) {
1432 ImGui::Image((ImTextureID)(intptr_t)tile8_texture, ImVec2(24, 24));
1433 }
1434
1435 // Show encoded palette row indicator
1436 // This shows which palette row the tile is encoded to use in the ROM
1437 int sheet_idx = GetSheetIndexForTile8(current_tile8_);
1438 int encoded_row = -1;
1439
1440 // Determine encoded row based on sheet and ProcessGraphicsBuffer behavior
1441 // Sheets 0, 3, 4, 5 have 0x88 added (row 8-9)
1442 // Other sheets have raw values (row 0)
1443 switch (sheet_idx) {
1444 case 0:
1445 case 3:
1446 case 4:
1447 case 5:
1448 encoded_row = 8; // 0x88 offset = row 8
1449 break;
1450 default:
1451 encoded_row = 0; // Raw values = row 0
1452 break;
1453 }
1454
1455 // Visual indicator showing sheet and encoded row
1456 ImGui::SameLine();
1457 ImGui::TextDisabled("S%d", sheet_idx);
1458 if (ImGui::IsItemHovered()) {
1459 ImGui::BeginTooltip();
1460 ImGui::Text("Sheet: %d", sheet_idx);
1461 ImGui::Text("Encoded Palette Row: %d", encoded_row);
1462 ImGui::Separator();
1463 ImGui::TextWrapped(
1464 "Graphics sheets have different palette encodings:\n"
1465 "- Sheets 0,3,4,5: Row 8 (offset 0x88)\n"
1466 "- Sheets 1,2,6,7: Row 0 (raw)");
1467 ImGui::EndTooltip();
1468 }
1469 }
1470
1471 // Tile8 transform options in compact form
1472 Checkbox("X Flip", &x_flip);
1473 SameLine();
1474 Checkbox("Y Flip", &y_flip);
1475 SameLine();
1476 Checkbox("Priority", &priority_tile);
1477
1478 Separator();
1479
1480 // Palette selector - more compact
1481 Text("Palette:");
1482 if (show_debug_info) {
1483 SameLine();
1484 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1485 ImGui::TextDisabled("(Slot %d)", actual_slot);
1486 }
1487
1488 // Compact palette grid
1489 ImGui::BeginGroup();
1490 float available_width = ImGui::GetContentRegionAvail().x;
1491 float button_size = std::min(32.0f, (available_width - 16.0f) / 4.0f);
1492
1493 for (int row = 0; row < 2; ++row) {
1494 for (int col = 0; col < 4; ++col) {
1495 if (col > 0)
1496 ImGui::SameLine();
1497
1498 int i = row * 4 + col;
1499 bool is_current = (current_palette_ == i);
1500
1501 // Modern button styling with better visual hierarchy
1502 ImGui::PushID(i);
1503
1504 gui::StyleColorGuard palette_btn_colors(
1505 {{ImGuiCol_Button,
1506 is_current ? ImVec4(0.2f, 0.7f, 0.3f, 1.0f)
1507 : ImVec4(0.3f, 0.3f, 0.35f, 1.0f)},
1508 {ImGuiCol_ButtonHovered,
1509 is_current ? ImVec4(0.3f, 0.8f, 0.4f, 1.0f)
1510 : ImVec4(0.4f, 0.4f, 0.45f, 1.0f)},
1511 {ImGuiCol_ButtonActive,
1512 is_current ? ImVec4(0.1f, 0.6f, 0.2f, 1.0f)
1513 : ImVec4(0.25f, 0.25f, 0.3f, 1.0f)},
1514 {ImGuiCol_Border,
1515 is_current ? ImVec4(0.4f, 0.9f, 0.5f, 1.0f)
1516 : ImVec4(0.5f, 0.5f, 0.5f, 0.3f)}});
1517 gui::StyleVarGuard palette_btn_border(
1518 ImGuiStyleVar_FrameBorderSize, 1.0f);
1519
1520 if (ImGui::Button(absl::StrFormat("%d", i).c_str(),
1521 ImVec2(button_size, button_size))) {
1522 if (current_palette_ != i) {
1523 current_palette_ = i;
1524 auto status = RefreshAllPalettes();
1525 if (!status.ok()) {
1526 util::logf("Failed to refresh palettes: %s",
1527 status.message().data());
1528 } else {
1529 util::logf("Palette successfully changed to %d",
1531 }
1532 }
1533 }
1534
1535 ImGui::PopID();
1536
1537 // Simplified tooltip
1538 if (ImGui::IsItemHovered()) {
1539 ImGui::BeginTooltip();
1540 if (show_debug_info) {
1541 ImGui::Text("Palette %d → Slots:", i);
1542 ImGui::Text(" S0,3,4: %d", GetActualPaletteSlot(i, 0));
1543 ImGui::Text(" S1,2: %d", GetActualPaletteSlot(i, 1));
1544 ImGui::Text(" S5,6: %d", GetActualPaletteSlot(i, 5));
1545 ImGui::Text(" S7: %d", GetActualPaletteSlot(i, 7));
1546 } else {
1547 ImGui::Text("Palette %d", i);
1548 if (is_current) {
1549 ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Active");
1550 }
1551 }
1552 ImGui::EndTooltip();
1553 }
1554 }
1555 }
1556 ImGui::EndGroup();
1557
1558 Separator();
1559
1560 // Compact action buttons
1561 if (Button("Clear", ImVec2(-1, 0))) {
1563 }
1564
1565 if (Button("Copy", ImVec2(-1, 0))) {
1567 }
1568
1569 if (Button("Paste", ImVec2(-1, 0))) {
1571 }
1572
1573 Separator();
1574
1575 // Save/Discard - full width buttons
1576 if (Button("Save Changes", ImVec2(-1, 0))) {
1578 }
1579 HOVER_HINT("Apply changes to overworld and regenerate blockset");
1580
1581 if (Button("Discard Changes", ImVec2(-1, 0))) {
1583 }
1584 HOVER_HINT("Reload tile16 from ROM, discarding local changes");
1585
1586 Separator();
1587
1588 bool can_undo = undo_manager_.CanUndo();
1589 if (!can_undo)
1590 BeginDisabled();
1591 if (Button("Undo", ImVec2(-1, 0))) {
1593 }
1594 if (!can_undo)
1595 EndDisabled();
1596
1597 // Advanced controls (collapsible)
1598 if (show_advanced_controls) {
1599 Separator();
1600 Text("Advanced:");
1601
1602 if (Button("Palette Settings", ImVec2(-1, 0))) {
1604 }
1605
1606 if (Button("Analyze Data", ImVec2(-1, 0))) {
1608 }
1609 HOVER_HINT("Analyze tile8 source data format and palette state");
1610
1611 if (Button("Manual Edit", ImVec2(-1, 0))) {
1612 ImGui::OpenPopup("ManualTile8Editor");
1613 }
1614
1615 if (Button("Refresh Blockset", ImVec2(-1, 0))) {
1617 }
1618
1619 // Scratch space in compact form
1620 Text("Scratch:");
1622
1623 // Manual tile8 editor popup
1625 }
1626
1627 // Compact debug information panel
1628 if (show_debug_info) {
1629 Separator();
1630 Text("Debug:");
1631 ImGui::TextDisabled("T16:%02X T8:%d Pal:%d", current_tile16_,
1633
1634 if (current_tile8_ >= 0) {
1635 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1636 int actual_slot = GetActualPaletteSlot(current_palette_, sheet_index);
1637 ImGui::TextDisabled("Sheet:%d Slot:%d", sheet_index, actual_slot);
1638 }
1639
1640 // Compact palette mapping table
1641 if (ImGui::CollapsingHeader("Palette Map",
1642 ImGuiTreeNodeFlags_DefaultOpen)) {
1643 ImGui::BeginChild("##PaletteMappingScroll", ImVec2(0, 120), true);
1644 if (ImGui::BeginTable("##PalMap", 3,
1645 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1646 ImGuiTableFlags_SizingFixedFit)) {
1647 ImGui::TableSetupColumn("Btn", ImGuiTableColumnFlags_WidthFixed, 30);
1648 ImGui::TableSetupColumn("S0,3-4", ImGuiTableColumnFlags_WidthFixed,
1649 50);
1650 ImGui::TableSetupColumn("S1-2", ImGuiTableColumnFlags_WidthFixed, 50);
1651 ImGui::TableHeadersRow();
1652
1653 for (int i = 0; i < 8; ++i) {
1654 ImGui::TableNextRow();
1655 ImGui::TableNextColumn();
1656 ImGui::Text("%d", i);
1657 ImGui::TableNextColumn();
1658 ImGui::Text("%d", GetActualPaletteSlot(i, 0));
1659 ImGui::TableNextColumn();
1660 ImGui::Text("%d", GetActualPaletteSlot(i, 1));
1661 }
1662 ImGui::EndTable();
1663 }
1664 ImGui::EndChild();
1665 }
1666
1667 // Color preview - compact
1668 if (ImGui::CollapsingHeader("Colors")) {
1669 if (overworld_palette_.size() >= 256) {
1670 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1671 ImGui::Text("Slot %d:", actual_slot);
1672
1673 for (int i = 0;
1674 i < 8 &&
1675 (actual_slot + i) < static_cast<int>(overworld_palette_.size());
1676 ++i) {
1677 int color_index = actual_slot + i;
1678 auto color = overworld_palette_[color_index];
1679 ImVec4 display_color = color.rgb();
1680
1681 ImGui::ColorButton(absl::StrFormat("##c%d", i).c_str(),
1682 display_color, ImGuiColorEditFlags_NoTooltip,
1683 ImVec2(20, 20));
1684 if (ImGui::IsItemHovered()) {
1685 ImGui::SetTooltip("%d:0x%04X", color_index, color.snes());
1686 }
1687
1688 if ((i + 1) % 4 != 0)
1689 ImGui::SameLine();
1690 }
1691 }
1692 }
1693 }
1694
1695 ImGui::EndGroup();
1696 EndTable();
1697 }
1698
1699 // Draw palette settings and canvas popups
1701
1702 // Show canvas popup windows if opened from context menu
1709
1710 return absl::OkStatus();
1711}
1712
1714 if (!current_gfx_bmp_.is_active() || current_gfx_bmp_.data() == nullptr) {
1715 return absl::FailedPreconditionError(
1716 "Current graphics bitmap not initialized");
1717 }
1718
1720
1721 // Calculate how many 8x8 tiles we can fit based on the current graphics
1722 // bitmap size SNES graphics are typically 128 pixels wide (16 tiles of 8
1723 // pixels each)
1724 const int tiles_per_row = current_gfx_bmp_.width() / 8;
1725 const int total_rows = current_gfx_bmp_.height() / 8;
1726 const int total_tiles = tiles_per_row * total_rows;
1727
1728 current_gfx_individual_.reserve(total_tiles);
1729
1730 // Extract individual 8x8 tiles from the graphics bitmap
1731 for (int tile_y = 0; tile_y < total_rows; ++tile_y) {
1732 for (int tile_x = 0; tile_x < tiles_per_row; ++tile_x) {
1733 std::vector<uint8_t> tile_data(64); // 8x8 = 64 pixels
1734
1735 // Extract tile data from the main graphics bitmap.
1736 // Preserve encoded palette offsets unless normalization is enabled.
1737 for (int py = 0; py < 8; ++py) {
1738 for (int px = 0; px < 8; ++px) {
1739 int src_x = tile_x * 8 + px;
1740 int src_y = tile_y * 8 + py;
1741 int src_index = src_y * current_gfx_bmp_.width() + src_x;
1742 int dst_index = py * 8 + px;
1743
1744 if (src_index < static_cast<int>(current_gfx_bmp_.size()) &&
1745 dst_index < 64) {
1746 uint8_t pixel_value = current_gfx_bmp_.data()[src_index];
1747
1749 pixel_value &= palette_normalization_mask_;
1750 }
1751
1752 tile_data[dst_index] = pixel_value;
1753 }
1754 }
1755 }
1756
1757 // Create the individual tile bitmap
1758 current_gfx_individual_.emplace_back();
1759 auto& tile_bitmap = current_gfx_individual_.back();
1760
1761 try {
1762 tile_bitmap.Create(8, 8, 8, tile_data);
1763
1764 // Set default palette using the same system as overworld
1765 if (overworld_palette_.size() >= 256) {
1766 // Use complete 256-color palette (same as overworld system)
1767 // The pixel data already contains correct color indices for the
1768 // 256-color palette
1769 tile_bitmap.SetPalette(overworld_palette_);
1770 } else if (game_data() &&
1771 game_data()->palette_groups.overworld_main.size() > 0) {
1772 // Fallback to GameData palette
1773 tile_bitmap.SetPalette(game_data()->palette_groups.overworld_main[0]);
1774 }
1775 // Queue texture creation via Arena's deferred system
1778 } catch (const std::exception& e) {
1779 util::logf("Error creating tile at (%d,%d): %s", tile_x, tile_y,
1780 e.what());
1781 // Create an empty bitmap as fallback
1782 tile_bitmap.Create(8, 8, 8, std::vector<uint8_t>(64, 0));
1783 }
1784 }
1785 }
1786
1787 // Apply current palette settings to all tiles
1788 if (rom_) {
1790 }
1791
1792 // Ensure canvas scroll size matches the full tilesheet at preview scale
1796
1797 util::logf("Loaded %zu individual tile8 graphics",
1799 return absl::OkStatus();
1800}
1801
1802absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
1803 if (tile_id < 0 || tile_id >= kTile16Count) {
1804 return absl::OutOfRangeError(
1805 absl::StrFormat("Invalid tile16 id: %d", tile_id));
1806 }
1807
1808 if (!tile16_blockset_ || !rom_) {
1809 return absl::FailedPreconditionError(
1810 "Tile16 blockset or ROM not initialized");
1811 }
1812
1813 // Commit any in-progress edits before switching the current tile selection so
1814 // undo/redo captures the correct "after" state.
1816
1817 current_tile16_ = tile_id;
1818 jump_to_tile_id_ = tile_id; // Sync input field with current tile
1819
1820 // Load editable tile16 metadata from pending state first, then ROM.
1821 auto pending_it = pending_tile16_changes_.find(current_tile16_);
1822 if (pending_it != pending_tile16_changes_.end()) {
1823 current_tile16_data_ = pending_it->second;
1824 } else {
1827 }
1828 SyncTilesInfoArray(&current_tile16_data_);
1829
1830 bool bitmap_loaded = false;
1831
1832 auto pending_bitmap_it = pending_tile16_bitmaps_.find(current_tile16_);
1833 if (pending_bitmap_it != pending_tile16_bitmaps_.end() &&
1834 pending_bitmap_it->second.is_active()) {
1836 pending_bitmap_it->second.vector());
1837 current_tile16_bmp_.SetPalette(pending_bitmap_it->second.palette());
1838 bitmap_loaded = true;
1839 } else {
1840 auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
1841 if (!tile_data.empty()) {
1842 for (auto& pixel : tile_data) {
1845 }
1846 }
1848 bitmap_loaded = true;
1849 }
1850 }
1851
1852 if (!bitmap_loaded) {
1854 } else {
1859 }
1860 }
1861
1862 util::logf("SetCurrentTile: loaded tile %d successfully", tile_id);
1863 return absl::OkStatus();
1864}
1865
1866void Tile16Editor::RequestTileSwitch(int target_tile_id) {
1867 // Validate that the tile16 editor is properly initialized
1868 if (!tile16_blockset_ || !rom_) {
1869 util::logf(
1870 "RequestTileSwitch: Editor not initialized (blockset=%p, rom=%p)",
1872 return;
1873 }
1874
1875 // Validate target tile ID
1876 if (target_tile_id < 0 || target_tile_id >= kTile16Count) {
1877 util::logf("RequestTileSwitch: Invalid target tile ID %d", target_tile_id);
1878 return;
1879 }
1880
1881 // Check if we're already on this tile
1882 if (target_tile_id == current_tile16_) {
1883 return;
1884 }
1885
1886 // Check if current tile has pending changes
1888 // Store target and show dialog
1889 pending_tile_switch_target_ = target_tile_id;
1891 util::logf("Tile %d has pending changes, showing confirmation dialog",
1893 } else {
1894 // No pending changes, switch directly
1895 auto status = SetCurrentTile(target_tile_id);
1896 if (!status.ok()) {
1897 util::logf("Failed to switch to tile %d: %s", target_tile_id,
1898 status.message().data());
1899 }
1900 }
1901}
1902
1903absl::Status Tile16Editor::CopyTile16ToClipboard(int tile_id) {
1904 if (tile_id < 0 || tile_id >= kTile16Count) {
1905 return absl::InvalidArgumentError("Invalid tile ID");
1906 }
1907 if (!rom_) {
1908 return absl::FailedPreconditionError("ROM not available");
1909 }
1910
1911 auto pending_tile_it = pending_tile16_changes_.find(tile_id);
1912 if (tile_id == current_tile16_) {
1914 } else if (pending_tile_it != pending_tile16_changes_.end()) {
1915 clipboard_tile16_.tile_data = pending_tile_it->second;
1916 } else {
1919 }
1920 SyncTilesInfoArray(&clipboard_tile16_.tile_data);
1921
1922 bool bitmap_copied = false;
1923 auto pending_bitmap_it = pending_tile16_bitmaps_.find(tile_id);
1924 if (tile_id == current_tile16_ && current_tile16_bmp_.is_active()) {
1928 bitmap_copied = true;
1929 } else if (pending_bitmap_it != pending_tile16_bitmaps_.end() &&
1930 pending_bitmap_it->second.is_active()) {
1932 pending_bitmap_it->second.vector());
1933 clipboard_tile16_.bitmap.SetPalette(pending_bitmap_it->second.palette());
1934 bitmap_copied = true;
1935 } else if (tile16_blockset_) {
1936 auto tile_pixels = gfx::GetTilemapData(*tile16_blockset_, tile_id);
1937 if (!tile_pixels.empty()) {
1940 bitmap_copied = true;
1941 }
1942 }
1943
1944 if (bitmap_copied) {
1947 }
1948
1950 return absl::OkStatus();
1951}
1952
1981
1983 if (slot < 0 || slot >= kNumScratchSlots) {
1984 return absl::InvalidArgumentError("Invalid scratch space slot");
1985 }
1987 return absl::FailedPreconditionError("No active tile16 to save");
1988 }
1989
1990 scratch_space_[slot].tile_data = current_tile16_data_;
1991 SyncTilesInfoArray(&scratch_space_[slot].tile_data);
1992 scratch_space_[slot].bitmap.Create(kTile16Size, kTile16Size, 8,
1994 scratch_space_[slot].bitmap.SetPalette(current_tile16_bmp_.palette());
1995 // Queue texture creation via Arena's deferred system
1998
1999 scratch_space_[slot].has_data = true;
2000 return absl::OkStatus();
2001}
2002
2004 if (slot < 0 || slot >= kNumScratchSlots) {
2005 return absl::InvalidArgumentError("Invalid scratch space slot");
2006 }
2007
2008 if (!scratch_space_[slot].has_data) {
2009 return absl::FailedPreconditionError("Scratch space slot is empty");
2010 }
2011
2012 SaveUndoState();
2013
2014 current_tile16_data_ = scratch_space_[slot].tile_data;
2015 SyncTilesInfoArray(&current_tile16_data_);
2016
2017 if (scratch_space_[slot].bitmap.is_active()) {
2019 scratch_space_[slot].bitmap.vector());
2020 current_tile16_bmp_.SetPalette(scratch_space_[slot].bitmap.palette());
2024 } else {
2026 }
2027
2031 }
2033 return absl::OkStatus();
2034}
2035
2036absl::Status Tile16Editor::ClearScratchSpace(int slot) {
2037 if (slot < 0 || slot >= kNumScratchSlots) {
2038 return absl::InvalidArgumentError("Invalid scratch space slot");
2039 }
2040
2041 scratch_space_[slot].has_data = false;
2042 return absl::OkStatus();
2043}
2044
2045// Advanced editing features
2048 return absl::FailedPreconditionError("No active tile16 to flip");
2049 }
2050
2051 SaveUndoState();
2052
2053 const gfx::Tile16 original = current_tile16_data_;
2054 current_tile16_data_.tile0_ = HorizontalFlipInfo(original.tile1_);
2055 current_tile16_data_.tile1_ = HorizontalFlipInfo(original.tile0_);
2056 current_tile16_data_.tile2_ = HorizontalFlipInfo(original.tile3_);
2057 current_tile16_data_.tile3_ = HorizontalFlipInfo(original.tile2_);
2058 SyncTilesInfoArray(&current_tile16_data_);
2059
2064 }
2065
2066 // Track this tile as having pending changes
2068
2069 return absl::OkStatus();
2070}
2071
2074 return absl::FailedPreconditionError("No active tile16 to flip");
2075 }
2076
2077 SaveUndoState();
2078
2079 const gfx::Tile16 original = current_tile16_data_;
2080 current_tile16_data_.tile0_ = VerticalFlipInfo(original.tile2_);
2081 current_tile16_data_.tile1_ = VerticalFlipInfo(original.tile3_);
2082 current_tile16_data_.tile2_ = VerticalFlipInfo(original.tile0_);
2083 current_tile16_data_.tile3_ = VerticalFlipInfo(original.tile1_);
2084 SyncTilesInfoArray(&current_tile16_data_);
2085
2090 }
2091
2092 // Track this tile as having pending changes
2094
2095 return absl::OkStatus();
2096}
2097
2100 return absl::FailedPreconditionError("No active tile16 to rotate");
2101 }
2102
2103 SaveUndoState();
2104
2105 // Tile16 metadata does not support arbitrary 8x8 rotation flags.
2106 // Rotate the 2x2 quadrant layout in a persistable way.
2107 const gfx::Tile16 original = current_tile16_data_;
2108 current_tile16_data_.tile0_ = original.tile2_; // BL -> TL
2109 current_tile16_data_.tile1_ = original.tile0_; // TL -> TR
2110 current_tile16_data_.tile2_ = original.tile3_; // BR -> BL
2111 current_tile16_data_.tile3_ = original.tile1_; // TR -> BR
2112 SyncTilesInfoArray(&current_tile16_data_);
2113
2118 }
2119
2120 // Track this tile as having pending changes
2122
2123 return absl::OkStatus();
2124}
2125
2126absl::Status Tile16Editor::FillTile16WithTile8(int tile8_id) {
2127 if (tile8_id < 0 ||
2128 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2129 return absl::InvalidArgumentError("Invalid tile8 ID");
2130 }
2131
2132 if (!current_gfx_individual_[tile8_id].is_active()) {
2133 return absl::FailedPreconditionError("Source tile8 not active");
2134 }
2135
2136 SaveUndoState();
2137
2138 const gfx::TileInfo fill_info(static_cast<uint16_t>(tile8_id), current_palette_,
2140 for (int quadrant = 0; quadrant < 4; ++quadrant) {
2141 TileInfoForQuadrant(&current_tile16_data_, quadrant) = fill_info;
2142 }
2143 SyncTilesInfoArray(&current_tile16_data_);
2144
2149 }
2150
2151 // Track this tile as having pending changes
2153
2154 return absl::OkStatus();
2155}
2156
2159 return absl::FailedPreconditionError("No active tile16 to clear");
2160 }
2161
2162 SaveUndoState();
2163
2164 const gfx::TileInfo clear_info(0, current_palette_, false, false, false);
2165 for (int quadrant = 0; quadrant < 4; ++quadrant) {
2166 TileInfoForQuadrant(&current_tile16_data_, quadrant) = clear_info;
2167 }
2168 SyncTilesInfoArray(&current_tile16_data_);
2169
2174 }
2175
2176 // Track this tile as having pending changes
2178
2179 return absl::OkStatus();
2180}
2181
2182// Palette management
2183absl::Status Tile16Editor::CyclePalette(bool forward) {
2184 uint8_t new_palette = current_palette_;
2185
2186 if (forward) {
2187 new_palette = (new_palette + 1) % 8;
2188 } else {
2189 new_palette = (new_palette == 0) ? 7 : new_palette - 1;
2190 }
2191
2192 current_palette_ = new_palette;
2193
2194 // Use the RefreshAllPalettes method which handles all the coordination
2196
2197 util::logf("Cycled to palette slot %d", current_palette_);
2198 return absl::OkStatus();
2199}
2200
2201absl::Status Tile16Editor::PreviewPaletteChange(uint8_t palette_id) {
2202 if (!show_palette_preview_) {
2203 return absl::OkStatus();
2204 }
2205
2206 if (palette_id >= 8) {
2207 return absl::InvalidArgumentError("Invalid palette ID");
2208 }
2209
2210 // Create a preview bitmap with the new palette
2211 if (!preview_tile16_.is_active()) {
2213 } else {
2214 // Recreate the preview bitmap with new data
2216 }
2217
2218 if (!game_data())
2219 return absl::FailedPreconditionError("GameData not available");
2220 const auto& ow_main_pal_group = game_data()->palette_groups.overworld_main;
2221 const gfx::SnesPalette* display_palette = nullptr;
2222 if (overworld_palette_.size() >= 256) {
2223 display_palette = &overworld_palette_;
2224 } else if (palette_.size() >= 256) {
2225 display_palette = &palette_;
2226 } else if (ow_main_pal_group.size() > 0) {
2227 display_palette = &ow_main_pal_group[0];
2228 }
2229
2230 if (display_palette && !display_palette->empty()) {
2231 if (auto_normalize_pixels_ && ow_main_pal_group.size() > palette_id) {
2232 preview_tile16_.SetPaletteWithTransparent(ow_main_pal_group[0],
2233 palette_id);
2234 } else {
2235 preview_tile16_.SetPalette(*display_palette);
2236 }
2237 // Queue texture update via Arena's deferred system
2240 preview_dirty_ = true;
2241 }
2242
2243 return absl::OkStatus();
2244}
2245
2246// Undo/Redo system (unified UndoManager framework)
2247
2263
2265 if (!pending_undo_before_.has_value()) return;
2267 pending_undo_before_.reset();
2268 return;
2269 }
2270
2271 // Capture the current (post-edit) state as the "after" snapshot
2272 Tile16Snapshot after;
2273 after.tile_id = current_tile16_;
2277 after.palette = current_palette_;
2278 after.x_flip = x_flip;
2279 after.y_flip = y_flip;
2280 after.priority = priority_tile;
2281
2282 // Build the restore callback that captures `this`
2283 auto restore_fn = [this](const Tile16Snapshot& snap) {
2284 RestoreFromSnapshot(snap);
2285 };
2286
2287 undo_manager_.Push(std::make_unique<Tile16EditAction>(
2288 std::move(*pending_undo_before_), std::move(after), restore_fn));
2289
2290 pending_undo_before_.reset();
2291}
2292
2295 return;
2296 }
2297
2298 // Finalize any previously pending snapshot before starting a new one
2300
2301 Tile16Snapshot before;
2302 before.tile_id = current_tile16_;
2306 before.palette = current_palette_;
2307 before.x_flip = x_flip;
2308 before.y_flip = y_flip;
2309 before.priority = priority_tile;
2310
2311 pending_undo_before_ = std::move(before);
2312}
2313
2314absl::Status Tile16Editor::Undo() {
2316 return undo_manager_.Undo();
2317}
2318
2319absl::Status Tile16Editor::Redo() {
2320 return undo_manager_.Redo();
2321}
2322
2324 if (!tile16_blockset_) {
2325 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2326 }
2327
2328 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2329 return absl::OutOfRangeError("Current tile16 ID out of range");
2330 }
2331
2332 if (current_palette_ >= 8) {
2333 return absl::OutOfRangeError("Current palette ID out of range");
2334 }
2335
2336 return absl::OkStatus();
2337}
2338
2339bool Tile16Editor::IsTile16Valid(int tile_id) const {
2340 return tile16_blockset_ != nullptr && tile_id >= 0 &&
2341 tile_id < kTile16Count;
2342}
2343
2344// Integration with overworld system
2346 if (!rom_) {
2347 return absl::FailedPreconditionError("ROM not available");
2348 }
2349
2351 return absl::FailedPreconditionError("No active tile16 to save");
2352 }
2353
2354 // Write the tile16 data to ROM first
2356
2357 // Update the tile16 blockset with current changes
2359
2360 // Commit changes to the tile16 blockset
2362
2365
2366 // Mark ROM as dirty so changes persist when saving
2367 rom_->set_dirty(true);
2368
2369 util::logf("Tile16 %d saved to ROM", current_tile16_);
2370 return absl::OkStatus();
2371}
2372
2374 if (!tile16_blockset_) {
2375 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2376 }
2377
2378 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2379 return absl::OutOfRangeError("Current tile16 ID out of range");
2380 }
2381
2382 // Update atlas directly instead of using problematic tile cache
2384
2385 return absl::OkStatus();
2386}
2387
2389 if (!tile16_blockset_) {
2390 return absl::FailedPreconditionError("Tile16 blockset not initialized");
2391 }
2392
2393 // Regenerate the tilemap data if needed
2395 // Queue texture update via Arena's deferred system
2398 }
2399
2400 // Update individual cached tiles
2401 // Note: With the new tile cache system, tiles are automatically managed
2402 // and don't need manual modification tracking like the old system
2403 // The cache handles LRU eviction and automatic updates
2404
2405 return absl::OkStatus();
2406}
2407
2409 // Step 1: Update ROM data with current tile16 changes
2411
2412 // Step 2: Update the local blockset to reflect changes
2414
2415 // Step 3: Update the atlas directly
2417
2418 // Step 4: Notify the parent editor (overworld editor) to regenerate its
2419 // blockset
2422 }
2423
2426
2427 util::logf("Committed Tile16 %d changes to overworld system",
2429 return absl::OkStatus();
2430}
2431
2433 // Reload the current tile16 from ROM to discard any local changes
2435
2436 util::logf("Discarded Tile16 changes for tile %d", current_tile16_);
2437 return absl::OkStatus();
2438}
2439
2441 if (pending_tile16_changes_.empty()) {
2442 return absl::OkStatus(); // Nothing to commit
2443 }
2444
2445 util::logf("Committing %zu pending tile16 changes to ROM",
2447
2448 // Write all pending changes to ROM
2449 for (const auto& [tile_id, tile_data] : pending_tile16_changes_) {
2450 auto status = rom_->WriteTile16(tile_id, zelda3::kTile16Ptr, tile_data);
2451 if (!status.ok()) {
2452 util::logf("Failed to write tile16 %d: %s", tile_id,
2453 status.message().data());
2454 return status;
2455 }
2456 }
2457
2458 // Clear pending changes
2461
2462 // Refresh the blockset to show committed changes
2464
2465 // Notify parent editor to refresh overworld display
2468 }
2469
2470 rom_->set_dirty(true);
2471 util::logf("All pending tile16 changes committed successfully");
2472 return absl::OkStatus();
2473}
2474
2476 if (pending_tile16_changes_.empty()) {
2477 return;
2478 }
2479
2480 util::logf("Discarding %zu pending tile16 changes",
2482
2485
2486 // Reload current tile to restore original state
2487 auto status = SetCurrentTile(current_tile16_);
2488 if (!status.ok()) {
2489 util::logf("Failed to reload tile after discard: %s",
2490 status.message().data());
2491 }
2492}
2493
2496 if (it != pending_tile16_changes_.end()) {
2497 pending_tile16_changes_.erase(it);
2499 util::logf("Discarded pending changes for tile %d", current_tile16_);
2500 }
2501
2502 // Reload tile from ROM
2503 auto status = SetCurrentTile(current_tile16_);
2504 if (!status.ok()) {
2505 util::logf("Failed to reload tile after discard: %s",
2506 status.message().data());
2507 }
2508}
2509
2511 if (current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2512 return;
2513 }
2514
2515 SyncTilesInfoArray(&current_tile16_data_);
2518 preview_dirty_ = true;
2519
2520 util::logf("Marked tile %d as modified (total pending: %zu)",
2522}
2523
2524absl::Status Tile16Editor::PickTile8FromTile16(const ImVec2& position) {
2525 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= kTile16Count) {
2526 return absl::InvalidArgumentError("Invalid tile16 or ROM not set");
2527 }
2528
2529 // Determine which quadrant of the tile16 was clicked
2530 int quad_x = (position.x < 8) ? 0 : 1; // Left or right half
2531 int quad_y = (position.y < 8) ? 0 : 1; // Top or bottom half
2532 int quadrant = quad_x + (quad_y * 2); // 0=TL, 1=TR, 2=BL, 3=BR
2533
2534 // Get the tile16 data structure
2535 auto* tile16_data = GetCurrentTile16Data();
2536 if (!tile16_data) {
2537 return absl::FailedPreconditionError("Failed to get tile16 data");
2538 }
2539
2540 // Extract the tile8 ID from the appropriate quadrant
2541 gfx::TileInfo tile_info;
2542 switch (quadrant) {
2543 case 0:
2544 tile_info = tile16_data->tile0_;
2545 break; // Top-left
2546 case 1:
2547 tile_info = tile16_data->tile1_;
2548 break; // Top-right
2549 case 2:
2550 tile_info = tile16_data->tile2_;
2551 break; // Bottom-left
2552 case 3:
2553 tile_info = tile16_data->tile3_;
2554 break; // Bottom-right
2555 }
2556
2557 // Set the current tile8 and palette
2558 current_tile8_ = tile_info.id_;
2559 current_palette_ = tile_info.palette_;
2560
2561 // Update the flip states based on the tile info
2562 x_flip = tile_info.horizontal_mirror_;
2563 y_flip = tile_info.vertical_mirror_;
2564 priority_tile = tile_info.over_;
2565
2566 // Refresh the palette to match the picked tile
2569
2570 util::logf("Picked tile8 %d with palette %d from quadrant %d of tile16 %d",
2572
2573 return absl::OkStatus();
2574}
2575
2576// Get the appropriate palette slot for current graphics sheet
2577int Tile16Editor::GetPaletteSlotForSheet(int sheet_index) const {
2578 // Based on ProcessGraphicsBuffer logic and overworld palette coordination:
2579 // Sheets 0,3-6: Use AUX palettes (slots 10-15 in 256-color palette)
2580 // Sheets 1-2: Use MAIN palette (slots 2-6 in 256-color palette)
2581 // Sheet 7: Use ANIMATED palette (slot 7 in 256-color palette)
2582
2583 switch (sheet_index) {
2584 case 0:
2585 return 10; // Main blockset -> AUX1 palette region
2586 case 1:
2587 return 2; // Main graphics -> MAIN palette region
2588 case 2:
2589 return 3; // Main graphics -> MAIN palette region
2590 case 3:
2591 return 11; // Area graphics -> AUX1 palette region
2592 case 4:
2593 return 12; // Area graphics -> AUX1 palette region
2594 case 5:
2595 return 13; // Area graphics -> AUX2 palette region
2596 case 6:
2597 return 14; // Area graphics -> AUX2 palette region
2598 case 7:
2599 return 7; // Animated tiles -> ANIMATED palette region
2600 default:
2601 return static_cast<int>(
2602 current_palette_); // Use current selection for other sheets
2603 }
2604}
2605
2606// NEW: Get the actual palette slot for a given palette button and sheet index
2607// This uses row-based addressing to match the overworld's approach:
2608// The 256-color palette is organized as 16 rows of 16 colors each.
2609// Palette buttons 0-7 map to CGRAM rows starting at the sheet's base row,
2610// skipping HUD rows for overworld visuals.
2612 int sheet_index) const {
2613 const int clamped_button = std::clamp(palette_button, 0, 7);
2614 const int base_row = GetPaletteBaseForSheet(sheet_index);
2615 const int actual_row = std::clamp(base_row + clamped_button, 0, 15);
2616
2617 // Palette buttons map to CGRAM rows starting from the sheet base.
2618 return actual_row * 16;
2619}
2620
2621// NEW: Get the sheet index for a given tile8 ID
2623 // Determine which graphics sheet a tile8 belongs to based on its position
2624 // This is based on the 256-tile per sheet organization
2625
2626 constexpr int kTilesPerSheet = 256; // 16x16 tiles per sheet
2627 int sheet_index = tile8_id / kTilesPerSheet;
2628
2629 // Clamp to valid sheet range (0-7)
2630 return std::min(7, std::max(0, sheet_index));
2631}
2632
2633// NEW: Get the actual palette slot for the current tile16 being edited
2635 // For the current tile16, we need to determine which sheet the tile8s belong
2636 // to and use the most appropriate palette region
2637
2638 if (current_tile8_ >= 0 &&
2639 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
2640 int sheet_index = GetSheetIndexForTile8(current_tile8_);
2641 return GetActualPaletteSlot(current_palette_, sheet_index);
2642 }
2643
2644 // Default to sheet 0 (main blockset) if no tile8 selected
2646}
2647
2648int Tile16Editor::GetPaletteBaseForSheet(int sheet_index) const {
2649 // Based on overworld palette structure and how ProcessGraphicsBuffer assigns
2650 // colors: The 256-color palette is organized as 16 rows of 16 colors each.
2651 // Different graphics sheets map to different palette regions:
2652 //
2653 // Row 0: Transparent/system colors
2654 // Row 1: HUD colors (palette index 0x10-0x1F)
2655 // Rows 2-4: MAIN/AUX1 palette region for main graphics
2656 // Rows 5-7: AUX2 palette region for area-specific graphics
2657 // Row 7: ANIMATED palette for animated tiles
2658 //
2659 // The palette_button (0-7) selects within the region.
2660 switch (sheet_index) {
2661 case 0: // Main blockset
2662 case 3: // Area graphics set 1
2663 case 4: // Area graphics set 2
2664 return 2; // AUX1 palette region starts at row 2
2665 case 5: // Area graphics set 3
2666 case 6: // Area graphics set 4
2667 return 5; // AUX2 palette region starts at row 5
2668 case 1: // Main graphics
2669 case 2: // Main graphics
2670 return 2; // MAIN palette region starts at row 2
2671 case 7: // Animated tiles
2672 return 7; // ANIMATED palette region at row 7
2673 default:
2674 return 2; // Default to MAIN region
2675 }
2676}
2677
2679 const gfx::SnesPalette& source, int target_row) const {
2680 // Create a remapped 256-color palette where all pixel values (0-255)
2681 // are mapped to the target palette row based on their low nibble.
2682 //
2683 // This allows the source bitmap (which has pre-encoded palette offsets)
2684 // to be viewed with the user-selected palette row.
2685 //
2686 // For each palette index i:
2687 // - Extract the color index: low_nibble = i & 0x0F
2688 // - Map to target row: (base_row + target_row) * 16 + low_nibble
2689 // - Copy the color from source palette at that position
2690
2691 gfx::SnesPalette remapped;
2692
2693 // Map palette buttons to actual CGRAM rows based on the current sheet.
2694 int sheet_index = 0;
2695 if (current_tile8_ >= 0 &&
2696 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
2698 }
2699 const int base_row = GetPaletteBaseForSheet(sheet_index);
2700 const int actual_target_row = std::clamp(base_row + target_row, 0, 15);
2701
2702 for (int i = 0; i < 256; ++i) {
2703 int low_nibble = i & 0x0F;
2704 int target_index = (actual_target_row * 16) + low_nibble;
2705
2706 // Make color 0 of each row transparent
2707 if (low_nibble == 0) {
2708 // Use transparent color (alpha = 0)
2709 remapped.AddColor(gfx::SnesColor(0));
2710 } else if (target_index < static_cast<int>(source.size())) {
2711 remapped.AddColor(source[target_index]);
2712 } else {
2713 // Fallback to black if out of bounds
2714 remapped.AddColor(gfx::SnesColor(0));
2715 }
2716 }
2717
2718 return remapped;
2719}
2720
2721int Tile16Editor::GetEncodedPaletteRow(uint8_t pixel_value) const {
2722 // Determine which palette row a pixel value encodes
2723 // ProcessGraphicsBuffer adds 0x88 (136) to sheets 0, 3, 4, 5
2724 // So pixel values map to rows as follows:
2725 // 0x00-0x0F (0-15): Row 0
2726 // 0x10-0x1F (16-31): Row 1
2727 // ...
2728 // 0x80-0x8F (128-143): Row 8
2729 // 0x90-0x9F (144-159): Row 9
2730 // etc.
2731 return pixel_value / 16;
2732}
2733
2736 return;
2737 }
2738
2739 const gfx::SnesPalette* display_palette = nullptr;
2740 if (overworld_palette_.size() >= 256) {
2741 display_palette = &overworld_palette_;
2742 } else if (palette_.size() >= 256) {
2743 display_palette = &palette_;
2744 } else if (game_data() &&
2745 !game_data()->palette_groups.overworld_main.empty()) {
2746 display_palette =
2748 }
2749
2750 if (!display_palette || display_palette->empty()) {
2751 return;
2752 }
2753
2755 const int palette_slot = GetActualPaletteSlotForCurrentTile16();
2756
2757 // Apply sub-palette with transparent color 0 using computed slot
2758 // SNES palette offset fix: add 1 to skip transparent color slot
2759 // SNES 4BPP uses 16 colors (transparent + 15)
2760 if (palette_slot >= 0 &&
2761 static_cast<size_t>(palette_slot + 16) <= display_palette->size()) {
2763 *display_palette, static_cast<size_t>(palette_slot + 1), 15);
2764 } else {
2765 current_tile16_bmp_.SetPaletteWithTransparent(*display_palette, 1, 15);
2766 }
2767 } else {
2768 current_tile16_bmp_.SetPalette(*display_palette);
2769 }
2770
2774}
2775
2776// Helper methods for palette management
2777absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) {
2778 if (tile8_id < 0 ||
2779 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2780 return absl::InvalidArgumentError("Invalid tile8 ID");
2781 }
2782
2783 if (!current_gfx_individual_[tile8_id].is_active()) {
2784 return absl::OkStatus(); // Skip inactive tiles
2785 }
2786
2787 if (!rom_) {
2788 return absl::FailedPreconditionError("ROM not set");
2789 }
2790
2791 // Use the complete 256-color overworld palette for consistency
2792 gfx::SnesPalette display_palette;
2793 if (overworld_palette_.size() >= 256) {
2794 display_palette = overworld_palette_;
2795 } else if (palette_.size() >= 256) {
2796 display_palette = palette_;
2797 } else if (game_data()) {
2798 // Fallback to GameData palette
2799 const auto& palette_groups = game_data()->palette_groups;
2800 if (palette_groups.overworld_main.size() > 0) {
2801 display_palette = palette_groups.overworld_main[0];
2802 } else {
2803 return absl::FailedPreconditionError("No overworld palette available");
2804 }
2805 }
2806
2807 // Validate current_palette_ index
2808 if (current_palette_ < 0 || current_palette_ >= 8) {
2809 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
2810 current_palette_ = 0;
2811 }
2812
2813 const int sheet_index = GetSheetIndexForTile8(tile8_id);
2814 const int palette_slot =
2815 GetActualPaletteSlot(static_cast<int>(current_palette_), sheet_index);
2816
2817 // Apply palette based on whether pixel values retain CGRAM row offsets.
2818 if (!display_palette.empty()) {
2820 if (palette_slot >= 0 &&
2821 static_cast<size_t>(palette_slot + 16) <= display_palette.size()) {
2822 current_gfx_individual_[tile8_id].SetPaletteWithTransparent(
2823 display_palette, static_cast<size_t>(palette_slot + 1), 15);
2824 } else {
2825 current_gfx_individual_[tile8_id].SetPaletteWithTransparent(
2826 display_palette, 1, 15);
2827 }
2828 } else {
2829 current_gfx_individual_[tile8_id].SetPalette(display_palette);
2830 }
2831 }
2832
2833 current_gfx_individual_[tile8_id].set_modified(true);
2834 // Queue texture update via Arena's deferred system
2836 &current_gfx_individual_[tile8_id]);
2837
2838 util::logf("Updated tile8 %d with palette slot %d (palette size: %zu colors)",
2839 tile8_id, current_palette_, display_palette.size());
2840
2841 return absl::OkStatus();
2842}
2843
2845 if (!rom_) {
2846 return absl::FailedPreconditionError("ROM not set");
2847 }
2848
2849 // Validate current_palette_ index
2850 if (current_palette_ < 0 || current_palette_ >= 8) {
2851 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
2852 current_palette_ = 0;
2853 }
2854
2855 // CRITICAL FIX: Use the complete overworld palette for proper color
2856 // coordination
2857 gfx::SnesPalette display_palette;
2858
2859 if (overworld_palette_.size() >= 256) {
2860 // Use the complete 256-color palette from overworld editor
2861 display_palette = overworld_palette_;
2862 util::logf("Using complete overworld palette with %zu colors",
2863 display_palette.size());
2864 } else if (palette_.size() >= 256) {
2865 // Fallback to the old palette_ if it's complete
2866 display_palette = palette_;
2867 util::logf("Using fallback complete palette with %zu colors",
2868 display_palette.size());
2869 } else if (game_data()) {
2870 // Last resort: Use GameData palette groups
2871 const auto& palette_groups = game_data()->palette_groups;
2872 if (palette_groups.overworld_main.size() > 0) {
2873 display_palette = palette_groups.overworld_main[0];
2874 util::logf("Warning: Using ROM main palette with %zu colors",
2875 display_palette.size());
2876 } else {
2877 return absl::FailedPreconditionError("No palette available");
2878 }
2879 }
2880
2881 if (display_palette.empty()) {
2882 return absl::FailedPreconditionError("Display palette empty");
2883 }
2884
2885 // The source bitmap (current_gfx_bmp_) contains 8bpp indexed pixel data.
2886 // If palette offsets are preserved, apply the full CGRAM palette. Otherwise,
2887 // remap the palette to the user-selected row.
2890 gfx::SnesPalette remapped_palette =
2892 current_gfx_bmp_.SetPalette(remapped_palette);
2893 util::logf("Applied remapped palette (button %d) to source bitmap",
2895 } else {
2896 current_gfx_bmp_.SetPalette(display_palette);
2897 util::logf("Applied full CGRAM palette to source bitmap");
2898 }
2899
2901 // Queue texture update via Arena's deferred system
2904 }
2905
2906 // Update current tile16 being edited
2909 // Use sheet-aware palette slot for current tile16
2910 // SNES palette offset fix: add 1 to skip transparent color slot
2911 int palette_slot = GetActualPaletteSlotForCurrentTile16();
2912
2913 if (palette_slot >= 0 &&
2914 static_cast<size_t>(palette_slot + 16) <= display_palette.size()) {
2916 display_palette, static_cast<size_t>(palette_slot + 1), 15);
2917 } else {
2918 current_tile16_bmp_.SetPaletteWithTransparent(display_palette, 1, 15);
2919 }
2920 } else {
2921 current_tile16_bmp_.SetPalette(display_palette);
2922 }
2923
2927 }
2928
2929 // Update individual tile8 graphics
2930 for (size_t i = 0; i < current_gfx_individual_.size(); ++i) {
2931 if (!current_gfx_individual_[i].is_active()) {
2932 continue;
2933 }
2934
2936 // Calculate per-tile8 palette slot based on which sheet it belongs to
2937 int sheet_index = GetSheetIndexForTile8(static_cast<int>(i));
2938 int palette_slot = GetActualPaletteSlot(current_palette_, sheet_index);
2939
2940 // Apply sub-palette with transparent color 0
2941 if (palette_slot >= 0 &&
2942 static_cast<size_t>(palette_slot + 16) <= display_palette.size()) {
2943 current_gfx_individual_[i].SetPaletteWithTransparent(
2944 display_palette, static_cast<size_t>(palette_slot + 1), 15);
2945 } else {
2946 // Fallback to slot 1 if computed slot exceeds palette bounds
2947 current_gfx_individual_[i].SetPaletteWithTransparent(display_palette, 1,
2948 15);
2949 }
2950 } else {
2951 current_gfx_individual_[i].SetPalette(display_palette);
2952 }
2953
2954 current_gfx_individual_[i].set_modified(true);
2955 // Queue texture update via Arena's deferred system
2958 }
2959
2960 util::logf(
2961 "Successfully refreshed all palettes in tile16 editor with palette %d",
2963 return absl::OkStatus();
2964}
2965
2967 util::logf("=== TILE8 SOURCE DATA ANALYSIS ===");
2968
2969 // Analyze current_gfx_bmp_
2970 util::logf("current_gfx_bmp_:");
2971 util::logf(" - Active: %s", current_gfx_bmp_.is_active() ? "yes" : "no");
2972 util::logf(" - Size: %dx%d", current_gfx_bmp_.width(),
2974 util::logf(" - Depth: %d bpp", current_gfx_bmp_.depth());
2975 util::logf(" - Data size: %zu bytes", current_gfx_bmp_.size());
2976 util::logf(" - Palette size: %zu colors", current_gfx_bmp_.palette().size());
2977
2978 // Analyze pixel value distribution in first 64 pixels (first tile8)
2979 if (current_gfx_bmp_.data() && current_gfx_bmp_.size() >= 64) {
2980 std::map<uint8_t, int> pixel_counts;
2981 for (size_t i = 0; i < 64; ++i) {
2982 uint8_t val = current_gfx_bmp_.data()[i];
2983 pixel_counts[val]++;
2984 }
2985 util::logf(" - First tile8 (Sheet 0) pixel distribution:");
2986 for (const auto& [val, count] : pixel_counts) {
2987 int row = GetEncodedPaletteRow(val);
2988 int col = val & 0x0F;
2989 util::logf(" Value 0x%02X (%3d) = Row %d, Col %d: %d pixels", val, val,
2990 row, col, count);
2991 }
2992
2993 // Check if values are in expected 4bpp range
2994 bool all_4bpp = true;
2995 for (const auto& [val, count] : pixel_counts) {
2996 if (val > 15) {
2997 all_4bpp = false;
2998 break;
2999 }
3000 }
3001 util::logf(" - Values in raw 4bpp range (0-15): %s",
3002 all_4bpp ? "yes" : "NO (pre-encoded)");
3003
3004 // Show what the remapping does
3005 util::logf(" - Palette remapping for viewing:");
3006 util::logf(" Selected palette: %d (row %d)", current_palette_,
3008 util::logf(" Pixels are remapped: (value & 0x0F) + (selected_row * 16)");
3009 }
3010
3011 // Analyze current_gfx_individual_
3012 util::logf("current_gfx_individual_:");
3013 util::logf(" - Count: %zu tiles", current_gfx_individual_.size());
3014
3015 if (!current_gfx_individual_.empty() &&
3016 current_gfx_individual_[0].is_active()) {
3017 const auto& first_tile = current_gfx_individual_[0];
3018 util::logf(" - First tile:");
3019 util::logf(" - Size: %dx%d", first_tile.width(), first_tile.height());
3020 util::logf(" - Depth: %d bpp", first_tile.depth());
3021 util::logf(" - Palette size: %zu colors", first_tile.palette().size());
3022
3023 if (first_tile.data() && first_tile.size() >= 64) {
3024 std::map<uint8_t, int> pixel_counts;
3025 for (size_t i = 0; i < 64; ++i) {
3026 uint8_t val = first_tile.data()[i];
3027 pixel_counts[val]++;
3028 }
3029 util::logf(" - Pixel distribution:");
3030 for (const auto& [val, count] : pixel_counts) {
3031 util::logf(" Value 0x%02X (%3d): %d pixels", val, val, count);
3032 }
3033 }
3034 }
3035
3036 // Analyze palette state
3037 util::logf("Palette state:");
3038 util::logf(" - current_palette_: %d", current_palette_);
3039 util::logf(" - overworld_palette_ size: %zu", overworld_palette_.size());
3040 util::logf(" - palette_ size: %zu", palette_.size());
3041
3042 // Calculate expected palette slot
3043 int palette_slot = GetActualPaletteSlot(current_palette_, 0);
3044 util::logf(" - GetActualPaletteSlot(%d, 0) = %d", current_palette_,
3045 palette_slot);
3046 util::logf(" - Expected palette offset for SetPaletteWithTransparent: %d",
3047 palette_slot + 1);
3048
3049 // Show first 16 colors of the overworld palette
3050 if (overworld_palette_.size() >= 16) {
3051 util::logf(" - First 16 palette colors (row 0):");
3052 for (int i = 0; i < 16; ++i) {
3053 auto color = overworld_palette_[i];
3054 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3055 static_cast<int>(color.rgb().x),
3056 static_cast<int>(color.rgb().y),
3057 static_cast<int>(color.rgb().z));
3058 }
3059 }
3060
3061 // Show colors at the selected palette slot
3062 if (overworld_palette_.size() >= static_cast<size_t>(palette_slot + 16)) {
3063 util::logf(" - Colors at palette slot %d (row %d):", palette_slot,
3064 palette_slot / 16);
3065 for (int i = 0; i < 16; ++i) {
3066 auto color = overworld_palette_[palette_slot + i];
3067 util::logf(" [%2d] SNES: 0x%04X RGB: (%d,%d,%d)", i, color.snes(),
3068 static_cast<int>(color.rgb().x),
3069 static_cast<int>(color.rgb().y),
3070 static_cast<int>(color.rgb().z));
3071 }
3072 }
3073
3074 util::logf("=== END ANALYSIS ===");
3075}
3076
3079 if (Begin("Advanced Palette Settings", &show_palette_settings_)) {
3080 Text("Pixel Normalization & Color Correction:");
3081
3082 int mask_value = static_cast<int>(palette_normalization_mask_);
3083 if (SliderInt("Normalization Mask", &mask_value, 1, 255, "0x%02X")) {
3084 palette_normalization_mask_ = static_cast<uint8_t>(mask_value);
3085 }
3086
3087 Checkbox("Auto Normalize Pixels", &auto_normalize_pixels_);
3088
3089 if (Button("Apply to All Graphics")) {
3090 auto reload_result = LoadTile8();
3091 if (!reload_result.ok()) {
3092 Text("Error: %s", reload_result.message().data());
3093 }
3094 }
3095
3096 SameLine();
3097 if (Button("Reset Defaults")) {
3100 auto reload_result = LoadTile8();
3101 (void)reload_result; // Suppress warning
3102 }
3103
3104 Separator();
3105 Text("Current State:");
3106 static constexpr std::array<const char*, 7> palette_group_names = {
3107 "OW Main", "OW Aux", "OW Anim", "Dungeon",
3108 "Sprites", "Armor", "Sword"};
3109 Text("Palette Group: %d (%s)", current_palette_group_,
3111 ? palette_group_names[current_palette_group_]
3112 : "Unknown");
3113 Text("Current Palette: %d", current_palette_);
3114
3115 Separator();
3116 Text("Sheet-Specific Fixes:");
3117
3118 // Sheet-specific palette fixes
3119 static bool fix_sheet_0 = true;
3120 static bool fix_sprite_sheets = true;
3121 static bool use_transparent_for_terrain = false;
3122
3123 if (Checkbox("Fix Sheet 0 (Trees)", &fix_sheet_0)) {
3124 auto reload_result = LoadTile8();
3125 if (!reload_result.ok()) {
3126 Text("Error reloading: %s", reload_result.message().data());
3127 }
3128 }
3129 HOVER_HINT(
3130 "Use direct palette for sheet 0 instead of transparent palette");
3131
3132 if (Checkbox("Fix Sprite Sheets", &fix_sprite_sheets)) {
3133 auto reload_result = LoadTile8();
3134 if (!reload_result.ok()) {
3135 Text("Error reloading: %s", reload_result.message().data());
3136 }
3137 }
3138 HOVER_HINT("Use direct palette for sprite graphics sheets");
3139
3140 if (Checkbox("Transparent for Terrain", &use_transparent_for_terrain)) {
3141 auto reload_result = LoadTile8();
3142 if (!reload_result.ok()) {
3143 Text("Error reloading: %s", reload_result.message().data());
3144 }
3145 }
3146 HOVER_HINT("Force transparent palette for terrain graphics");
3147
3148 Separator();
3149 Text("Color Analysis:");
3150 if (current_tile8_ >= 0 &&
3151 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
3153 Text("Selected Tile8 Analysis:");
3154 const auto& tile_data =
3156 std::map<uint8_t, int> pixel_counts;
3157 for (uint8_t pixel : tile_data) {
3158 pixel_counts[pixel & 0x0F]++; // Normalize to 4-bit
3159 }
3160
3161 Text("Pixel Value Distribution:");
3162 for (const auto& pair : pixel_counts) {
3163 int value = pair.first;
3164 int count = pair.second;
3165 Text(" Value %d (0x%X): %d pixels", value, value, count);
3166 }
3167
3168 Text("Palette Colors Used:");
3169 const auto& palette = current_gfx_individual_[current_tile8_].palette();
3170 for (const auto& pair : pixel_counts) {
3171 int value = pair.first;
3172 int count = pair.second;
3173 if (value < static_cast<int>(palette.size())) {
3174 auto color = palette[value];
3175 ImVec4 display_color = color.rgb();
3176 ImGui::ColorButton(("##analysis" + std::to_string(value)).c_str(),
3177 display_color, ImGuiColorEditFlags_NoTooltip,
3178 ImVec2(16, 16));
3179 if (ImGui::IsItemHovered()) {
3180 ImGui::SetTooltip("Index %d: 0x%04X (%d pixels)", value,
3181 color.snes(), count);
3182 }
3183 if (value % 8 != 7)
3184 ImGui::SameLine();
3185 }
3186 }
3187 }
3188
3189 // Enhanced ROM Palette Management Section
3190 Separator();
3191 if (CollapsingHeader("ROM Palette Manager") && rom_) {
3192 Text("Experimental ROM Palette Selection:");
3193 HOVER_HINT(
3194 "Use ROM palettes to experiment with different color schemes");
3195
3196 if (Button("Open Enhanced Palette Editor")) {
3198 }
3199 SameLine();
3200 if (Button("Show Color Analysis")) {
3202 }
3203
3204 // Quick palette application
3205 static int quick_group = 0;
3206 static int quick_index = 0;
3207
3208 SliderInt("ROM Group", &quick_group, 0, 6);
3209 SliderInt("Palette Index", &quick_index, 0, 7);
3210
3211 if (Button("Apply to Tile8 Source")) {
3212 if (tile8_source_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3213 util::logf("Applied ROM palette group %d, index %d to Tile8 source",
3214 quick_group, quick_index);
3215 }
3216 }
3217 SameLine();
3218 if (Button("Apply to Tile16 Editor")) {
3219 if (tile16_edit_canvas_.ApplyROMPalette(quick_group, quick_index)) {
3220 util::logf(
3221 "Applied ROM palette group %d, index %d to Tile16 editor",
3222 quick_group, quick_index);
3223 }
3224 }
3225 }
3226 }
3227 End();
3228 }
3229}
3230
3232 Text("Layout Scratch:");
3233 for (int i = 0; i < 4; ++i) {
3234 ImGui::PushID(i);
3235 std::string slot_name = "S" + std::to_string(i + 1);
3236
3237 if (Button((slot_name + " Save").c_str(), ImVec2(70, 20))) {
3239 }
3240 SameLine();
3241
3242 bool can_load = layout_scratch_[i].in_use;
3243 if (!can_load) {
3244 ImGui::BeginDisabled();
3245 }
3246 if (Button((slot_name + " Load").c_str(), ImVec2(70, 20)) && can_load) {
3248 }
3249 if (!can_load) {
3250 ImGui::EndDisabled();
3251 }
3252 SameLine();
3253 TextDisabled("%s", layout_scratch_[i].name.c_str());
3254 ImGui::PopID();
3255 }
3256}
3257
3259 if (slot < 0 || slot >= 4) {
3260 return absl::InvalidArgumentError("Invalid scratch slot");
3261 }
3262
3263 int total_tiles = ComputeTile16Count(tile16_blockset_);
3264 if (total_tiles <= 0) {
3265 return absl::FailedPreconditionError("Tile16 blockset is not available");
3266 }
3267
3268 const int start_tile = std::clamp(current_tile16_, 0, total_tiles - 1);
3269 for (int y = 0; y < 8; ++y) {
3270 for (int x = 0; x < 8; ++x) {
3271 const int tile_id = start_tile + (y * 8) + x;
3272 layout_scratch_[slot].tile_layout[y][x] =
3273 (tile_id < total_tiles) ? tile_id : -1;
3274 }
3275 }
3276
3277 layout_scratch_[slot].in_use = true;
3278 layout_scratch_[slot].name =
3279 absl::StrFormat("From %03X", static_cast<uint16_t>(start_tile));
3280
3281 return absl::OkStatus();
3282}
3283
3285 if (slot < 0 || slot >= 4) {
3286 return absl::InvalidArgumentError("Invalid scratch slot");
3287 }
3288
3289 if (!layout_scratch_[slot].in_use) {
3290 return absl::FailedPreconditionError("Scratch slot is empty");
3291 }
3292
3293 const int first_tile = layout_scratch_[slot].tile_layout[0][0];
3294 if (first_tile < 0) {
3295 return absl::FailedPreconditionError("Scratch slot has no valid tile data");
3296 }
3297
3298 RETURN_IF_ERROR(SetCurrentTile(first_tile));
3300 scroll_to_current_ = true;
3301
3302 selected_tiles_.clear();
3303 selected_tiles_.reserve(64);
3304 for (int y = 0; y < 8; ++y) {
3305 for (int x = 0; x < 8; ++x) {
3306 const int tile_id = layout_scratch_[slot].tile_layout[y][x];
3307 if (tile_id >= 0) {
3308 selected_tiles_.push_back(tile_id);
3309 }
3310 }
3311 }
3312 selection_start_tile_ = first_tile;
3313
3314 return absl::OkStatus();
3315}
3316
3318 if (ImGui::BeginPopupModal("ManualTile8Editor", nullptr,
3319 ImGuiWindowFlags_AlwaysAutoResize)) {
3320 ImGui::Text("Manual Tile8 Configuration for Tile16 %02X", current_tile16_);
3321 ImGui::Separator();
3322
3323 auto* tile_data = GetCurrentTile16Data();
3324 if (tile_data) {
3325 ImGui::Text("Current Tile16 ROM Data:");
3326
3327 // Display and edit each quadrant using TileInfo structure
3328 const char* quadrant_names[] = {"Top-Left", "Top-Right", "Bottom-Left",
3329 "Bottom-Right"};
3330
3331 for (int q = 0; q < 4; q++) {
3332 ImGui::Text("%s Quadrant:", quadrant_names[q]);
3333
3334 // Get the current TileInfo for this quadrant
3335 gfx::TileInfo* tile_info = nullptr;
3336 switch (q) {
3337 case 0:
3338 tile_info = &tile_data->tile0_;
3339 break;
3340 case 1:
3341 tile_info = &tile_data->tile1_;
3342 break;
3343 case 2:
3344 tile_info = &tile_data->tile2_;
3345 break;
3346 case 3:
3347 tile_info = &tile_data->tile3_;
3348 break;
3349 }
3350
3351 if (tile_info) {
3352 // Editable inputs for TileInfo components
3353 ImGui::PushID(q);
3354
3355 int tile_id_int = static_cast<int>(tile_info->id_);
3356 if (ImGui::InputInt("Tile8 ID", &tile_id_int, 1, 10)) {
3357 tile_info->id_ =
3358 static_cast<uint16_t>(std::max(0, std::min(tile_id_int, 1023)));
3359 }
3360
3361 int palette_int = static_cast<int>(tile_info->palette_);
3362 if (ImGui::SliderInt("Palette", &palette_int, 0, 7)) {
3363 tile_info->palette_ = static_cast<uint8_t>(palette_int);
3364 }
3365
3366 ImGui::Checkbox("X Flip", &tile_info->horizontal_mirror_);
3367 ImGui::SameLine();
3368 ImGui::Checkbox("Y Flip", &tile_info->vertical_mirror_);
3369 ImGui::SameLine();
3370 ImGui::Checkbox("Priority", &tile_info->over_);
3371
3372 if (ImGui::Button("Apply to Graphics")) {
3373 SyncTilesInfoArray(tile_data);
3374
3375 auto update_result = UpdateROMTile16Data();
3376 if (!update_result.ok()) {
3377 ImGui::Text("Error: %s", update_result.message().data());
3378 }
3379
3380 auto refresh_result = SetCurrentTile(current_tile16_);
3381 if (!refresh_result.ok()) {
3382 ImGui::Text("Refresh Error: %s", refresh_result.message().data());
3383 }
3384 }
3385
3386 ImGui::PopID();
3387 }
3388
3389 if (q < 3)
3390 ImGui::Separator();
3391 }
3392
3393 ImGui::Separator();
3394 if (ImGui::Button("Apply All Changes")) {
3395 auto update_result = UpdateROMTile16Data();
3396 if (!update_result.ok()) {
3397 ImGui::Text("Update Error: %s", update_result.message().data());
3398 }
3399
3400 auto save_result = SaveTile16ToROM();
3401 if (!save_result.ok()) {
3402 ImGui::Text("Save Error: %s", save_result.message().data());
3403 }
3404 }
3405 ImGui::SameLine();
3406 if (ImGui::Button("Refresh Display")) {
3407 auto refresh_result = SetCurrentTile(current_tile16_);
3408 if (!refresh_result.ok()) {
3409 ImGui::Text("Refresh Error: %s", refresh_result.message().data());
3410 }
3411 }
3412
3413 } else {
3414 ImGui::Text("Tile16 data not accessible");
3415 ImGui::Text("Current tile16: %d", current_tile16_);
3416 if (rom_) {
3417 ImGui::Text("Valid range: 0-4095 (4096 total tiles)");
3418 }
3419 }
3420
3421 ImGui::Separator();
3422 if (ImGui::Button("Close")) {
3423 ImGui::CloseCurrentPopup();
3424 }
3425
3426 ImGui::EndPopup();
3427 }
3428}
3429
3431 // Skip if live preview is disabled
3432 if (!live_preview_enabled_) {
3433 return absl::OkStatus();
3434 }
3435
3436 // Check if preview needs updating
3437 if (!preview_dirty_) {
3438 return absl::OkStatus();
3439 }
3440
3441 // Ensure we have valid tile data
3443 preview_dirty_ = false;
3444 return absl::OkStatus();
3445 }
3446
3447 // Update the preview bitmap from current tile16
3448 if (!preview_tile16_.is_active()) {
3450 } else {
3451 // Recreate with updated data
3453 }
3454
3455 // Apply the current palette
3456 if (game_data()) {
3457 const auto& ow_main_pal_group = game_data()->palette_groups.overworld_main;
3458 const gfx::SnesPalette* display_palette = nullptr;
3459 if (overworld_palette_.size() >= 256) {
3460 display_palette = &overworld_palette_;
3461 } else if (palette_.size() >= 256) {
3462 display_palette = &palette_;
3463 } else if (ow_main_pal_group.size() > 0) {
3464 display_palette = &ow_main_pal_group[0];
3465 }
3466
3467 if (display_palette && !display_palette->empty()) {
3469 ow_main_pal_group.size() > current_palette_) {
3470 preview_tile16_.SetPaletteWithTransparent(ow_main_pal_group[0],
3472 } else {
3473 preview_tile16_.SetPalette(*display_palette);
3474 }
3475 }
3476 }
3477
3478 // Queue texture update
3481
3482 // Clear the dirty flag
3483 preview_dirty_ = false;
3484
3485 return absl::OkStatus();
3486}
3487
3489 if (!ImGui::IsAnyItemActive()) {
3490 bool ctrl_held = ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
3491 ImGui::IsKeyDown(ImGuiKey_RightCtrl);
3492
3493 // Editing shortcuts (only fire without Ctrl to avoid conflicts)
3494 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
3495 status_ = ClearTile16();
3496 }
3497 if (ImGui::IsKeyPressed(ImGuiKey_H) && !ctrl_held) {
3499 }
3500 if (ImGui::IsKeyPressed(ImGuiKey_V) && !ctrl_held) {
3502 }
3503 if (ImGui::IsKeyPressed(ImGuiKey_R) && !ctrl_held) {
3505 }
3506 if (ImGui::IsKeyPressed(ImGuiKey_F) && !ctrl_held) {
3507 if (current_tile8_ >= 0 &&
3508 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
3510 }
3511 }
3512
3513 // Palette shortcuts
3514 if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
3515 status_ = CyclePalette(false);
3516 }
3517 if (ImGui::IsKeyPressed(ImGuiKey_E)) {
3518 status_ = CyclePalette(true);
3519 }
3520
3521 // Palette number shortcuts (1-8) - just set and refresh, don't cycle
3522 for (int i = 0; i < 8; ++i) {
3523 if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_1 + i))) {
3524 current_palette_ = i;
3526 }
3527 }
3528
3529 // Ctrl-modified shortcuts
3530 if (ctrl_held) {
3531 if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
3532 status_ = Undo();
3533 }
3534 if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
3535 status_ = Redo();
3536 }
3537 if (ImGui::IsKeyPressed(ImGuiKey_C)) {
3539 }
3540 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
3542 }
3543 if (ImGui::IsKeyPressed(ImGuiKey_S)) {
3544 if (ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
3545 ImGui::IsKeyDown(ImGuiKey_RightShift)) {
3547 } else {
3549 }
3550 }
3551 }
3552 }
3553}
3554
3557 return;
3558 }
3559
3560 constexpr int kTilesPerRow = 8;
3561 int tile_x = (tile_id % kTilesPerRow) * kTile16Size;
3562 int tile_y = (tile_id / kTilesPerRow) * kTile16Size;
3563
3564 for (int ty = 0; ty < kTile16Size; ++ty) {
3565 for (int tx = 0; tx < kTile16Size; ++tx) {
3566 int src_index = ty * kTile16Size + tx;
3567 int dst_index =
3568 (tile_y + ty) * tile16_blockset_->atlas.width() + (tile_x + tx);
3569
3570 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
3571 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
3573 dst_index, current_tile16_bmp_.data()[src_index]);
3574 }
3575 }
3576 }
3577
3581}
3582
3583} // namespace editor
3584} // 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 SaveTile16ToScratchSpace(int slot)
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 FillTile16WithTile8(int tile8_id)
gfx::Tilemap * tile16_blockset_
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).
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()
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::SnesPalette overworld_palette_
absl::Status Initialize(const gfx::Bitmap &tile16_blockset_bmp, const gfx::Bitmap &current_gfx_bmp, std::array< uint8_t, 0x200 > &all_tiles_types)
static constexpr int kTilesPerPage
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.
absl::Status UpdateAsPanel()
Update the editor content without MenuBar (for EditorPanel usage)
absl::Status RefreshTile16Blockset()
Tile16ClipboardData clipboard_tile16_
absl::Status RefreshAllPalettes()
Refresh all tile8 palettes after a palette change.
void DrawPaletteSettings()
Draw palette settings UI.
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_
bool IsTile16Valid(int tile_id) const
gfx::Tile16 * GetCurrentTile16Data()
void MarkCurrentTileModified()
Mark the current tile as having pending changes.
std::vector< gfx::Bitmap > current_gfx_individual_
bool has_pending_changes() const
Check if any tiles have uncommitted changes.
absl::Status CommitChangesToOverworld()
Full commit workflow: ROM + blockset + notify parent.
absl::Status UpdateBlocksetBitmap()
void DiscardCurrentTileChanges()
Discard only the current tile's pending changes.
void RequestTileSwitch(int target_tile_id)
absl::Status DrawToCurrentTile16(ImVec2 pos, const gfx::Bitmap *source_tile=nullptr)
absl::Status PreviewPaletteChange(uint8_t palette_id)
absl::Status CopyTile16ToClipboard(int tile_id)
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_
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 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 WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:581
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
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 set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:853
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)
Tile composition of four 8x8 tiles.
Definition snes_tile.h:142
std::array< TileInfo, 4 > tiles_info
Definition snes_tile.h:148
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
gfx::TileInfo HorizontalFlipInfo(gfx::TileInfo info)
int ComputeTile16Count(const gfx::Tilemap *tile16_blockset)
gfx::TileInfo & TileInfoForQuadrant(gfx::Tile16 *tile, int quadrant)
gfx::TileInfo VerticalFlipInfo(gfx::TileInfo info)
constexpr int kTile16PixelCount
constexpr int kTile8PixelCount
constexpr float kTile8DisplayScale
constexpr int kTile8Size
constexpr int kTile16Size
constexpr int kNumScratchSlots
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:270
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:640
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
constexpr int kNumTile16Individual
Definition overworld.h:239
constexpr uint32_t kTile16Ptr
Definition game_data.h:57
#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
Tilemap structure for SNES tile-based graphics management.
Definition tilemap.h:118
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:89