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
5#include "absl/status/status.h"
6#include "app/gfx/arena.h"
7#include "app/gfx/bitmap.h"
11#include "app/gui/canvas.h"
12#include "app/gui/input.h"
13#include "app/gui/style.h"
14#include "app/rom.h"
16#include "imgui/imgui.h"
17#include "util/hex.h"
18#include "util/log.h"
19#include "util/macro.h"
20
21namespace yaze {
22namespace editor {
23
24using namespace ImGui;
25
27 const gfx::Bitmap& tile16_blockset_bmp, const gfx::Bitmap& current_gfx_bmp,
28 std::array<uint8_t, 0x200>& all_tiles_types) {
29 all_tiles_types_ = all_tiles_types;
30
31 // Copy the graphics bitmap (palette will be set later by overworld editor)
32 current_gfx_bmp_.Create(current_gfx_bmp.width(), current_gfx_bmp.height(),
33 current_gfx_bmp.depth(), current_gfx_bmp.vector());
34 current_gfx_bmp_.SetPalette(current_gfx_bmp.palette()); // Temporary palette
35 // TODO: Queue texture for later rendering.
36 // core::Renderer::Get().RenderBitmap(&current_gfx_bmp_);
37
38 // Copy the tile16 blockset bitmap
40 tile16_blockset_bmp.width(), tile16_blockset_bmp.height(),
41 tile16_blockset_bmp.depth(), tile16_blockset_bmp.vector());
42 tile16_blockset_bmp_.SetPalette(tile16_blockset_bmp.palette());
43 // TODO: Queue texture for later rendering.
44 // core::Renderer::Get().RenderBitmap(&tile16_blockset_bmp_);
45
46 // Note: LoadTile8() will be called after palette is set by overworld editor
47 // This ensures proper palette coordination from the start
48
49 // Initialize current tile16 bitmap - this will be set by SetCurrentTile
51 std::vector<uint8_t>(kTile16PixelCount, 0));
52 current_tile16_bmp_.SetPalette(tile16_blockset_bmp.palette());
53 // TODO: Queue texture for later rendering.
54 // core::Renderer::Get().RenderBitmap(&current_tile16_bmp_);
55
56 // Initialize enhanced canvas features with proper sizing
59
60 // Configure canvases with proper initialization
63
64 // Initialize enhanced palette editors if ROM is available
65 if (rom_) {
68 }
69
70 // Initialize the current tile16 properly from the blockset
71 if (tile16_blockset_) {
72 RETURN_IF_ERROR(SetCurrentTile(0)); // Start with tile 0
73 }
74
76
77 // Setup collision type labels for tile8 canvas
78 ImVector<std::string> tile16_names;
79 for (int i = 0; i < 0x200; ++i) {
80 std::string str = util::HexByte(all_tiles_types_[i]);
81 tile16_names.push_back(str);
82 }
83 *tile8_source_canvas_.mutable_labels(0) = tile16_names;
85
86 // Setup tile info table
88 [&]() { Text("Tile16: %02X", current_tile16_); });
90 [&]() { Text("Tile8: %02X", current_tile8_); });
91 gui::AddTableColumn(tile_edit_table_, "##tile16Flip", [&]() {
92 Checkbox("X Flip", &x_flip);
93 Checkbox("Y Flip", &y_flip);
94 Checkbox("Priority", &priority_tile);
95 });
96
97 return absl::OkStatus();
98}
99
100absl::Status Tile16Editor::Update() {
102 return absl::InvalidArgumentError("Blockset not initialized, open a ROM.");
103 }
104
105 if (BeginMenuBar()) {
106 if (BeginMenu("View")) {
107 Checkbox("Show Collision Types",
109 EndMenu();
110 }
111
112 if (BeginMenu("Edit")) {
113 if (MenuItem("Copy Current Tile16", "Ctrl+C")) {
115 }
116 if (MenuItem("Paste to Current Tile16", "Ctrl+V")) {
118 }
119 EndMenu();
120 }
121
122 if (BeginMenu("File")) {
123 if (MenuItem("Save Changes to ROM", "Ctrl+S")) {
125 }
126 if (MenuItem("Commit to Blockset", "Ctrl+Shift+S")) {
128 }
129 Separator();
130 bool live_preview = live_preview_enabled_;
131 if (MenuItem("Live Preview", nullptr, &live_preview)) {
132 EnableLivePreview(live_preview);
133 }
134 EndMenu();
135 }
136
137 if (BeginMenu("Scratch Space")) {
138 for (int i = 0; i < 4; i++) {
139 std::string slot_name = "Slot " + std::to_string(i + 1);
140 if (scratch_space_used_[i]) {
141 if (MenuItem((slot_name + " (Load)").c_str())) {
143 }
144 if (MenuItem((slot_name + " (Save)").c_str())) {
146 }
147 if (MenuItem((slot_name + " (Clear)").c_str())) {
149 }
150 } else {
151 if (MenuItem((slot_name + " (Save)").c_str())) {
153 }
154 }
155 if (i < 3)
156 Separator();
157 }
158 EndMenu();
159 }
160
161 EndMenuBar();
162 }
163
164 // About popup
165 if (BeginPopupModal("About Tile16 Editor", NULL,
166 ImGuiWindowFlags_AlwaysAutoResize)) {
167 Text("Tile16 Editor for Link to the Past");
168 Text("This editor allows you to edit 16x16 tiles used in the game.");
169 Text("Features:");
170 BulletText("Edit Tile16 graphics by placing 8x8 tiles in the quadrants");
171 BulletText("Copy and paste Tile16 graphics");
172 BulletText("Save and load Tile16 graphics to/from scratch space");
173 BulletText("Preview Tile16 graphics at a larger size");
174 Separator();
175 if (Button("Close")) {
176 CloseCurrentPopup();
177 }
178 EndPopup();
179 }
180
181 // Handle keyboard shortcuts
182 if (!ImGui::IsAnyItemActive()) {
183 // Editing shortcuts
184 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
186 }
187 if (ImGui::IsKeyPressed(ImGuiKey_H)) {
189 }
190 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
192 }
193 if (ImGui::IsKeyPressed(ImGuiKey_R)) {
195 }
196 if (ImGui::IsKeyPressed(ImGuiKey_F)) {
197 if (current_tile8_ >= 0 &&
198 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
200 }
201 }
202
203 // Palette shortcuts
204 if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
205 status_ = CyclePalette(false);
206 }
207 if (ImGui::IsKeyPressed(ImGuiKey_E)) {
208 status_ = CyclePalette(true);
209 }
210
211 // Palette number shortcuts (1-8)
212 for (int i = 0; i < 8; ++i) {
213 if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_1 + i))) {
215 status_ = CyclePalette(true);
216 status_ = CyclePalette(false);
218 }
219 }
220
221 // Undo/Redo with Ctrl
222 if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
223 ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
224 if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
225 status_ = Undo();
226 }
227 if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
228 status_ = Redo();
229 }
230 if (ImGui::IsKeyPressed(ImGuiKey_C)) {
232 }
233 if (ImGui::IsKeyPressed(ImGuiKey_V)) {
235 }
236 if (ImGui::IsKeyPressed(ImGuiKey_S)) {
237 if (ImGui::IsKeyDown(ImGuiKey_LeftShift) ||
238 ImGui::IsKeyDown(ImGuiKey_RightShift)) {
240 } else {
242 }
243 }
244 }
245 }
246
248
249 // Draw palette settings popup if enabled
251
252 return absl::OkStatus();
253}
254
256 // REFACTORED: Single unified table layout in UpdateTile16Edit
258}
259
262 gui::BeginChildWithScrollbar("##Tile16EditorBlocksetScrollRegion");
265
267
268 // CRITICAL FIX: Handle single clicks properly like the overworld editor
269 bool tile_selected = false;
270
271 // First, call DrawTileSelector for visual feedback
273
274 // Then check for single click to update tile selection
275 if (ImGui::IsItemClicked(ImGuiMouseButton_Left) &&
277 tile_selected = true;
278 }
279
280 if (tile_selected) {
281 // Get mouse position relative to canvas
282 const ImGuiIO& io = ImGui::GetIO();
283 ImVec2 canvas_pos = blockset_canvas_.zero_point();
284 ImVec2 mouse_pos =
285 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
286
287 // Calculate grid position (32x32 tiles in blockset)
288 int grid_x = static_cast<int>(mouse_pos.x / 32);
289 int grid_y = static_cast<int>(mouse_pos.y / 32);
290 int selected_tile = grid_x + grid_y * 8; // 8 tiles per row in blockset
291
292 if (selected_tile != current_tile16_ && selected_tile >= 0 &&
293 selected_tile < 512) {
294 RETURN_IF_ERROR(SetCurrentTile(selected_tile));
295 util::logf("Selected Tile16 from blockset: %d (grid: %d,%d)",
296 selected_tile, grid_x, grid_y);
297 }
298 }
303 EndChild();
304
305 return absl::OkStatus();
306}
307
308// ROM data access methods
310 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= 4096) {
311 return nullptr;
312 }
313
314 // Read the current tile16 data from ROM
315 auto tile_result = rom_->ReadTile16(current_tile16_);
316 if (!tile_result.ok()) {
317 return nullptr;
318 }
319
320 // Store in instance variable for proper persistence
321 current_tile16_data_ = tile_result.value();
322 return &current_tile16_data_;
323}
324
326 auto* tile_data = GetCurrentTile16Data();
327 if (!tile_data) {
328 return absl::FailedPreconditionError("Cannot access current tile16 data");
329 }
330
331 // Write the modified tile16 data back to ROM
333
334 util::logf("ROM Tile16 data written for tile %d", current_tile16_);
335 return absl::OkStatus();
336}
337
339 if (!tile16_blockset_) {
340 return absl::FailedPreconditionError("Tile16 blockset not available");
341 }
342
343 // CRITICAL FIX: Force regeneration without using problematic tile cache
344 // Directly mark atlas as modified to trigger regeneration from ROM data
345
346 // Mark atlas as modified to trigger regeneration
348
349 // Queue texture update via Arena's deferred system
352
353 util::logf("Tile16 blockset refreshed and regenerated");
354 return absl::OkStatus();
355}
356
358 gfx::ScopedTimer timer("tile16_blockset_update");
359
360 if (!tile16_blockset_) {
361 return absl::FailedPreconditionError("Tile16 blockset not initialized");
362 }
363
364 if (current_tile16_ < 0 || current_tile16_ >= zelda3::kNumTile16Individual) {
365 return absl::OutOfRangeError("Current tile16 ID out of range");
366 }
367
368 // Use optimized batch operations for better performance
370 // Calculate the position of this tile in the blockset bitmap
371 constexpr int kTilesPerRow =
372 8; // Standard SNES tile16 layout is 8 tiles per row
373 int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
374 int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
375
376 // Use dirty region tracking for efficient updates (region calculated but not used in current implementation)
377
378 // Copy pixel data from current tile to blockset bitmap using batch operations
379 for (int tile_y_offset = 0; tile_y_offset < kTile16Size; ++tile_y_offset) {
380 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
381 ++tile_x_offset) {
382 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
383 int dst_index =
384 (tile_y + tile_y_offset) * tile16_blockset_bmp_.width() +
385 (tile_x + tile_x_offset);
386
387 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
388 dst_index < static_cast<int>(tile16_blockset_bmp_.size())) {
389 uint8_t pixel_value = current_tile16_bmp_.data()[src_index];
390 tile16_blockset_bmp_.WriteToPixel(dst_index, pixel_value);
391 }
392 }
393 }
394
395 // Mark the blockset bitmap as modified and queue texture update
399
400 // Also update the tile16 blockset atlas if available
402 // Update the atlas with the new tile data
403 for (int tile_y_offset = 0; tile_y_offset < kTile16Size;
404 ++tile_y_offset) {
405 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
406 ++tile_x_offset) {
407 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
408 int dst_index =
409 (tile_y + tile_y_offset) * tile16_blockset_->atlas.width() +
410 (tile_x + tile_x_offset);
411
412 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
413 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
415 dst_index, current_tile16_bmp_.data()[src_index]);
416 }
417 }
418 }
419
423 }
424 }
425
426 return absl::OkStatus();
427}
428
430 // Get the current tile16 data from ROM
431 auto* tile_data = GetCurrentTile16Data();
432 if (!tile_data) {
433 return absl::FailedPreconditionError("Cannot access current tile16 data");
434 }
435
436 // Create a new 16x16 bitmap for the tile16
437 std::vector<uint8_t> tile16_pixels(kTile16PixelCount, 0);
438
439 // Process each quadrant (2x2 grid of 8x8 tiles)
440 for (int quadrant = 0; quadrant < 4; ++quadrant) {
441 gfx::TileInfo* tile_info = nullptr;
442 int quadrant_x = quadrant % 2;
443 int quadrant_y = quadrant / 2;
444
445 // Get the tile info for this quadrant
446 switch (quadrant) {
447 case 0:
448 tile_info = &tile_data->tile0_;
449 break;
450 case 1:
451 tile_info = &tile_data->tile1_;
452 break;
453 case 2:
454 tile_info = &tile_data->tile2_;
455 break;
456 case 3:
457 tile_info = &tile_data->tile3_;
458 break;
459 }
460
461 if (!tile_info)
462 continue;
463
464 // Get the tile8 ID and properties
465 int tile8_id = tile_info->id_;
466 bool x_flip = tile_info->horizontal_mirror_;
467 bool y_flip = tile_info->vertical_mirror_;
468 // Palette information stored in tile_info but applied via separate palette system
469
470 // Get the source tile8 bitmap
471 if (tile8_id >= 0 &&
472 tile8_id < static_cast<int>(current_gfx_individual_.size()) &&
473 current_gfx_individual_[tile8_id].is_active()) {
474
475 const auto& source_tile8 = current_gfx_individual_[tile8_id];
476
477 // Copy the 8x8 tile into the appropriate quadrant of the 16x16 tile
478 for (int ty = 0; ty < kTile8Size; ++ty) {
479 for (int tx = 0; tx < kTile8Size; ++tx) {
480 // Apply flip transformations
481 int src_x = x_flip ? (kTile8Size - 1 - tx) : tx;
482 int src_y = y_flip ? (kTile8Size - 1 - ty) : ty;
483 int src_index = src_y * kTile8Size + src_x;
484
485 // Calculate destination in tile16
486 int dst_x = (quadrant_x * kTile8Size) + tx;
487 int dst_y = (quadrant_y * kTile8Size) + ty;
488 int dst_index = dst_y * kTile16Size + dst_x;
489
490 // Copy pixel with bounds checking
491 if (src_index >= 0 &&
492 src_index < static_cast<int>(source_tile8.size()) &&
493 dst_index >= 0 && dst_index < kTile16PixelCount) {
494 uint8_t pixel = source_tile8.data()[src_index];
495 // Apply palette offset if needed
496 tile16_pixels[dst_index] = pixel;
497 }
498 }
499 }
500 }
501 }
502
503 // Update the current tile16 bitmap with the regenerated data
505
506 // Set the appropriate palette using the same system as overworld
507 if (overworld_palette_.size() >= 256) {
508 // Use complete 256-color palette (same as overworld system)
509 // The pixel data already contains correct color indices for the 256-color palette
511 } else {
512 // Fallback to ROM palette
513 const auto& ow_main_pal_group = rom()->palette_group().overworld_main;
514 if (ow_main_pal_group.size() > 0) {
515 current_tile16_bmp_.SetPalette(ow_main_pal_group[0]);
516 }
517 }
518
519 // Queue texture creation via Arena's deferred system
522
523 util::logf("Regenerated Tile16 bitmap for tile %d from ROM data",
525 return absl::OkStatus();
526}
527
528absl::Status Tile16Editor::DrawToCurrentTile16(ImVec2 pos,
529 const gfx::Bitmap* source_tile) {
530 constexpr int kTile8Size = 8;
531 constexpr int kTile16Size = 16;
532
533 // Save undo state before making changes
534 auto now = std::chrono::steady_clock::now();
535 auto time_since_last_edit =
536 std::chrono::duration_cast<std::chrono::milliseconds>(now -
538 .count();
539
540 if (time_since_last_edit > 100) { // 100ms threshold
542 last_edit_time_ = now;
543 }
544
545 // Validate inputs
546 if (current_tile8_ < 0 ||
547 current_tile8_ >= static_cast<int>(current_gfx_individual_.size())) {
548 return absl::OutOfRangeError(
549 absl::StrFormat("Invalid tile8 index: %d", current_tile8_));
550 }
551
552 if (!current_gfx_individual_[current_tile8_].is_active()) {
553 return absl::FailedPreconditionError("Source tile8 bitmap not active");
554 }
555
557 return absl::FailedPreconditionError("Target tile16 bitmap not active");
558 }
559
560 // Determine which quadrant was clicked - handle the 8x scale factor properly
561 int quadrant_x = (pos.x >= kTile8Size) ? 1 : 0;
562 int quadrant_y = (pos.y >= kTile8Size) ? 1 : 0;
563
564 int start_x = quadrant_x * kTile8Size;
565 int start_y = quadrant_y * kTile8Size;
566
567 // Get source tile8 data - use provided tile if available, otherwise use current tile8
568 const gfx::Bitmap* tile_to_use =
569 source_tile ? source_tile : &current_gfx_individual_[current_tile8_];
570 if (tile_to_use->size() < 64) {
571 return absl::FailedPreconditionError("Source tile data too small");
572 }
573
574 // Copy tile8 into tile16 quadrant with proper transformations
575 for (int tile_y = 0; tile_y < kTile8Size; ++tile_y) {
576 for (int tile_x = 0; tile_x < kTile8Size; ++tile_x) {
577 // Apply flip transformations to source coordinates only if using original tile
578 // If a pre-flipped tile is provided, use direct coordinates
579 int src_x;
580 int src_y;
581 if (source_tile) {
582 // Pre-flipped tile provided, use direct coordinates
583 src_x = tile_x;
584 src_y = tile_y;
585 } else {
586 // Original tile, apply flip transformations
587 src_x = x_flip ? (kTile8Size - 1 - tile_x) : tile_x;
588 src_y = y_flip ? (kTile8Size - 1 - tile_y) : tile_y;
589 }
590 int src_index = src_y * kTile8Size + src_x;
591
592 // Calculate destination in tile16
593 int dst_x = start_x + tile_x;
594 int dst_y = start_y + tile_y;
595 int dst_index = dst_y * kTile16Size + dst_x;
596
597 // Bounds check and copy pixel
598 if (src_index >= 0 && src_index < static_cast<int>(tile_to_use->size()) &&
599 dst_index >= 0 &&
600 dst_index < static_cast<int>(current_tile16_bmp_.size())) {
601
602 uint8_t pixel_value = tile_to_use->data()[src_index];
603
604 // Keep original pixel values - palette selection is handled by TileInfo metadata
605 // not by modifying pixel data directly
606 current_tile16_bmp_.WriteToPixel(dst_index, pixel_value);
607 }
608 }
609 }
610
611 // Mark the bitmap as modified and queue texture update
615
616 // Update ROM data when painting to tile16
617 auto* tile_data = GetCurrentTile16Data();
618 if (tile_data) {
619 // Update the quadrant's TileInfo based on current settings
620 int quadrant_index = quadrant_x + (quadrant_y * 2);
621 if (quadrant_index >= 0 && quadrant_index < 4) {
622
623 // Create new TileInfo with current settings
624 gfx::TileInfo new_tile_info(static_cast<uint16_t>(current_tile8_),
627
628 // Get pointer to the correct quadrant TileInfo
629 gfx::TileInfo* quadrant_tile = nullptr;
630 switch (quadrant_index) {
631 case 0:
632 quadrant_tile = &tile_data->tile0_;
633 break;
634 case 1:
635 quadrant_tile = &tile_data->tile1_;
636 break;
637 case 2:
638 quadrant_tile = &tile_data->tile2_;
639 break;
640 case 3:
641 quadrant_tile = &tile_data->tile3_;
642 break;
643 }
644
645 if (quadrant_tile && !(*quadrant_tile == new_tile_info)) {
646 *quadrant_tile = new_tile_info;
647 // Update the tiles_info array as well
648 tile_data->tiles_info[quadrant_index] = new_tile_info;
649
651 "Updated ROM Tile16 %d, quadrant %d: Tile8=%d, Pal=%d, XFlip=%d, "
652 "YFlip=%d, Priority=%d",
655 }
656 }
657 }
658
659 // CRITICAL FIX: Don't write to ROM immediately - only update local data
660 // ROM will be updated when user explicitly clicks "Save to ROM"
661
662 // Update the blockset bitmap displayed in the editor (local preview only)
664
665 // Update live preview if enabled (but don't save to ROM)
668 }
669
671 "Local tile16 changes made (not saved to ROM yet). Use 'Save to ROM' to "
672 "commit.");
673
674 return absl::OkStatus();
675}
676
678 static bool show_advanced_controls = false;
679 static bool show_debug_info = false;
680
681 // Modern header with improved styling
682 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 4));
683 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 4));
684
685 // Header section with better visual hierarchy
686 ImGui::BeginGroup();
687 ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tile16 Editor");
688 ImGui::SameLine();
689 ImGui::TextDisabled("ID: %02X", current_tile16_);
690 ImGui::SameLine();
691 ImGui::TextDisabled("|");
692 ImGui::SameLine();
693 ImGui::TextDisabled("Palette: %d", current_palette_);
694
695 // Show actual palette slot for debugging
696 if (show_debug_info) {
697 ImGui::SameLine();
698 int actual_slot = GetActualPaletteSlotForCurrentTile16();
699 ImGui::TextDisabled("(Slot: %d)", actual_slot);
700 }
701
702 ImGui::EndGroup();
703
704 // Modern button styling for controls
705 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 180);
706 if (ImGui::Button("Debug Info", ImVec2(80, 0))) {
707 show_debug_info = !show_debug_info;
708 }
709 ImGui::SameLine();
710 if (ImGui::Button("Advanced", ImVec2(80, 0))) {
711 show_advanced_controls = !show_advanced_controls;
712 }
713
714 ImGui::PopStyleVar(2);
715
716 ImGui::Separator();
717
718 // REFACTORED: Improved 3-column layout with better space utilization
719 if (ImGui::BeginTable("##Tile16EditLayout", 3,
720 ImGuiTableFlags_Resizable |
721 ImGuiTableFlags_BordersInnerV |
722 ImGuiTableFlags_SizingStretchProp)) {
723 ImGui::TableSetupColumn("Tile16 Blockset",
724 ImGuiTableColumnFlags_WidthStretch, 0.35f);
725 ImGui::TableSetupColumn("Tile8 Source", ImGuiTableColumnFlags_WidthStretch,
726 0.35f);
727 ImGui::TableSetupColumn("Editor & Controls",
728 ImGuiTableColumnFlags_WidthStretch, 0.30f);
729
730 ImGui::TableHeadersRow();
731 ImGui::TableNextRow();
732
733 // ========== COLUMN 1: Tile16 Blockset ==========
734 ImGui::TableNextColumn();
735 ImGui::BeginGroup();
736
737 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile16 Blockset");
738
739 // Blockset canvas with scrolling
740 if (BeginChild("##BlocksetScrollable",
741 ImVec2(0, ImGui::GetContentRegionAvail().y), true,
742 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
743
746
747 // Handle tile selection from blockset
748 bool tile_selected = false;
750
751 if (ImGui::IsItemClicked(ImGuiMouseButton_Left) &&
753 tile_selected = true;
754 }
755
756 if (tile_selected) {
757 const ImGuiIO& io = ImGui::GetIO();
758 ImVec2 canvas_pos = blockset_canvas_.zero_point();
759 ImVec2 mouse_pos =
760 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
761
762 int grid_x = static_cast<int>(mouse_pos.x /
764 int grid_y = static_cast<int>(mouse_pos.y /
766 int selected_tile = grid_x + grid_y * 8;
767
768 if (selected_tile != current_tile16_ && selected_tile >= 0) {
769 RETURN_IF_ERROR(SetCurrentTile(selected_tile));
770 util::logf("Selected Tile16 from blockset: %d", selected_tile);
771 }
772 }
773
777 }
778 EndChild();
779 ImGui::EndGroup();
780
781 // ========== COLUMN 2: Tile8 Source ==========
782 ImGui::TableNextColumn();
783 ImGui::BeginGroup();
784
785 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "Tile8 Source");
786
788
789 // Scrollable tile8 source
790 if (BeginChild("##Tile8SourceScrollable", ImVec2(0, 0), true,
791 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
792
795
796 // Tile8 selection with improved feedback
797 bool tile8_selected = false;
799
800 // Check for clicks properly
801 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
802 tile8_selected = true;
803 }
804
805 if (tile8_selected) {
806 const ImGuiIO& io = ImGui::GetIO();
807 ImVec2 canvas_pos = tile8_source_canvas_.zero_point();
808 ImVec2 mouse_pos =
809 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
810
811 // Account for dynamic zoom when calculating tile position
812 int tile_x = static_cast<int>(
813 mouse_pos.x /
814 (8 * 4)); // 8 pixel tile * 4x scale = 32 pixels per tile
815 int tile_y = static_cast<int>(mouse_pos.y / (8 * 4));
816
817 // Calculate tiles per row based on bitmap width
818 int tiles_per_row = current_gfx_bmp_.width() / 8;
819 int new_tile8 = tile_x + (tile_y * tiles_per_row);
820
821 if (new_tile8 != current_tile8_ && new_tile8 >= 0 &&
822 new_tile8 < static_cast<int>(current_gfx_individual_.size()) &&
823 current_gfx_individual_[new_tile8].is_active()) {
824 current_tile8_ = new_tile8;
826 util::logf("Selected Tile8: %d", current_tile8_);
827 }
828 }
829
833 }
834 EndChild();
835 ImGui::EndGroup();
836
837 // ========== COLUMN 3: Tile16 Editor + Controls ==========
838 TableNextColumn();
839 ImGui::BeginGroup();
840
841 // Fixed size container to prevent canvas expansion
842 if (ImGui::BeginChild("##Tile16FixedCanvas", ImVec2(90, 90), true,
843 ImGuiWindowFlags_NoScrollbar |
844 ImGuiWindowFlags_NoScrollWithMouse)) {
845
846 tile16_edit_canvas_.DrawBackground(ImVec2(64, 64));
848
849 // Draw current tile16 bitmap with dynamic zoom
852 }
853
854 // Handle tile8 painting with improved hover preview
855 if (current_tile8_ >= 0 &&
856 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
858
859 // Create a display tile that shows the current palette selection
860 gfx::Bitmap display_tile;
861
862 // Get the original pixel data (already has sheet offsets from ProcessGraphicsBuffer)
863 std::vector<uint8_t> tile_data =
865
866 // The pixel data already contains the correct indices for the 256-color palette
867 // We don't need to remap - just use it as-is
868 display_tile.Create(8, 8, 8, tile_data);
869
870 // Apply the complete 256-color palette
871 if (overworld_palette_.size() >= 256) {
872 display_tile.SetPalette(overworld_palette_);
873 } else {
874 display_tile.SetPalette(
876 }
877
878 // Apply flips if needed
879 if (x_flip || y_flip) {
880 auto& data = display_tile.mutable_data();
881
882 if (x_flip) {
883 for (int y = 0; y < 8; ++y) {
884 for (int x = 0; x < 4; ++x) {
885 std::swap(data[y * 8 + x], data[y * 8 + (7 - x)]);
886 }
887 }
888 }
889
890 if (y_flip) {
891 for (int y = 0; y < 4; ++y) {
892 for (int x = 0; x < 8; ++x) {
893 std::swap(data[y * 8 + x], data[(7 - y) * 8 + x]);
894 }
895 }
896 }
897 }
898
899 // Queue texture creation for display tile
902
903 // CRITICAL FIX: Handle tile painting with simple click instead of click+drag
904 // Draw the preview first
906 display_tile, 8, 4);
907
908 // Check for simple click to paint tile8 to tile16
909 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
910 const ImGuiIO& io = ImGui::GetIO();
911 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
912 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
913 io.MousePos.y - canvas_pos.y);
914
915 // Convert canvas coordinates to tile16 coordinates with dynamic zoom
916 int tile_x = static_cast<int>(mouse_pos.x /
917 4);
918 int tile_y = static_cast<int>(mouse_pos.y /
919 4);
920
921 // Clamp to valid range
922 tile_x = std::max(0, std::min(15, tile_x));
923 tile_y = std::max(0, std::min(15, tile_y));
924
925 util::logf("Tile16 canvas click: (%.2f, %.2f) -> Tile16: (%d, %d)",
926 mouse_pos.x, mouse_pos.y, tile_x, tile_y);
927
929 DrawToCurrentTile16(ImVec2(tile_x, tile_y), &display_tile));
930 }
931
932 // CRITICAL FIX: Right-click to pick tile8 from tile16
933 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
934 const ImGuiIO& io = ImGui::GetIO();
935 ImVec2 canvas_pos = tile16_edit_canvas_.zero_point();
936 ImVec2 mouse_pos = ImVec2(io.MousePos.x - canvas_pos.x,
937 io.MousePos.y - canvas_pos.y);
938
939 // Convert with dynamic zoom
940 int tile_x = static_cast<int>(mouse_pos.x /
941 4);
942 int tile_y = static_cast<int>(mouse_pos.y /
943 4);
944
945 // Clamp to valid range
946 tile_x = std::max(0, std::min(15, tile_x));
947 tile_y = std::max(0, std::min(15, tile_y));
948
949 RETURN_IF_ERROR(PickTile8FromTile16(ImVec2(tile_x, tile_y)));
950 util::logf("Right-clicked to pick tile8 from tile16 at (%d, %d)",
951 tile_x, tile_y);
952 }
953 }
954
955 tile16_edit_canvas_.DrawGrid(8.0F); // Scale grid with zoom
957 }
958 ImGui::EndChild();
959
960 Separator();
961
962 // === Compact Controls Section ===
963
964 // Tile8 info and preview
965 if (current_tile8_ >= 0 &&
966 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
968 Text("Tile8: %02X", current_tile8_);
969 SameLine();
970 auto* tile8_texture = current_gfx_individual_[current_tile8_].texture();
971 if (tile8_texture) {
972 ImGui::Image((ImTextureID)(intptr_t)tile8_texture, ImVec2(24, 24));
973 }
974 }
975
976 // Tile8 transform options in compact form
977 Checkbox("X Flip", &x_flip);
978 SameLine();
979 Checkbox("Y Flip", &y_flip);
980 SameLine();
981 Checkbox("Priority", &priority_tile);
982
983 Separator();
984
985 // Palette selector - more compact
986 Text("Palette:");
987 if (show_debug_info) {
988 SameLine();
989 int actual_slot = GetActualPaletteSlotForCurrentTile16();
990 ImGui::TextDisabled("(Slot %d)", actual_slot);
991 }
992
993 // Compact palette grid
994 ImGui::BeginGroup();
995 float available_width = ImGui::GetContentRegionAvail().x;
996 float button_size = std::min(32.0f, (available_width - 16.0f) / 4.0f);
997
998 for (int row = 0; row < 2; ++row) {
999 for (int col = 0; col < 4; ++col) {
1000 if (col > 0)
1001 ImGui::SameLine();
1002
1003 int i = row * 4 + col;
1004 bool is_current = (current_palette_ == i);
1005
1006 // Modern button styling with better visual hierarchy
1007 ImGui::PushID(i);
1008
1009 if (is_current) {
1010 ImGui::PushStyleColor(ImGuiCol_Button,
1011 ImVec4(0.2f, 0.7f, 0.3f, 1.0f));
1012 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1013 ImVec4(0.3f, 0.8f, 0.4f, 1.0f));
1014 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
1015 ImVec4(0.1f, 0.6f, 0.2f, 1.0f));
1016 } else {
1017 ImGui::PushStyleColor(ImGuiCol_Button,
1018 ImVec4(0.3f, 0.3f, 0.35f, 1.0f));
1019 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1020 ImVec4(0.4f, 0.4f, 0.45f, 1.0f));
1021 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
1022 ImVec4(0.25f, 0.25f, 0.3f, 1.0f));
1023 }
1024
1025 // Add border for better definition
1026 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
1027 ImGui::PushStyleColor(ImGuiCol_Border,
1028 is_current ? ImVec4(0.4f, 0.9f, 0.5f, 1.0f)
1029 : ImVec4(0.5f, 0.5f, 0.5f, 0.3f));
1030
1031 if (ImGui::Button(absl::StrFormat("%d", i).c_str(),
1032 ImVec2(button_size, button_size))) {
1033 if (current_palette_ != i) {
1034 current_palette_ = i;
1035 auto status = RefreshAllPalettes();
1036 if (!status.ok()) {
1037 util::logf("Failed to refresh palettes: %s",
1038 status.message().data());
1039 } else {
1040 util::logf("Palette successfully changed to %d",
1042 }
1043 }
1044 }
1045
1046 ImGui::PopStyleColor(4); // 3 button colors + 1 border color
1047 ImGui::PopStyleVar(1); // border size
1048 ImGui::PopID();
1049
1050 // Simplified tooltip
1051 if (ImGui::IsItemHovered()) {
1052 ImGui::BeginTooltip();
1053 if (show_debug_info) {
1054 ImGui::Text("Palette %d → Slots:", i);
1055 ImGui::Text(" S0,3,4: %d", GetActualPaletteSlot(i, 0));
1056 ImGui::Text(" S1,2: %d", GetActualPaletteSlot(i, 1));
1057 ImGui::Text(" S5,6: %d", GetActualPaletteSlot(i, 5));
1058 ImGui::Text(" S7: %d", GetActualPaletteSlot(i, 7));
1059 } else {
1060 ImGui::Text("Palette %d", i);
1061 if (is_current) {
1062 ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Active");
1063 }
1064 }
1065 ImGui::EndTooltip();
1066 }
1067 }
1068 }
1069 ImGui::EndGroup();
1070
1071 Separator();
1072
1073 // Compact action buttons
1074 if (Button("Clear", ImVec2(-1, 0))) {
1076 }
1077
1078 if (Button("Copy", ImVec2(-1, 0))) {
1080 }
1081
1082 if (Button("Paste", ImVec2(-1, 0))) {
1084 }
1085
1086 Separator();
1087
1088 // Save/Discard - full width buttons
1089 if (Button("Save Changes", ImVec2(-1, 0))) {
1091 }
1092 HOVER_HINT("Apply changes to overworld and regenerate blockset");
1093
1094 if (Button("Discard Changes", ImVec2(-1, 0))) {
1096 }
1097 HOVER_HINT("Reload tile16 from ROM, discarding local changes");
1098
1099 Separator();
1100
1101 bool can_undo = !undo_stack_.empty();
1102 if (!can_undo)
1103 BeginDisabled();
1104 if (Button("Undo", ImVec2(-1, 0))) {
1106 }
1107 if (!can_undo)
1108 EndDisabled();
1109
1110 // Advanced controls (collapsible)
1111 if (show_advanced_controls) {
1112 Separator();
1113 Text("Advanced:");
1114
1115 if (Button("Palette Settings", ImVec2(-1, 0))) {
1117 }
1118
1119 if (Button("Manual Edit", ImVec2(-1, 0))) {
1120 ImGui::OpenPopup("ManualTile8Editor");
1121 }
1122
1123 if (Button("Refresh Blockset", ImVec2(-1, 0))) {
1125 }
1126
1127 // Scratch space in compact form
1128 Text("Scratch:");
1130
1131 // Manual tile8 editor popup
1133 }
1134
1135 // Compact debug information panel
1136 if (show_debug_info) {
1137 Separator();
1138 Text("Debug:");
1139 ImGui::TextDisabled("T16:%02X T8:%d Pal:%d", current_tile16_,
1141
1142 if (current_tile8_ >= 0) {
1143 int sheet_index = GetSheetIndexForTile8(current_tile8_);
1144 int actual_slot = GetActualPaletteSlot(current_palette_, sheet_index);
1145 ImGui::TextDisabled("Sheet:%d Slot:%d", sheet_index, actual_slot);
1146 }
1147
1148 // Compact palette mapping table
1149 if (ImGui::CollapsingHeader("Palette Map",
1150 ImGuiTreeNodeFlags_DefaultOpen)) {
1151 ImGui::BeginChild("##PaletteMappingScroll", ImVec2(0, 120), true);
1152 if (ImGui::BeginTable("##PalMap", 3,
1153 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1154 ImGuiTableFlags_SizingFixedFit)) {
1155 ImGui::TableSetupColumn("Btn", ImGuiTableColumnFlags_WidthFixed, 30);
1156 ImGui::TableSetupColumn("S0,3-4", ImGuiTableColumnFlags_WidthFixed,
1157 50);
1158 ImGui::TableSetupColumn("S1-2", ImGuiTableColumnFlags_WidthFixed, 50);
1159 ImGui::TableHeadersRow();
1160
1161 for (int i = 0; i < 8; ++i) {
1162 ImGui::TableNextRow();
1163 ImGui::TableNextColumn();
1164 ImGui::Text("%d", i);
1165 ImGui::TableNextColumn();
1166 ImGui::Text("%d", GetActualPaletteSlot(i, 0));
1167 ImGui::TableNextColumn();
1168 ImGui::Text("%d", GetActualPaletteSlot(i, 1));
1169 }
1170 ImGui::EndTable();
1171 }
1172 ImGui::EndChild();
1173 }
1174
1175 // Color preview - compact
1176 if (ImGui::CollapsingHeader("Colors")) {
1177 if (overworld_palette_.size() >= 256) {
1178 int actual_slot = GetActualPaletteSlotForCurrentTile16();
1179 ImGui::Text("Slot %d:", actual_slot);
1180
1181 for (int i = 0;
1182 i < 8 &&
1183 (actual_slot + i) < static_cast<int>(overworld_palette_.size());
1184 ++i) {
1185 int color_index = actual_slot + i;
1186 auto color = overworld_palette_[color_index];
1187 ImVec4 display_color = color.rgb();
1188
1189 ImGui::ColorButton(absl::StrFormat("##c%d", i).c_str(),
1190 display_color, ImGuiColorEditFlags_NoTooltip,
1191 ImVec2(20, 20));
1192 if (ImGui::IsItemHovered()) {
1193 ImGui::SetTooltip("%d:0x%04X", color_index, color.snes());
1194 }
1195
1196 if ((i + 1) % 4 != 0)
1197 ImGui::SameLine();
1198 }
1199 }
1200 }
1201 }
1202
1203 ImGui::EndGroup();
1204 EndTable();
1205 }
1206
1207 // Draw palette settings and canvas popups
1209
1210 // Show canvas popup windows if opened from context menu
1217
1218 return absl::OkStatus();
1219}
1220
1222 if (!current_gfx_bmp_.is_active() || current_gfx_bmp_.data() == nullptr) {
1223 return absl::FailedPreconditionError(
1224 "Current graphics bitmap not initialized");
1225 }
1226
1228
1229 // Calculate how many 8x8 tiles we can fit based on the current graphics bitmap size
1230 // SNES graphics are typically 128 pixels wide (16 tiles of 8 pixels each)
1231 const int tiles_per_row = current_gfx_bmp_.width() / 8;
1232 const int total_rows = current_gfx_bmp_.height() / 8;
1233 const int total_tiles = tiles_per_row * total_rows;
1234
1235 current_gfx_individual_.reserve(total_tiles);
1236
1237 // Extract individual 8x8 tiles from the graphics bitmap
1238 for (int tile_y = 0; tile_y < total_rows; ++tile_y) {
1239 for (int tile_x = 0; tile_x < tiles_per_row; ++tile_x) {
1240 std::vector<uint8_t> tile_data(64); // 8x8 = 64 pixels
1241
1242 // Extract tile data from the main graphics bitmap
1243 for (int py = 0; py < 8; ++py) {
1244 for (int px = 0; px < 8; ++px) {
1245 int src_x = tile_x * 8 + px;
1246 int src_y = tile_y * 8 + py;
1247 int src_index = src_y * current_gfx_bmp_.width() + src_x;
1248 int dst_index = py * 8 + px;
1249
1250 if (src_index < static_cast<int>(current_gfx_bmp_.size()) &&
1251 dst_index < 64) {
1252 uint8_t pixel_value = current_gfx_bmp_.data()[src_index];
1253
1254 // Apply normalization based on settings
1256 pixel_value &= palette_normalization_mask_;
1257 }
1258
1259 tile_data[dst_index] = pixel_value;
1260 }
1261 }
1262 }
1263
1264 // Create the individual tile bitmap
1265 current_gfx_individual_.emplace_back();
1266 auto& tile_bitmap = current_gfx_individual_.back();
1267
1268 try {
1269 tile_bitmap.Create(8, 8, 8, tile_data);
1270
1271 // Set default palette using the same system as overworld
1272 if (overworld_palette_.size() >= 256) {
1273 // Use complete 256-color palette (same as overworld system)
1274 // The pixel data already contains correct color indices for the 256-color palette
1275 tile_bitmap.SetPalette(overworld_palette_);
1276 } else if (rom() && rom()->palette_group().overworld_main.size() > 0) {
1277 // Fallback to ROM palette
1278 tile_bitmap.SetPalette(rom()->palette_group().overworld_main[0]);
1279 }
1280 // Queue texture creation via Arena's deferred system
1283 } catch (const std::exception& e) {
1284 util::logf("Error creating tile at (%d,%d): %s", tile_x, tile_y,
1285 e.what());
1286 // Create an empty bitmap as fallback
1287 tile_bitmap.Create(8, 8, 8, std::vector<uint8_t>(64, 0));
1288 }
1289 }
1290 }
1291
1292 // Apply current palette settings to all tiles
1293 if (rom_) {
1295 }
1296
1297 util::logf("Loaded %zu individual tile8 graphics",
1299 return absl::OkStatus();
1300}
1301
1302absl::Status Tile16Editor::SetCurrentTile(int tile_id) {
1303 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
1304 return absl::OutOfRangeError(
1305 absl::StrFormat("Invalid tile16 id: %d", tile_id));
1306 }
1307
1308 if (!tile16_blockset_) {
1309 return absl::FailedPreconditionError("Tile16 blockset not initialized");
1310 }
1311
1312 current_tile16_ = tile_id;
1313
1314 // Initialize the instance variable with current ROM data
1315 auto tile_result = rom_->ReadTile16(current_tile16_);
1316 if (tile_result.ok()) {
1317 current_tile16_data_ = tile_result.value();
1318 }
1319
1320 // Extract tile data using the same method as GetTilemapData
1321 auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
1322
1323 if (tile_data.empty()) {
1324 // If GetTilemapData fails, manually extract from the atlas
1325 const int kTilesPerRow = 8; // Standard tile16 blockset layout
1326 int tile_x = (tile_id % kTilesPerRow) * kTile16Size;
1327 int tile_y = (tile_id / kTilesPerRow) * kTile16Size;
1328
1329 tile_data.resize(kTile16PixelCount);
1330
1331 // Manual extraction without the buggy offset increment
1332 for (int ty = 0; ty < kTile16Size; ty++) {
1333 for (int tx = 0; tx < kTile16Size; tx++) {
1334 int pixel_x = tile_x + tx;
1335 int pixel_y = tile_y + ty;
1336 int src_index = (pixel_y * tile16_blockset_->atlas.width()) + pixel_x;
1337 int dst_index = ty * kTile16Size + tx;
1338
1339 if (src_index < static_cast<int>(tile16_blockset_->atlas.size()) &&
1340 dst_index < static_cast<int>(tile_data.size())) {
1341 uint8_t pixel_value = tile16_blockset_->atlas.data()[src_index];
1342 // Normalize pixel values to valid palette range
1343 pixel_value &= 0x0F; // Keep only lower 4 bits for palette index
1344 tile_data[dst_index] = pixel_value;
1345 }
1346 }
1347 }
1348 } else {
1349 // Normalize the extracted data based on settings
1351 for (auto& pixel : tile_data) {
1353 }
1354 }
1355 }
1356
1357 // Create the bitmap with the extracted data
1359
1360 // Use the same palette system as the overworld (complete 256-color palette)
1361 if (overworld_palette_.size() >= 256) {
1362 // Use complete 256-color palette (same as overworld system)
1363 // The pixel data already contains correct color indices for the 256-color palette
1365 } else if (palette_.size() >= 256) {
1367 } else if (rom()->palette_group().overworld_main.size() > 0) {
1368 current_tile16_bmp_.SetPalette(rom()->palette_group().overworld_main[0]);
1369 }
1370
1371 // Queue texture creation via Arena's deferred system
1374
1375 // Simple success logging
1376 util::logf("SetCurrentTile: loaded tile %d successfully", tile_id);
1377
1378 return absl::OkStatus();
1379}
1380
1381absl::Status Tile16Editor::CopyTile16ToClipboard(int tile_id) {
1382 if (tile_id < 0 || tile_id >= zelda3::kNumTile16Individual) {
1383 return absl::InvalidArgumentError("Invalid tile ID");
1384 }
1385
1386 // CRITICAL FIX: Extract tile data directly from atlas instead of using problematic tile cache
1387 auto tile_data = gfx::GetTilemapData(*tile16_blockset_, tile_id);
1388 if (!tile_data.empty()) {
1389 clipboard_tile16_.Create(16, 16, 8, tile_data);
1391 }
1392 // Queue texture creation via Arena's deferred system
1395
1396 clipboard_has_data_ = true;
1397 return absl::OkStatus();
1398}
1399
1401 if (!clipboard_has_data_) {
1402 return absl::FailedPreconditionError("Clipboard is empty");
1403 }
1404
1405 // Copy the clipboard data to the current tile16
1408 // Queue texture creation via Arena's deferred system
1411
1412 return absl::OkStatus();
1413}
1414
1416 if (slot < 0 || slot >= 4) {
1417 return absl::InvalidArgumentError("Invalid scratch space slot");
1418 }
1419
1420 // Create a copy of the current tile16 bitmap
1421 scratch_space_[slot].Create(16, 16, 8, current_tile16_bmp_.vector());
1422 scratch_space_[slot].SetPalette(current_tile16_bmp_.palette());
1423 // Queue texture creation via Arena's deferred system
1426
1427 scratch_space_used_[slot] = true;
1428 return absl::OkStatus();
1429}
1430
1432 if (slot < 0 || slot >= 4) {
1433 return absl::InvalidArgumentError("Invalid scratch space slot");
1434 }
1435
1436 if (!scratch_space_used_[slot]) {
1437 return absl::FailedPreconditionError("Scratch space slot is empty");
1438 }
1439
1440 // Copy the scratch space data to the current tile16
1441 current_tile16_bmp_.Create(16, 16, 8, scratch_space_[slot].vector());
1443 // Queue texture creation via Arena's deferred system
1446
1447 return absl::OkStatus();
1448}
1449
1450absl::Status Tile16Editor::ClearScratchSpace(int slot) {
1451 if (slot < 0 || slot >= 4) {
1452 return absl::InvalidArgumentError("Invalid scratch space slot");
1453 }
1454
1455 scratch_space_used_[slot] = false;
1456 return absl::OkStatus();
1457}
1458
1459// Advanced editing features
1462 return absl::FailedPreconditionError("No active tile16 to flip");
1463 }
1464
1465 SaveUndoState();
1466
1467 // Create a temporary bitmap for the flipped result
1468 gfx::Bitmap flipped_bitmap;
1469 flipped_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
1470
1471 // Flip horizontally by copying pixels in reverse x order
1472 for (int y = 0; y < 16; ++y) {
1473 for (int x = 0; x < 16; ++x) {
1474 int src_index = y * 16 + x;
1475 int dst_index = y * 16 + (15 - x);
1476 if (src_index < current_tile16_bmp_.size() &&
1477 dst_index < flipped_bitmap.size()) {
1478 flipped_bitmap.WriteToPixel(dst_index,
1479 current_tile16_bmp_.data()[src_index]);
1480 }
1481 }
1482 }
1483
1484 // Copy the flipped result back
1485 current_tile16_bmp_ = std::move(flipped_bitmap);
1488
1489 // Queue texture update via Arena's deferred system
1492 return absl::OkStatus();
1493}
1494
1497 return absl::FailedPreconditionError("No active tile16 to flip");
1498 }
1499
1500 SaveUndoState();
1501
1502 // Create a temporary bitmap for the flipped result
1503 gfx::Bitmap flipped_bitmap;
1504 flipped_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
1505
1506 // Flip vertically by copying pixels in reverse y order
1507 for (int y = 0; y < 16; ++y) {
1508 for (int x = 0; x < 16; ++x) {
1509 int src_index = y * 16 + x;
1510 int dst_index = (15 - y) * 16 + x;
1511 if (src_index < current_tile16_bmp_.size() &&
1512 dst_index < flipped_bitmap.size()) {
1513 flipped_bitmap.WriteToPixel(dst_index,
1514 current_tile16_bmp_.data()[src_index]);
1515 }
1516 }
1517 }
1518
1519 // Copy the flipped result back
1520 current_tile16_bmp_ = std::move(flipped_bitmap);
1523
1524 // Queue texture update via Arena's deferred system
1527 return absl::OkStatus();
1528}
1529
1532 return absl::FailedPreconditionError("No active tile16 to rotate");
1533 }
1534
1535 SaveUndoState();
1536
1537 // Create a temporary bitmap for the rotated result
1538 gfx::Bitmap rotated_bitmap;
1539 rotated_bitmap.Create(16, 16, 8, std::vector<uint8_t>(256, 0));
1540
1541 // Rotate 90 degrees clockwise
1542 for (int y = 0; y < 16; ++y) {
1543 for (int x = 0; x < 16; ++x) {
1544 int src_index = y * 16 + x;
1545 int dst_index = x * 16 + (15 - y);
1546 if (src_index < current_tile16_bmp_.size() &&
1547 dst_index < rotated_bitmap.size()) {
1548 rotated_bitmap.WriteToPixel(dst_index,
1549 current_tile16_bmp_.data()[src_index]);
1550 }
1551 }
1552 }
1553
1554 // Copy the rotated result back
1555 current_tile16_bmp_ = std::move(rotated_bitmap);
1558
1559 // Queue texture update via Arena's deferred system
1562 return absl::OkStatus();
1563}
1564
1565absl::Status Tile16Editor::FillTile16WithTile8(int tile8_id) {
1566 if (tile8_id < 0 ||
1567 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
1568 return absl::InvalidArgumentError("Invalid tile8 ID");
1569 }
1570
1571 if (!current_gfx_individual_[tile8_id].is_active()) {
1572 return absl::FailedPreconditionError("Source tile8 not active");
1573 }
1574
1575 SaveUndoState();
1576
1577 // Fill all four quadrants with the same tile8
1578 for (int quadrant = 0; quadrant < 4; ++quadrant) {
1579 int start_x = (quadrant % 2) * 8;
1580 int start_y = (quadrant / 2) * 8;
1581
1582 for (int y = 0; y < 8; ++y) {
1583 for (int x = 0; x < 8; ++x) {
1584 int src_index = y * 8 + x;
1585 int dst_index = (start_y + y) * 16 + (start_x + x);
1586
1587 if (src_index < current_gfx_individual_[tile8_id].size() &&
1588 dst_index < current_tile16_bmp_.size()) {
1589 uint8_t pixel_value =
1590 current_gfx_individual_[tile8_id].data()[src_index];
1591 current_tile16_bmp_.WriteToPixel(dst_index, pixel_value);
1592 }
1593 }
1594 }
1595 }
1596
1598 // Queue texture update via Arena's deferred system
1601 return absl::OkStatus();
1602}
1603
1606 return absl::FailedPreconditionError("No active tile16 to clear");
1607 }
1608
1609 SaveUndoState();
1610
1611 // Fill with transparent/background color (0)
1612 auto& data = current_tile16_bmp_.mutable_data();
1613 std::fill(data.begin(), data.end(), 0);
1614
1616 // Queue texture update via Arena's deferred system
1619 return absl::OkStatus();
1620}
1621
1622// Palette management
1623absl::Status Tile16Editor::CyclePalette(bool forward) {
1624 uint8_t new_palette = current_palette_;
1625
1626 if (forward) {
1627 new_palette = (new_palette + 1) % 8;
1628 } else {
1629 new_palette = (new_palette == 0) ? 7 : new_palette - 1;
1630 }
1631
1632 current_palette_ = new_palette;
1633
1634 // Use the RefreshAllPalettes method which handles all the coordination
1636
1637 util::logf("Cycled to palette slot %d", current_palette_);
1638 return absl::OkStatus();
1639}
1640
1641absl::Status Tile16Editor::PreviewPaletteChange(uint8_t palette_id) {
1642 if (!show_palette_preview_) {
1643 return absl::OkStatus();
1644 }
1645
1646 if (palette_id >= 8) {
1647 return absl::InvalidArgumentError("Invalid palette ID");
1648 }
1649
1650 // Create a preview bitmap with the new palette
1651 if (!preview_tile16_.is_active()) {
1653 } else {
1654 // Recreate the preview bitmap with new data
1656 }
1657
1658 const auto& ow_main_pal_group = rom()->palette_group().overworld_main;
1659 if (ow_main_pal_group.size() > palette_id) {
1660 preview_tile16_.SetPaletteWithTransparent(ow_main_pal_group[0], palette_id);
1661 // Queue texture update via Arena's deferred system
1664 preview_dirty_ = true;
1665 }
1666
1667 return absl::OkStatus();
1668}
1669
1670// Undo/Redo system
1673 return;
1674 }
1675
1676 UndoState state;
1677 state.tile_id = current_tile16_;
1678 state.tile_bitmap.Create(16, 16, 8, current_tile16_bmp_.vector());
1680 state.palette = current_palette_;
1681 state.x_flip = x_flip;
1682 state.y_flip = y_flip;
1683 state.priority = priority_tile;
1684
1685 undo_stack_.push_back(std::move(state));
1686
1687 // Limit undo stack size
1688 if (undo_stack_.size() > kMaxUndoStates_) {
1689 undo_stack_.erase(undo_stack_.begin());
1690 }
1691
1692 // Clear redo stack when new action is performed
1693 redo_stack_.clear();
1694}
1695
1696absl::Status Tile16Editor::Undo() {
1697 if (undo_stack_.empty()) {
1698 return absl::FailedPreconditionError("Nothing to undo");
1699 }
1700
1701 // Save current state to redo stack
1702 UndoState current_state;
1703 current_state.tile_id = current_tile16_;
1704 current_state.tile_bitmap.Create(16, 16, 8, current_tile16_bmp_.vector());
1706 current_state.palette = current_palette_;
1707 current_state.x_flip = x_flip;
1708 current_state.y_flip = y_flip;
1709 current_state.priority = priority_tile;
1710 redo_stack_.push_back(std::move(current_state));
1711
1712 // Restore previous state
1713 const UndoState& previous_state = undo_stack_.back();
1714 current_tile16_ = previous_state.tile_id;
1715 current_tile16_bmp_ = previous_state.tile_bitmap;
1716 current_palette_ = previous_state.palette;
1717 x_flip = previous_state.x_flip;
1718 y_flip = previous_state.y_flip;
1719 priority_tile = previous_state.priority;
1720
1721 // Queue texture update via Arena's deferred system
1724 undo_stack_.pop_back();
1725
1726 return absl::OkStatus();
1727}
1728
1729absl::Status Tile16Editor::Redo() {
1730 if (redo_stack_.empty()) {
1731 return absl::FailedPreconditionError("Nothing to redo");
1732 }
1733
1734 // Save current state to undo stack
1735 SaveUndoState();
1736
1737 // Restore next state
1738 const UndoState& next_state = redo_stack_.back();
1739 current_tile16_ = next_state.tile_id;
1740 current_tile16_bmp_ = next_state.tile_bitmap;
1741 current_palette_ = next_state.palette;
1742 x_flip = next_state.x_flip;
1743 y_flip = next_state.y_flip;
1744 priority_tile = next_state.priority;
1745
1746 // Queue texture update via Arena's deferred system
1749 redo_stack_.pop_back();
1750
1751 return absl::OkStatus();
1752}
1753
1755 if (!tile16_blockset_) {
1756 return absl::FailedPreconditionError("Tile16 blockset not initialized");
1757 }
1758
1759 if (current_tile16_ < 0 ||
1760 current_tile16_ >= static_cast<int>(tile16_blockset_->atlas.size())) {
1761 return absl::OutOfRangeError("Current tile16 ID out of range");
1762 }
1763
1764 if (current_palette_ >= 8) {
1765 return absl::OutOfRangeError("Current palette ID out of range");
1766 }
1767
1768 return absl::OkStatus();
1769}
1770
1771bool Tile16Editor::IsTile16Valid(int tile_id) const {
1772 return tile_id >= 0 && tile16_blockset_ &&
1773 tile_id < static_cast<int>(tile16_blockset_->atlas.size());
1774}
1775
1776// Integration with overworld system
1778 if (!rom_) {
1779 return absl::FailedPreconditionError("ROM not available");
1780 }
1781
1783 return absl::FailedPreconditionError("No active tile16 to save");
1784 }
1785
1786 // Update the tile16 blockset with current changes
1788
1789 // Commit changes to the tile16 blockset
1791
1792 // Mark ROM as dirty to ensure saving
1793 return absl::OkStatus();
1794}
1795
1797 if (!tile16_blockset_) {
1798 return absl::FailedPreconditionError("Tile16 blockset not initialized");
1799 }
1800
1801 if (current_tile16_ < 0 || current_tile16_ >= zelda3::kNumTile16Individual) {
1802 return absl::OutOfRangeError("Current tile16 ID out of range");
1803 }
1804
1805 // CRITICAL FIX: Update atlas directly instead of using problematic tile cache
1806 // This prevents the move-related crashes we experienced earlier
1807
1808 // Update the atlas if needed
1810 // Update the portion of the atlas that corresponds to this tile
1811 constexpr int kTilesPerRow =
1812 8; // Standard SNES tile16 layout is 8 tiles per row
1813 int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
1814 int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
1815
1816 // Copy pixel data from current tile to atlas
1817 for (int tile_y_offset = 0; tile_y_offset < kTile16Size; ++tile_y_offset) {
1818 for (int tile_x_offset = 0; tile_x_offset < kTile16Size;
1819 ++tile_x_offset) {
1820 int src_index = tile_y_offset * kTile16Size + tile_x_offset;
1821 int dst_index =
1822 (tile_y + tile_y_offset) * tile16_blockset_->atlas.width() +
1823 (tile_x + tile_x_offset);
1824
1825 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
1826 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
1828 dst_index, current_tile16_bmp_.data()[src_index]);
1829 }
1830 }
1831 }
1832
1834 // Queue texture update via Arena's deferred system
1837 }
1838
1839 return absl::OkStatus();
1840}
1841
1843 if (!tile16_blockset_) {
1844 return absl::FailedPreconditionError("Tile16 blockset not initialized");
1845 }
1846
1847 // Regenerate the tilemap data if needed
1849 // Queue texture update via Arena's deferred system
1852 }
1853
1854 // Update individual cached tiles
1855 // Note: With the new tile cache system, tiles are automatically managed
1856 // and don't need manual modification tracking like the old system
1857 // The cache handles LRU eviction and automatic updates
1858
1859 return absl::OkStatus();
1860}
1861
1863 // CRITICAL FIX: Complete workflow for tile16 changes
1864 // This method now only commits to ROM when explicitly called (user presses Save)
1865
1866 // Step 1: Update ROM data with current tile16 changes
1868
1869 // Step 2: Update the local blockset to reflect changes
1871
1872 // Step 3: Update the atlas directly (bypass problematic tile cache)
1874 // Calculate the position of this tile in the blockset atlas
1875 constexpr int kTilesPerRow = 8;
1876 int tile_x = (current_tile16_ % kTilesPerRow) * kTile16Size;
1877 int tile_y = (current_tile16_ / kTilesPerRow) * kTile16Size;
1878
1879 // Copy current tile16 bitmap data directly to atlas
1880 for (int ty = 0; ty < kTile16Size; ++ty) {
1881 for (int tx = 0; tx < kTile16Size; ++tx) {
1882 int src_index = ty * kTile16Size + tx;
1883 int dst_index =
1884 (tile_y + ty) * tile16_blockset_->atlas.width() + (tile_x + tx);
1885
1886 if (src_index < static_cast<int>(current_tile16_bmp_.size()) &&
1887 dst_index < static_cast<int>(tile16_blockset_->atlas.size())) {
1889 dst_index, current_tile16_bmp_.data()[src_index]);
1890 }
1891 }
1892 }
1893
1895 // Queue texture update via Arena's deferred system
1898 }
1899
1900 // Step 4: Notify the parent editor (overworld editor) to regenerate its blockset
1903 }
1904
1905 util::logf("Committed Tile16 %d changes to overworld system",
1907 return absl::OkStatus();
1908}
1909
1911 // Reload the current tile16 from ROM to discard any local changes
1913
1914 util::logf("Discarded Tile16 changes for tile %d", current_tile16_);
1915 return absl::OkStatus();
1916}
1917
1918absl::Status Tile16Editor::PickTile8FromTile16(const ImVec2& position) {
1919 // Get the current tile16 data from ROM
1920 if (!rom_ || current_tile16_ < 0 || current_tile16_ >= 512) {
1921 return absl::InvalidArgumentError("Invalid tile16 or ROM not set");
1922 }
1923
1924 // Determine which quadrant of the tile16 was clicked
1925 int quad_x = (position.x < 8) ? 0 : 1; // Left or right half
1926 int quad_y = (position.y < 8) ? 0 : 1; // Top or bottom half
1927 int quadrant = quad_x + (quad_y * 2); // 0=TL, 1=TR, 2=BL, 3=BR
1928
1929 // Get the tile16 data structure
1930 auto* tile16_data = GetCurrentTile16Data();
1931 if (!tile16_data) {
1932 return absl::FailedPreconditionError("Failed to get tile16 data");
1933 }
1934
1935 // Extract the tile8 ID from the appropriate quadrant
1936 gfx::TileInfo tile_info;
1937 switch (quadrant) {
1938 case 0:
1939 tile_info = tile16_data->tile0_;
1940 break; // Top-left
1941 case 1:
1942 tile_info = tile16_data->tile1_;
1943 break; // Top-right
1944 case 2:
1945 tile_info = tile16_data->tile2_;
1946 break; // Bottom-left
1947 case 3:
1948 tile_info = tile16_data->tile3_;
1949 break; // Bottom-right
1950 }
1951
1952 // Set the current tile8 and palette
1953 current_tile8_ = tile_info.id_;
1954 current_palette_ = tile_info.palette_;
1955
1956 // Update the flip states based on the tile info
1957 x_flip = tile_info.horizontal_mirror_;
1958 y_flip = tile_info.vertical_mirror_;
1959 priority_tile = tile_info.over_;
1960
1961 // Refresh the palette to match the picked tile
1964
1965 util::logf("Picked tile8 %d with palette %d from quadrant %d of tile16 %d",
1967
1968 return absl::OkStatus();
1969}
1970
1971// Get the appropriate palette slot for current graphics sheet
1972int Tile16Editor::GetPaletteSlotForSheet(int sheet_index) const {
1973 // Based on ProcessGraphicsBuffer logic and overworld palette coordination:
1974 // Sheets 0,3-6: Use AUX palettes (slots 10-15 in 256-color palette)
1975 // Sheets 1-2: Use MAIN palette (slots 2-6 in 256-color palette)
1976 // Sheet 7: Use ANIMATED palette (slot 7 in 256-color palette)
1977
1978 switch (sheet_index) {
1979 case 0:
1980 return 10; // Main blockset -> AUX1 palette region
1981 case 1:
1982 return 2; // Main graphics -> MAIN palette region
1983 case 2:
1984 return 3; // Main graphics -> MAIN palette region
1985 case 3:
1986 return 11; // Area graphics -> AUX1 palette region
1987 case 4:
1988 return 12; // Area graphics -> AUX1 palette region
1989 case 5:
1990 return 13; // Area graphics -> AUX2 palette region
1991 case 6:
1992 return 14; // Area graphics -> AUX2 palette region
1993 case 7:
1994 return 7; // Animated tiles -> ANIMATED palette region
1995 default:
1996 return static_cast<int>(
1997 current_palette_); // Use current selection for other sheets
1998 }
1999}
2000
2001// NEW: Get the actual palette slot for a given palette button and sheet index
2003 int sheet_index) const {
2004 // Map palette buttons 0-7 to actual 256-color palette slots based on sheet type
2005 // Based on the correct 256-color palette structure from SetColorsPalette()
2006 // The 256-color palette is organized as a 16x16 grid (16 colors per row)
2007
2008 switch (sheet_index) {
2009 case 0: // Main blockset -> AUX1 region (right side, rows 2-4, cols 9-15)
2010 case 3:
2011 case 4:
2012 // AUX1 palette: Row 2-4, cols 9-15 = slots 41-47, 57-63, 73-79
2013 // Use row 2, col 9 + palette_button offset
2014 return 41 + palette_button; // Row 2, col 9 = slot 41
2015
2016 case 5:
2017 case 6: // Area graphics -> AUX2 region (right side, rows 5-7, cols 9-15)
2018 // AUX2 palette: Row 5-7, cols 9-15 = slots 89-95, 105-111, 121-127
2019 // Use row 5, col 9 + palette_button offset
2020 return 89 + palette_button; // Row 5, col 9 = slot 89
2021
2022 case 1:
2023 case 2: // Main graphics -> MAIN region (left side, rows 2-6, cols 1-7)
2024 // MAIN palette: Row 2-6, cols 1-7 = slots 33-39, 49-55, 65-71, 81-87, 97-103
2025 // Use row 2, col 1 + palette_button offset
2026 return 33 + palette_button; // Row 2, col 1 = slot 33
2027
2028 case 7: // Animated tiles -> ANIMATED region (row 7, cols 1-7)
2029 // ANIMATED palette: Row 7, cols 1-7 = slots 113-119
2030 return 113 + palette_button; // Row 7, col 1 = slot 113
2031
2032 default:
2033 return 33 + palette_button; // Default to MAIN region
2034 }
2035}
2036
2037// NEW: Get the sheet index for a given tile8 ID
2039 // Determine which graphics sheet a tile8 belongs to based on its position
2040 // This is based on the 256-tile per sheet organization
2041
2042 constexpr int kTilesPerSheet = 256; // 16x16 tiles per sheet
2043 int sheet_index = tile8_id / kTilesPerSheet;
2044
2045 // Clamp to valid sheet range (0-7)
2046 return std::min(7, std::max(0, sheet_index));
2047}
2048
2049// NEW: Get the actual palette slot for the current tile16 being edited
2051 // For the current tile16, we need to determine which sheet the tile8s belong to
2052 // and use the most appropriate palette region
2053
2054 if (current_tile8_ >= 0 &&
2055 current_tile8_ < static_cast<int>(current_gfx_individual_.size())) {
2056 int sheet_index = GetSheetIndexForTile8(current_tile8_);
2057 return GetActualPaletteSlot(current_palette_, sheet_index);
2058 }
2059
2060 // Default to sheet 0 (main blockset) if no tile8 selected
2062}
2063
2064// Helper methods for palette management
2065absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) {
2066 if (tile8_id < 0 ||
2067 tile8_id >= static_cast<int>(current_gfx_individual_.size())) {
2068 return absl::InvalidArgumentError("Invalid tile8 ID");
2069 }
2070
2071 if (!current_gfx_individual_[tile8_id].is_active()) {
2072 return absl::OkStatus(); // Skip inactive tiles
2073 }
2074
2075 if (!rom_) {
2076 return absl::FailedPreconditionError("ROM not set");
2077 }
2078
2079 // Use the complete 256-color overworld palette for consistency
2080 gfx::SnesPalette display_palette;
2081 if (overworld_palette_.size() >= 256) {
2082 display_palette = overworld_palette_;
2083 } else if (palette_.size() >= 256) {
2084 display_palette = palette_;
2085 } else {
2086 // Fallback to ROM palette
2087 const auto& palette_groups = rom()->palette_group();
2088 if (palette_groups.overworld_main.size() > 0) {
2089 display_palette = palette_groups.overworld_main[0];
2090 } else {
2091 return absl::FailedPreconditionError("No overworld palette available");
2092 }
2093 }
2094
2095 // Validate current_palette_ index
2096 if (current_palette_ < 0 || current_palette_ >= 8) {
2097 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
2098 current_palette_ = 0;
2099 }
2100
2101 // // Use the same palette system as the overworld (complete 256-color palette)
2102 // if (display_palette.size() >= 256) {
2103 // // Apply complete 256-color palette (same as overworld system)
2104 // // The pixel data already contains correct color indices for the 256-color palette
2105 // current_gfx_individual_[tile8_id].SetPalette(display_palette);
2106 // } else {
2107 // For smaller palettes, use SetPaletteWithTransparent with current palette
2108 current_gfx_individual_[tile8_id].SetPaletteWithTransparent(display_palette,
2110 // }
2111
2112 current_gfx_individual_[tile8_id].set_modified(true);
2113 // Queue texture update via Arena's deferred system
2116
2117 util::logf("Updated tile8 %d with palette slot %d (palette size: %zu colors)",
2118 tile8_id, current_palette_, display_palette.size());
2119
2120 return absl::OkStatus();
2121}
2122
2124 if (!rom_) {
2125 return absl::FailedPreconditionError("ROM not set");
2126 }
2127
2128 // Validate current_palette_ index
2129 if (current_palette_ < 0 || current_palette_ >= 8) {
2130 util::logf("Warning: Invalid palette index %d, using 0", current_palette_);
2131 current_palette_ = 0;
2132 }
2133
2134 // CRITICAL FIX: Use the complete overworld palette for proper color coordination
2135 gfx::SnesPalette display_palette;
2136
2137 if (overworld_palette_.size() >= 256) {
2138 // Use the complete 256-color palette from overworld editor
2139 display_palette = overworld_palette_;
2140 util::logf("Using complete overworld palette with %zu colors",
2141 display_palette.size());
2142 } else if (palette_.size() >= 256) {
2143 // Fallback to the old palette_ if it's complete
2144 display_palette = palette_;
2145 util::logf("Using fallback complete palette with %zu colors",
2146 display_palette.size());
2147 } else {
2148 // Last resort: Use ROM palette groups
2149 const auto& palette_groups = rom()->palette_group();
2150 if (palette_groups.overworld_main.size() > 0) {
2151 display_palette = palette_groups.overworld_main[0];
2152 util::logf("Warning: Using ROM main palette with %zu colors",
2153 display_palette.size());
2154 } else {
2155 return absl::FailedPreconditionError("No palette available");
2156 }
2157 }
2158
2159 // CRITICAL FIX: Use the same palette system as the overworld
2160 // The overworld system applies the complete 256-color palette to the main graphics bitmap
2161 // Individual tile8 graphics use the same palette but with proper color mapping
2162
2164 // Apply the complete 256-color palette to the source bitmap (same as overworld)
2165 current_gfx_bmp_.SetPalette(display_palette);
2167 // Queue texture update via Arena's deferred system
2170 util::logf(
2171 "Applied complete 256-color palette to source bitmap (same as "
2172 "overworld)");
2173 }
2174
2175 // Update current tile16 being edited with complete 256-color palette
2177 // Use complete 256-color palette (same as overworld system)
2178 current_tile16_bmp_.SetPalette(display_palette);
2180 // Queue texture update via Arena's deferred system
2183 }
2184
2185 // Update all individual tile8 graphics with complete 256-color palette
2186 for (size_t i = 0; i < current_gfx_individual_.size(); ++i) {
2187 if (current_gfx_individual_[i].is_active()) {
2188 // Use complete 256-color palette (same as overworld system)
2189 // The pixel data already contains correct color indices for the 256-color palette
2190 current_gfx_individual_[i].SetPalette(display_palette);
2191 current_gfx_individual_[i].set_modified(true);
2192 // Queue texture update via Arena's deferred system
2195 }
2196 }
2197
2198 util::logf(
2199 "Successfully refreshed all palettes in tile16 editor using complete "
2200 "256-color palette "
2201 "(same as overworld system)");
2202 return absl::OkStatus();
2203}
2204
2207 if (Begin("Advanced Palette Settings", &show_palette_settings_)) {
2208 Text("Pixel Normalization & Color Correction:");
2209
2210 int mask_value = static_cast<int>(palette_normalization_mask_);
2211 if (SliderInt("Normalization Mask", &mask_value, 1, 255, "0x%02X")) {
2212 palette_normalization_mask_ = static_cast<uint8_t>(mask_value);
2213 }
2214
2215 Checkbox("Auto Normalize Pixels", &auto_normalize_pixels_);
2216
2217 if (Button("Apply to All Graphics")) {
2218 auto reload_result = LoadTile8();
2219 if (!reload_result.ok()) {
2220 Text("Error: %s", reload_result.message().data());
2221 }
2222 }
2223
2224 SameLine();
2225 if (Button("Reset Defaults")) {
2228 auto reload_result = LoadTile8();
2229 (void)reload_result; // Suppress warning
2230 }
2231
2232 Separator();
2233 Text("Current State:");
2234 static constexpr std::array<const char*, 7> palette_group_names = {
2235 "OW Main", "OW Aux", "OW Anim", "Dungeon",
2236 "Sprites", "Armor", "Sword"};
2237 Text("Palette Group: %d (%s)", current_palette_group_,
2239 ? palette_group_names[current_palette_group_]
2240 : "Unknown");
2241 Text("Current Palette: %d", current_palette_);
2242
2243 Separator();
2244 Text("Sheet-Specific Fixes:");
2245
2246 // Sheet-specific palette fixes
2247 static bool fix_sheet_0 = true;
2248 static bool fix_sprite_sheets = true;
2249 static bool use_transparent_for_terrain = false;
2250
2251 if (Checkbox("Fix Sheet 0 (Trees)", &fix_sheet_0)) {
2252 auto reload_result = LoadTile8();
2253 if (!reload_result.ok()) {
2254 Text("Error reloading: %s", reload_result.message().data());
2255 }
2256 }
2257 HOVER_HINT(
2258 "Use direct palette for sheet 0 instead of transparent palette");
2259
2260 if (Checkbox("Fix Sprite Sheets", &fix_sprite_sheets)) {
2261 auto reload_result = LoadTile8();
2262 if (!reload_result.ok()) {
2263 Text("Error reloading: %s", reload_result.message().data());
2264 }
2265 }
2266 HOVER_HINT("Use direct palette for sprite graphics sheets");
2267
2268 if (Checkbox("Transparent for Terrain", &use_transparent_for_terrain)) {
2269 auto reload_result = LoadTile8();
2270 if (!reload_result.ok()) {
2271 Text("Error reloading: %s", reload_result.message().data());
2272 }
2273 }
2274 HOVER_HINT("Force transparent palette for terrain graphics");
2275
2276 Separator();
2277 Text("Color Analysis:");
2278 if (current_tile8_ >= 0 &&
2279 current_tile8_ < static_cast<int>(current_gfx_individual_.size()) &&
2281 Text("Selected Tile8 Analysis:");
2282 const auto& tile_data =
2284 std::map<uint8_t, int> pixel_counts;
2285 for (uint8_t pixel : tile_data) {
2286 pixel_counts[pixel & 0x0F]++; // Normalize to 4-bit
2287 }
2288
2289 Text("Pixel Value Distribution:");
2290 for (const auto& pair : pixel_counts) {
2291 int value = pair.first;
2292 int count = pair.second;
2293 Text(" Value %d (0x%X): %d pixels", value, value, count);
2294 }
2295
2296 Text("Palette Colors Used:");
2297 const auto& palette = current_gfx_individual_[current_tile8_].palette();
2298 for (const auto& pair : pixel_counts) {
2299 int value = pair.first;
2300 int count = pair.second;
2301 if (value < static_cast<int>(palette.size())) {
2302 auto color = palette[value];
2303 ImVec4 display_color = color.rgb();
2304 ImGui::ColorButton(("##analysis" + std::to_string(value)).c_str(),
2305 display_color, ImGuiColorEditFlags_NoTooltip,
2306 ImVec2(16, 16));
2307 if (ImGui::IsItemHovered()) {
2308 ImGui::SetTooltip("Index %d: 0x%04X (%d pixels)", value,
2309 color.snes(), count);
2310 }
2311 if (value % 8 != 7)
2312 ImGui::SameLine();
2313 }
2314 }
2315 }
2316
2317 // Enhanced ROM Palette Management Section
2318 Separator();
2319 if (CollapsingHeader("ROM Palette Manager") && rom_) {
2320 Text("Experimental ROM Palette Selection:");
2321 HOVER_HINT(
2322 "Use ROM palettes to experiment with different color schemes");
2323
2324 if (Button("Open Enhanced Palette Editor")) {
2326 }
2327 SameLine();
2328 if (Button("Show Color Analysis")) {
2330 }
2331
2332 // Quick palette application
2333 static int quick_group = 0;
2334 static int quick_index = 0;
2335
2336 SliderInt("ROM Group", &quick_group, 0, 6);
2337 SliderInt("Palette Index", &quick_index, 0, 7);
2338
2339 if (Button("Apply to Tile8 Source")) {
2340 if (tile8_source_canvas_.ApplyROMPalette(quick_group, quick_index)) {
2341 util::logf("Applied ROM palette group %d, index %d to Tile8 source",
2342 quick_group, quick_index);
2343 }
2344 }
2345 SameLine();
2346 if (Button("Apply to Tile16 Editor")) {
2347 if (tile16_edit_canvas_.ApplyROMPalette(quick_group, quick_index)) {
2348 util::logf(
2349 "Applied ROM palette group %d, index %d to Tile16 editor",
2350 quick_group, quick_index);
2351 }
2352 }
2353 }
2354 }
2355 End();
2356 }
2357}
2358
2360 Text("Layout Scratch:");
2361 for (int i = 0; i < 4; i++) {
2362 if (i > 0)
2363 SameLine();
2364 std::string slot_name = "S" + std::to_string(i + 1);
2365
2366 if (layout_scratch_[i].in_use) {
2367 if (Button((slot_name + " Load").c_str(), ImVec2(40, 20))) {
2368 // Load layout from scratch - placeholder for now
2369 }
2370 } else {
2371 if (Button((slot_name + " Save").c_str(), ImVec2(40, 20))) {
2372 // Save current layout to scratch - placeholder for now
2373 }
2374 }
2375 }
2376}
2377
2379 if (slot < 0 || slot >= 4) {
2380 return absl::InvalidArgumentError("Invalid scratch slot");
2381 }
2382
2383 // For now, just mark as used - full implementation would save current editing state
2384 layout_scratch_[slot].in_use = true;
2385 layout_scratch_[slot].name = absl::StrFormat("Layout %d", slot + 1);
2386
2387 return absl::OkStatus();
2388}
2389
2391 if (slot < 0 || slot >= 4) {
2392 return absl::InvalidArgumentError("Invalid scratch slot");
2393 }
2394
2395 if (!layout_scratch_[slot].in_use) {
2396 return absl::FailedPreconditionError("Scratch slot is empty");
2397 }
2398
2399 // Placeholder - full implementation would restore editing state
2400 return absl::OkStatus();
2401}
2402
2404 if (ImGui::BeginPopupModal("ManualTile8Editor", nullptr,
2405 ImGuiWindowFlags_AlwaysAutoResize)) {
2406 ImGui::Text("Manual Tile8 Configuration for Tile16 %02X", current_tile16_);
2407 ImGui::Separator();
2408
2409 auto* tile_data = GetCurrentTile16Data();
2410 if (tile_data) {
2411 ImGui::Text("Current Tile16 ROM Data:");
2412
2413 // Display and edit each quadrant using TileInfo structure
2414 const char* quadrant_names[] = {"Top-Left", "Top-Right", "Bottom-Left",
2415 "Bottom-Right"};
2416
2417 for (int q = 0; q < 4; q++) {
2418 ImGui::Text("%s Quadrant:", quadrant_names[q]);
2419
2420 // Get the current TileInfo for this quadrant
2421 gfx::TileInfo* tile_info = nullptr;
2422 switch (q) {
2423 case 0:
2424 tile_info = &tile_data->tile0_;
2425 break;
2426 case 1:
2427 tile_info = &tile_data->tile1_;
2428 break;
2429 case 2:
2430 tile_info = &tile_data->tile2_;
2431 break;
2432 case 3:
2433 tile_info = &tile_data->tile3_;
2434 break;
2435 }
2436
2437 if (tile_info) {
2438 // Editable inputs for TileInfo components
2439 ImGui::PushID(q);
2440
2441 int tile_id_int = static_cast<int>(tile_info->id_);
2442 if (ImGui::InputInt("Tile8 ID", &tile_id_int, 1, 10)) {
2443 tile_info->id_ =
2444 static_cast<uint16_t>(std::max(0, std::min(tile_id_int, 1023)));
2445 }
2446
2447 int palette_int = static_cast<int>(tile_info->palette_);
2448 if (ImGui::SliderInt("Palette", &palette_int, 0, 7)) {
2449 tile_info->palette_ = static_cast<uint8_t>(palette_int);
2450 }
2451
2452 ImGui::Checkbox("X Flip", &tile_info->horizontal_mirror_);
2453 ImGui::SameLine();
2454 ImGui::Checkbox("Y Flip", &tile_info->vertical_mirror_);
2455 ImGui::SameLine();
2456 ImGui::Checkbox("Priority", &tile_info->over_);
2457
2458 if (ImGui::Button("Apply to Graphics")) {
2459 // Update the tiles_info array and regenerate graphics
2460 tile_data->tiles_info[q] = *tile_info;
2461
2462 auto update_result = UpdateROMTile16Data();
2463 if (!update_result.ok()) {
2464 ImGui::Text("Error: %s", update_result.message().data());
2465 }
2466
2467 auto refresh_result = SetCurrentTile(current_tile16_);
2468 if (!refresh_result.ok()) {
2469 ImGui::Text("Refresh Error: %s", refresh_result.message().data());
2470 }
2471 }
2472
2473 ImGui::PopID();
2474 }
2475
2476 if (q < 3)
2477 ImGui::Separator();
2478 }
2479
2480 ImGui::Separator();
2481 if (ImGui::Button("Apply All Changes")) {
2482 auto update_result = UpdateROMTile16Data();
2483 if (!update_result.ok()) {
2484 ImGui::Text("Update Error: %s", update_result.message().data());
2485 }
2486
2487 auto save_result = SaveTile16ToROM();
2488 if (!save_result.ok()) {
2489 ImGui::Text("Save Error: %s", save_result.message().data());
2490 }
2491 }
2492 ImGui::SameLine();
2493 if (ImGui::Button("Refresh Display")) {
2494 auto refresh_result = SetCurrentTile(current_tile16_);
2495 if (!refresh_result.ok()) {
2496 ImGui::Text("Refresh Error: %s", refresh_result.message().data());
2497 }
2498 }
2499
2500 } else {
2501 ImGui::Text("Tile16 data not accessible");
2502 ImGui::Text("Current tile16: %d", current_tile16_);
2503 if (rom_) {
2504 ImGui::Text("Valid range: 0-4095 (4096 total tiles)");
2505 }
2506 }
2507
2508 ImGui::Separator();
2509 if (ImGui::Button("Close")) {
2510 ImGui::CloseCurrentPopup();
2511 }
2512
2513 ImGui::EndPopup();
2514 }
2515}
2516
2517} // namespace editor
2518} // namespace yaze
auto palette_group() const
Definition rom.h:213
absl::StatusOr< gfx::Tile16 > ReadTile16(uint32_t tile16_id)
Definition rom.cc:694
absl::Status WriteTile16(int tile16_id, const gfx::Tile16 &tile)
Definition rom.cc:712
auto size() const
Definition rom.h:202
absl::Status SaveTile16ToScratchSpace(int slot)
absl::Status LoadLayoutFromScratch(int slot)
std::chrono::steady_clock::time_point last_edit_time_
absl::Status CyclePalette(bool forward=true)
absl::Status FillTile16WithTile8(int tile8_id)
gfx::Tilemap * tile16_blockset_
int GetActualPaletteSlotForCurrentTile16() const
std::array< uint8_t, 0x200 > all_tiles_types_
absl::Status RegenerateTile16BitmapFromROM()
absl::Status PasteTile16FromClipboard()
absl::Status SaveLayoutToScratch(int slot)
absl::Status LoadTile16FromScratchSpace(int slot)
int GetPaletteSlotForSheet(int sheet_index) const
int GetActualPaletteSlot(int palette_button, int sheet_index) const
std::vector< UndoState > undo_stack_
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)
void EnableLivePreview(bool enable)
absl::Status SetCurrentTile(int id)
absl::Status UpdateTile8Palette(int tile8_id)
absl::Status RefreshTile16Blockset()
std::array< gfx::Bitmap, 4 > scratch_space_
std::vector< UndoState > redo_stack_
absl::Status UpdateOverworldTilemap()
absl::Status CommitChangesToBlockset()
std::array< LayoutScratch, 4 > layout_scratch_
absl::Status UpdateROMTile16Data()
bool IsTile16Valid(int tile_id) const
gfx::Tile16 * GetCurrentTile16Data()
std::vector< gfx::Bitmap > current_gfx_individual_
absl::Status CommitChangesToOverworld()
absl::Status UpdateBlocksetBitmap()
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)
std::function< absl::Status()> on_changes_committed_
static constexpr size_t kMaxUndoStates_
int GetSheetIndexForTile8(int tile8_id) const
std::array< bool, 4 > scratch_space_used_
absl::Status PickTile8FromTile16(const ImVec2 &position)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:32
static Arena & Get()
Definition arena.cc:15
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:66
const uint8_t * data() const
Definition bitmap.h:257
const SnesPalette & palette() const
Definition bitmap.h:251
void WriteToPixel(int position, uint8_t value)
Write a value to a pixel at the given position.
Definition bitmap.cc:377
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:162
const std::vector< uint8_t > & vector() const
Definition bitmap.h:261
auto size() const
Definition bitmap.h:256
bool is_active() const
Definition bitmap.h:264
void set_modified(bool modified)
Definition bitmap.h:267
int height() const
Definition bitmap.h:254
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap.
Definition bitmap.cc:292
int width() const
Definition bitmap.h:253
int depth() const
Definition bitmap.h:255
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
Definition bitmap.cc:303
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:258
bool modified() const
Definition bitmap.h:263
RAII timer for automatic timing management.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
Tile composition of four 8x8 tiles.
Definition snes_tile.h:137
SNES 16-bit tile metadata container.
Definition snes_tile.h:50
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1062
void ShowScalingControls()
Definition canvas.cc:1709
void ShowAdvancedCanvasProperties()
Definition canvas.cc:1586
void DrawContextMenu()
Definition canvas.cc:441
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:920
bool DrawTilePainter(const Bitmap &bitmap, int size, float scale=1.0f)
Definition canvas.cc:728
auto custom_labels_enabled()
Definition canvas.h:346
auto mutable_labels(int i)
Definition canvas.h:383
void InitializePaletteEditor(Rom *rom)
Definition canvas.cc:245
void set_draggable(bool draggable)
Definition canvas.h:316
float GetGlobalScale() const
Definition canvas.h:329
auto zero_point() const
Definition canvas.h:310
bool IsMouseHovering() const
Definition canvas.h:301
void ShowPaletteEditor()
Definition canvas.cc:252
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:381
void InitializeDefaults()
Definition canvas.cc:110
void SetAutoResize(bool auto_resize)
Definition canvas.h:255
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1386
bool ApplyROMPalette(int group_index, int palette_index)
Definition canvas.cc:266
void ShowColorAnalysis()
Definition canvas.cc:260
#define RETURN_IF_ERROR(expression)
Definition macro.h:53
#define HOVER_HINT(string)
Definition macro.h:24
Definition input.cc:20
constexpr int kTile16PixelCount
constexpr int kTile8Size
constexpr int kTile16Size
std::vector< uint8_t > GetTilemapData(Tilemap &tilemap, int tile_id)
Definition tilemap.cc:235
void BeginPadding(int i)
Definition style.cc:272
void AddTableColumn(Table &table, const std::string &label, GuiElement element)
Definition input.cc:406
void EndPadding()
Definition style.cc:276
void BeginChildWithScrollbar(const char *str_id)
Definition style.cc:284
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:116
constexpr int kNumTile16Individual
Definition overworld.h:120
Main namespace for the application.
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:110