yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sprite_editor.cc
Go to the documentation of this file.
1#include "sprite_editor.h"
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cerrno>
6#include <cstdlib>
7#include <cstring>
8
9#include "absl/strings/str_format.h"
17#include "app/gui/core/icons.h"
18#include "app/gui/core/input.h"
21#include "util/file_util.h"
22#include "util/hex.h"
23#include "util/macro.h"
25
26namespace yaze {
27namespace editor {
28
29using ImGui::BeginTable;
30using ImGui::Button;
31using ImGui::EndTable;
32using ImGui::Selectable;
33using ImGui::Separator;
34using ImGui::TableHeadersRow;
35using ImGui::TableNextColumn;
36using ImGui::TableNextRow;
37using ImGui::TableSetupColumn;
38using ImGui::Text;
39
40namespace {
41template <size_t N>
42void CopyStringToBuffer(const std::string& src, char (&dest)[N]) {
43 std::strncpy(dest, src.c_str(), N - 1);
44 dest[N - 1] = '\0';
45}
46
47int ParseIntOrDefault(const std::string& text, int fallback = 0) {
48 if (text.empty()) {
49 return fallback;
50 }
51 errno = 0;
52 char* end = nullptr;
53 long value = std::strtol(text.c_str(), &end, 0);
54 if (end == text.c_str() || errno == ERANGE) {
55 return fallback;
56 }
57 return static_cast<int>(value);
58}
59} // namespace
60
63 return;
64 auto* window_manager = dependencies_.window_manager;
65
66 // Register WindowContent implementations with callbacks
67 // EditorPanels provide both metadata (icon, name, priority) and drawing logic
68 window_manager->RegisterWindowContent(
69 std::make_unique<VanillaSpriteEditorPanel>([this]() {
70 if (rom_ && rom_->is_loaded()) {
71 DrawVanillaSpriteEditor();
72 } else {
73 ImGui::TextDisabled(tr("Load a ROM to view vanilla sprites"));
74 }
75 }));
76
77 window_manager->RegisterWindowContent(
78 std::make_unique<CustomSpriteEditorPanel>(
79 [this]() { DrawCustomSprites(); }));
80}
81
82absl::Status SpriteEditor::Load() {
83 gfx::ScopedTimer timer("SpriteEditor::Load");
84 return absl::OkStatus();
85}
86
87absl::Status SpriteEditor::Update() {
88 if (rom()->is_loaded() && !sheets_loaded_) {
89 sheets_loaded_ = true;
90 }
91
92 // Update animation playback for custom sprites
93 float current_time = ImGui::GetTime();
94 float delta_time = current_time - last_frame_time_;
95 last_frame_time_ = current_time;
96 UpdateAnimationPlayback(delta_time);
97
98 // Handle editor-level shortcuts
100
101 // Panel drawing is handled by WorkspaceWindowManager via registered EditorPanels
102 // Each panel's Draw() callback invokes the appropriate draw method
103
104 // Commit any pending undo transaction at the end of the frame
106
107 return status_.ok() ? absl::OkStatus() : status_;
108}
109
111 // Animation playback shortcuts (when custom sprite panel is active)
112 if (ImGui::IsKeyPressed(ImGuiKey_Space, false) &&
113 !ImGui::GetIO().WantTextInput) {
115 }
116
117 // Frame navigation
118 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket, false)) {
119 if (current_frame_ > 0) {
122 }
123 }
124 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket, false)) {
127 }
128
129 // Sprite navigation
130 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
131 if (current_sprite_id_ > 0) {
134 }
135 }
136 if (ImGui::GetIO().KeyCtrl &&
137 ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
140 }
141}
142
143absl::Status SpriteEditor::Save() {
145 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
146 const std::string& zsm_path = GetCurrentZsmPath();
147 if (zsm_path.empty()) {
149 } else {
150 SaveZsmFile(zsm_path);
151 }
152 }
153 return absl::OkStatus();
154}
155
157 // Sidebar handled by EditorManager for card-based editors
158}
159
160// ============================================================
161// Vanilla Sprite Editor
162// ============================================================
163
165 if (ImGui::BeginTable("##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
166 ImVec2(0, 0))) {
167 TableSetupColumn("Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
168 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch,
169 ImGui::GetContentRegionAvail().x);
170 TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
171 TableHeadersRow();
172 TableNextRow();
173
174 TableNextColumn();
176
177 TableNextColumn();
178 static int next_tab_id = 0;
179
180 if (gui::BeginThemedTabBar("SpriteTabBar", kSpriteTabBarFlags)) {
181 if (ImGui::TabItemButton(ICON_MD_ADD, kSpriteTabFlags)) {
182 if (std::find(active_sprites_.begin(), active_sprites_.end(),
184 next_tab_id++;
185 }
186 active_sprites_.push_back(next_tab_id++);
187 }
188
189 for (int n = 0; n < active_sprites_.Size;) {
190 bool open = true;
191
192 if (active_sprites_[n] > sizeof(zelda3::kSpriteDefaultNames) / 4) {
193 active_sprites_.erase(active_sprites_.Data + n);
194 continue;
195 }
196
197 if (ImGui::BeginTabItem(
199 ImGuiTabItemFlags_None)) {
201 ImGui::EndTabItem();
202 }
203
204 if (!open)
205 active_sprites_.erase(active_sprites_.Data + n);
206 else
207 n++;
208 }
209
211 }
212
213 TableNextColumn();
214 if (sheets_loaded_) {
216 }
217 ImGui::EndTable();
218 }
219}
220
222 static bool flip_x = false;
223 static bool flip_y = false;
224 if (ImGui::BeginChild(gui::GetID("##SpriteCanvas"),
225 ImGui::GetContentRegionAvail(), true)) {
228
229 // Render vanilla sprite if layout exists
230 if (current_sprite_id_ >= 0) {
231 const auto* layout = zelda3::SpriteOamRegistry::GetLayout(
232 static_cast<uint8_t>(current_sprite_id_));
233 if (layout) {
234 // Load required sheets for this sprite
235 LoadSheetsForSprite(layout->required_sheets);
236 RenderVanillaSprite(*layout);
237
238 // Draw the preview bitmap centered on canvas
241 }
242
243 // Show sprite info
244 ImGui::SetCursorPos(ImVec2(10, 10));
245 Text(tr("Sprite: %s (0x%02X)"), layout->name, layout->sprite_id);
246 Text(tr("Tiles: %zu"), layout->tiles.size());
247 }
248 }
249
252
253 if (ImGui::BeginTable("##OAMTable", 7, ImGuiTableFlags_Resizable,
254 ImVec2(0, 0))) {
255 TableSetupColumn("X", ImGuiTableColumnFlags_WidthStretch);
256 TableSetupColumn("Y", ImGuiTableColumnFlags_WidthStretch);
257 TableSetupColumn("Tile", ImGuiTableColumnFlags_WidthStretch);
258 TableSetupColumn("Palette", ImGuiTableColumnFlags_WidthStretch);
259 TableSetupColumn("Priority", ImGuiTableColumnFlags_WidthStretch);
260 TableSetupColumn("Flip X", ImGuiTableColumnFlags_WidthStretch);
261 TableSetupColumn("Flip Y", ImGuiTableColumnFlags_WidthStretch);
262 TableHeadersRow();
263 TableNextRow();
264
265 TableNextColumn();
267
268 TableNextColumn();
270
271 TableNextColumn();
273
274 TableNextColumn();
276
277 TableNextColumn();
279
280 TableNextColumn();
281 if (ImGui::Checkbox("##XFlip", &flip_x)) {
282 oam_config_.flip_x = flip_x;
283 }
284
285 TableNextColumn();
286 if (ImGui::Checkbox("##YFlip", &flip_y)) {
287 oam_config_.flip_y = flip_y;
288 }
289
290 ImGui::EndTable();
291 }
292
294 }
295 ImGui::EndChild();
296}
297
299 if (ImGui::BeginChild(gui::GetID("sheet_label"),
300 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
301 ImGuiWindowFlags_NoDecoration)) {
302 // Track previous sheet values for change detection
303 static uint8_t prev_sheets[8] = {0};
304 bool sheets_changed = false;
305
306 for (int i = 0; i < 8; i++) {
307 std::string sheet_label = absl::StrFormat("Sheet %d", i);
308 if (gui::InputHexByte(sheet_label.c_str(), &current_sheets_[i])) {
309 sheets_changed = true;
310 }
311 if (i % 2 == 0)
312 ImGui::SameLine();
313 }
314
315 // Reload graphics buffer if sheets changed
316 if (sheets_changed || std::memcmp(prev_sheets, current_sheets_, 8) != 0) {
317 std::memcpy(prev_sheets, current_sheets_, 8);
318 gfx_buffer_loaded_ = false;
320 }
321
326 for (int i = 0; i < 8; i++) {
328 gfx::Arena::Get().gfx_sheets().at(current_sheets_[i]), 1,
329 (i * 0x40) + 1, 2);
330 }
333 }
334 ImGui::EndChild();
335}
336
338 if (ImGui::BeginChild(gui::GetID("##SpritesList"),
339 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
340 ImGuiWindowFlags_NoDecoration)) {
341 int i = 0;
342 for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) {
344 current_sprite_id_ == i, "Sprite Names", util::HexByte(i),
346 if (ImGui::IsItemClicked()) {
347 if (current_sprite_id_ != i) {
350 }
351 if (!active_sprites_.contains(i)) {
352 active_sprites_.push_back(i);
353 }
354 }
355 i++;
356 }
357 }
358 ImGui::EndChild();
359}
360
362 if (ImGui::Button(tr("Add Frame"))) {
363 // Add a new frame
364 }
365 if (ImGui::Button(tr("Remove Frame"))) {
366 // Remove the current frame
367 }
368}
369
370// ============================================================
371// Custom ZSM Sprite Editor
372// ============================================================
373
375 if (BeginTable("##CustomSpritesTable", 3,
376 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
377 ImVec2(0, 0))) {
378 TableSetupColumn("Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
379 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch);
380 TableSetupColumn("Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
381
382 TableHeadersRow();
383 TableNextRow();
384 TableNextColumn();
385
387
388 TableNextColumn();
390
391 TableNextColumn();
393
394 EndTable();
395 }
396}
397
399 // Capture undo snapshot before any widgets can mutate sprite data.
400 // CommitUndoTransaction() in Update() will push the action only if
401 // a mutation actually occurred (zsm_dirty_ was set).
403 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
405 }
406
407 // File operations toolbar
408 if (ImGui::Button(ICON_MD_ADD " New")) {
410 }
411 ImGui::SameLine();
412 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
413 std::string file_path = util::FileDialogWrapper::ShowOpenFileDialog();
414 if (!file_path.empty()) {
415 LoadZsmFile(file_path);
416 }
417 }
418 ImGui::SameLine();
419 if (ImGui::Button(ICON_MD_SAVE " Save")) {
421 const std::string& zsm_path = GetCurrentZsmPath();
422 if (zsm_path.empty()) {
424 } else {
425 SaveZsmFile(zsm_path);
426 }
427 }
428 }
429 ImGui::SameLine();
430 if (ImGui::Button(ICON_MD_SAVE_AS " Save As")) {
432 }
433
434 Separator();
435
436 // Sprite list
437 Text(tr("Loaded Sprites:"));
438 if (ImGui::BeginChild("SpriteList", ImVec2(0, 100), true)) {
439 for (size_t i = 0; i < custom_sprites_.size(); i++) {
440 std::string label = custom_sprites_[i].sprName.empty()
441 ? "Unnamed Sprite"
442 : custom_sprites_[i].sprName;
443 if (Selectable(label.c_str(), current_custom_sprite_index_ == (int)i)) {
444 current_custom_sprite_index_ = static_cast<int>(i);
445 current_frame_ = custom_sprites_[i].editor.Frames.empty() ? -1 : 0;
447 custom_sprites_[i].animations.empty() ? -1 : 0;
450 animation_playing_ = false;
451 frame_timer_ = 0.0f;
453 }
454 }
455 }
456 ImGui::EndChild();
457
458 Separator();
459
460 // Show properties for selected sprite
463 if (gui::BeginThemedTabBar("SpriteDataTabs")) {
464 if (ImGui::BeginTabItem(tr("Properties"))) {
466 ImGui::EndTabItem();
467 }
468 if (ImGui::BeginTabItem(tr("Animations"))) {
470 ImGui::EndTabItem();
471 }
472 if (ImGui::BeginTabItem(tr("Routines"))) {
474 ImGui::EndTabItem();
475 }
477 }
478 } else {
479 Text(tr("No sprite selected"));
480 }
481}
482
484 zsprite::ZSprite new_sprite;
485 new_sprite.Reset();
486 new_sprite.sprName = "New Sprite";
487
488 // Add default frame
489 new_sprite.editor.Frames.emplace_back();
490
491 // Add default animation
492 new_sprite.animations.emplace_back(0, 0, 1, "Idle");
493
494 custom_sprites_.push_back(std::move(new_sprite));
495 custom_sprite_paths_.push_back(std::string());
496 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
497 current_frame_ = 0;
501 animation_playing_ = false;
502 frame_timer_ = 0.0f;
503 zsm_dirty_ = true;
505}
506
507void SpriteEditor::LoadZsmFile(const std::string& path) {
508 zsprite::ZSprite sprite;
509 status_ = sprite.Load(path);
510 if (status_.ok()) {
511 custom_sprites_.push_back(std::move(sprite));
512 custom_sprite_paths_.push_back(path);
513 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
514 current_frame_ = custom_sprites_.back().editor.Frames.empty() ? -1 : 0;
516 custom_sprites_.back().animations.empty() ? -1 : 0;
519 animation_playing_ = false;
520 frame_timer_ = 0.0f;
521 zsm_dirty_ = false;
523 }
524}
525
526void SpriteEditor::SaveZsmFile(const std::string& path) {
530 if (status_.ok()) {
531 SetCurrentZsmPath(path);
532 zsm_dirty_ = false;
533 }
534 }
535}
536
539 std::string path =
541 if (!path.empty()) {
542 SaveZsmFile(path);
543 }
544 }
545}
546
547// ============================================================
548// Properties Panel
549// ============================================================
550
553
554 // Basic info
555 Text(tr("Sprite Info"));
556 Separator();
557
558 static char name_buf[256];
559 CopyStringToBuffer(sprite.sprName, name_buf);
560 if (ImGui::InputText(tr("Name"), name_buf, sizeof(name_buf))) {
561 sprite.sprName = name_buf;
562 sprite.property_sprname.Text = name_buf;
564 }
565
566 static char id_buf[32];
567 CopyStringToBuffer(sprite.property_sprid.Text, id_buf);
568 if (ImGui::InputText(tr("Sprite ID"), id_buf, sizeof(id_buf))) {
569 sprite.property_sprid.Text = id_buf;
571 }
572
573 Separator();
575
576 Separator();
578}
579
582
583 Text(tr("Stats"));
584
585 // Use InputInt for numeric values
586 int prize = ParseIntOrDefault(sprite.property_prize.Text);
587 if (ImGui::InputInt(tr("Prize"), &prize)) {
588 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
590 }
591
592 int palette = ParseIntOrDefault(sprite.property_palette.Text);
593 if (ImGui::InputInt(tr("Palette"), &palette)) {
594 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
596 }
597
598 int oamnbr = ParseIntOrDefault(sprite.property_oamnbr.Text);
599 if (ImGui::InputInt(tr("OAM Count"), &oamnbr)) {
600 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
602 }
603
604 int hitbox = ParseIntOrDefault(sprite.property_hitbox.Text);
605 if (ImGui::InputInt(tr("Hitbox"), &hitbox)) {
606 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
608 }
609
610 int health = ParseIntOrDefault(sprite.property_health.Text);
611 if (ImGui::InputInt(tr("Health"), &health)) {
612 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
614 }
615
616 int damage = ParseIntOrDefault(sprite.property_damage.Text);
617 if (ImGui::InputInt(tr("Damage"), &damage)) {
618 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
620 }
621}
622
625
626 Text(tr("Behavior Flags"));
627
628 // Two columns for boolean properties
629 if (ImGui::BeginTable("BoolProps", 2, ImGuiTableFlags_None)) {
630 // Column 1
631 ImGui::TableNextColumn();
632 if (ImGui::Checkbox(tr("Blockable"), &sprite.property_blockable.IsChecked))
634 if (ImGui::Checkbox(tr("Can Fall"), &sprite.property_canfall.IsChecked))
636 if (ImGui::Checkbox(tr("Collision Layer"),
637 &sprite.property_collisionlayer.IsChecked))
639 if (ImGui::Checkbox(tr("Custom Death"),
640 &sprite.property_customdeath.IsChecked))
642 if (ImGui::Checkbox(tr("Damage Sound"),
643 &sprite.property_damagesound.IsChecked))
645 if (ImGui::Checkbox(tr("Deflect Arrows"),
646 &sprite.property_deflectarrows.IsChecked))
648 if (ImGui::Checkbox(tr("Deflect Projectiles"),
649 &sprite.property_deflectprojectiles.IsChecked))
651 if (ImGui::Checkbox(tr("Fast"), &sprite.property_fast.IsChecked))
653 if (ImGui::Checkbox(tr("Harmless"), &sprite.property_harmless.IsChecked))
655 if (ImGui::Checkbox(tr("Impervious"),
656 &sprite.property_impervious.IsChecked))
658
659 // Column 2
660 ImGui::TableNextColumn();
661 if (ImGui::Checkbox(tr("Impervious Arrow"),
662 &sprite.property_imperviousarrow.IsChecked))
664 if (ImGui::Checkbox(tr("Impervious Melee"),
665 &sprite.property_imperviousmelee.IsChecked))
667 if (ImGui::Checkbox(tr("Interaction"),
668 &sprite.property_interaction.IsChecked))
670 if (ImGui::Checkbox(tr("Is Boss"), &sprite.property_isboss.IsChecked))
672 if (ImGui::Checkbox(tr("Persist"), &sprite.property_persist.IsChecked))
674 if (ImGui::Checkbox(tr("Shadow"), &sprite.property_shadow.IsChecked))
676 if (ImGui::Checkbox(tr("Small Shadow"),
677 &sprite.property_smallshadow.IsChecked))
679 if (ImGui::Checkbox(tr("Stasis"), &sprite.property_statis.IsChecked))
681 if (ImGui::Checkbox(tr("Statue"), &sprite.property_statue.IsChecked))
683 if (ImGui::Checkbox(tr("Water Sprite"),
684 &sprite.property_watersprite.IsChecked))
686
687 ImGui::EndTable();
688 }
689}
690
691// ============================================================
692// Animation Panel
693// ============================================================
694
697
698 // Playback controls
699 if (animation_playing_) {
700 if (ImGui::Button(ICON_MD_STOP " Stop")) {
701 animation_playing_ = false;
702 }
703 } else {
704 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
705 animation_playing_ = true;
706 frame_timer_ = 0.0f;
707 }
708 }
709 ImGui::SameLine();
710 if (ImGui::Button(ICON_MD_SKIP_PREVIOUS)) {
711 if (current_frame_ > 0)
714 }
715 HOVER_HINT("Previous Frame");
716 ImGui::SameLine();
717 if (ImGui::Button(ICON_MD_SKIP_NEXT)) {
718 if (current_frame_ < (int)sprite.editor.Frames.size() - 1)
721 }
722 HOVER_HINT("Next Frame");
723 ImGui::SameLine();
724 Text(tr("Frame: %d / %d"), current_frame_,
725 (int)sprite.editor.Frames.size() - 1);
726
727 Separator();
728
729 // Animation list
730 Text(tr("Animations"));
731 if (ImGui::Button(ICON_MD_ADD " Add Animation")) {
732 int frame_count = static_cast<int>(sprite.editor.Frames.size());
733 sprite.animations.emplace_back(0, frame_count > 0 ? frame_count - 1 : 0, 1,
734 "New Animation");
736 }
737 HOVER_HINT("Add a new animation sequence");
738
739 if (ImGui::BeginChild("AnimList", ImVec2(0, 120), true)) {
740 for (size_t i = 0; i < sprite.animations.size(); i++) {
741 auto& anim = sprite.animations[i];
742 std::string label = anim.frame_name.empty() ? "Unnamed" : anim.frame_name;
743 if (Selectable(label.c_str(), current_animation_index_ == (int)i)) {
744 current_animation_index_ = static_cast<int>(i);
745 current_frame_ = anim.frame_start;
747 }
748 }
749 }
750 ImGui::EndChild();
751
752 // Edit selected animation
753 if (current_animation_index_ >= 0 &&
754 current_animation_index_ < (int)sprite.animations.size()) {
755 auto& anim = sprite.animations[current_animation_index_];
756
757 Separator();
758 Text(tr("Animation Properties"));
759
760 static char anim_name[128];
761 CopyStringToBuffer(anim.frame_name, anim_name);
762 if (ImGui::InputText(tr("Name##Anim"), anim_name, sizeof(anim_name))) {
763 anim.frame_name = anim_name;
765 }
766
767 int start = anim.frame_start;
768 int end = anim.frame_end;
769 int speed = anim.frame_speed;
770
771 if (ImGui::SliderInt(tr("Start Frame"), &start, 0,
772 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
773 anim.frame_start = static_cast<uint8_t>(start);
775 }
776 if (ImGui::SliderInt(tr("End Frame"), &end, 0,
777 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
778 anim.frame_end = static_cast<uint8_t>(end);
780 }
781 if (ImGui::SliderInt(tr("Speed"), &speed, 1, 16)) {
782 anim.frame_speed = static_cast<uint8_t>(speed);
784 }
785
786 if (ImGui::Button(tr("Delete Animation")) && sprite.animations.size() > 1) {
787 sprite.animations.erase(sprite.animations.begin() +
790 std::min(current_animation_index_, (int)sprite.animations.size() - 1);
792 }
793 HOVER_HINT("Delete the selected animation");
794 }
795
796 Separator();
798}
799
802
803 Text(tr("Frames"));
804 if (ImGui::Button(ICON_MD_ADD " Add Frame")) {
805 sprite.editor.Frames.emplace_back();
807 }
808 HOVER_HINT("Add a new animation frame");
809 ImGui::SameLine();
810 if (ImGui::Button(ICON_MD_DELETE " Delete Frame") &&
811 sprite.editor.Frames.size() > 1 && current_frame_ >= 0) {
812 sprite.editor.Frames.erase(sprite.editor.Frames.begin() + current_frame_);
814 std::min(current_frame_, (int)sprite.editor.Frames.size() - 1);
817 }
818 HOVER_HINT("Delete the current frame");
819
820 // Frame selector
821 if (ImGui::BeginChild("FrameList", ImVec2(0, 80), true,
822 ImGuiWindowFlags_HorizontalScrollbar)) {
823 for (size_t i = 0; i < sprite.editor.Frames.size(); i++) {
824 ImGui::PushID(static_cast<int>(i));
825 std::string label = absl::StrFormat("F%d", i);
826 if (Selectable(label.c_str(), current_frame_ == (int)i,
827 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
828 current_frame_ = static_cast<int>(i);
830 }
831 ImGui::SameLine();
832 ImGui::PopID();
833 }
834 }
835 ImGui::EndChild();
836
837 // Edit tiles in current frame
838 if (current_frame_ >= 0 &&
839 current_frame_ < (int)sprite.editor.Frames.size()) {
840 auto& frame = sprite.editor.Frames[current_frame_];
841
842 Separator();
843 Text(tr("Tiles in Frame %d"), current_frame_);
844
845 if (ImGui::Button(ICON_MD_ADD " Add Tile")) {
846 frame.Tiles.emplace_back();
849 }
850 HOVER_HINT("Add a new tile to this frame");
851
852 if (ImGui::BeginChild("TileList", ImVec2(0, 100), true)) {
853 for (size_t i = 0; i < frame.Tiles.size(); i++) {
854 auto& tile = frame.Tiles[i];
855 std::string label = absl::StrFormat("Tile %d (ID: %d)", i, tile.id);
856 if (Selectable(label.c_str(), selected_tile_index_ == (int)i)) {
857 selected_tile_index_ = static_cast<int>(i);
858 }
859 }
860 }
861 ImGui::EndChild();
862
863 // Edit selected tile
864 if (selected_tile_index_ >= 0 &&
865 selected_tile_index_ < (int)frame.Tiles.size()) {
866 auto& tile = frame.Tiles[selected_tile_index_];
867
868 int tile_id = tile.id;
869 if (ImGui::InputInt(tr("Tile ID"), &tile_id)) {
870 tile.id = static_cast<uint16_t>(std::clamp(tile_id, 0, 511));
873 }
874
875 int x = tile.x, y = tile.y;
876 if (ImGui::InputInt(tr("X"), &x)) {
877 tile.x = static_cast<uint8_t>(std::clamp(x, 0, 251));
880 }
881 if (ImGui::InputInt(tr("Y"), &y)) {
882 tile.y = static_cast<uint8_t>(std::clamp(y, 0, 219));
885 }
886
887 int pal = tile.palette;
888 if (ImGui::SliderInt(tr("Palette##Tile"), &pal, 0, 7)) {
889 tile.palette = static_cast<uint8_t>(pal);
892 }
893
894 if (ImGui::Checkbox(tr("16x16"), &tile.size)) {
897 }
898 ImGui::SameLine();
899 if (ImGui::Checkbox(tr("Flip X"), &tile.mirror_x)) {
902 }
903 ImGui::SameLine();
904 if (ImGui::Checkbox(tr("Flip Y"), &tile.mirror_y)) {
907 }
908
909 if (ImGui::Button(tr("Delete Tile"))) {
910 frame.Tiles.erase(frame.Tiles.begin() + selected_tile_index_);
914 }
915 HOVER_HINT("Delete the selected tile");
916 }
917 }
918}
919
923 return;
924 }
925
927 if (current_animation_index_ < 0 ||
928 current_animation_index_ >= (int)sprite.animations.size()) {
929 return;
930 }
931
932 auto& anim = sprite.animations[current_animation_index_];
933
934 frame_timer_ += delta_time;
935 float frame_duration = anim.frame_speed / 60.0f;
936
937 if (frame_timer_ >= frame_duration) {
938 frame_timer_ = 0;
940 if (current_frame_ > anim.frame_end) {
941 current_frame_ = anim.frame_start;
942 }
944 }
945}
946
947// ============================================================
948// User Routines Panel
949// ============================================================
950
953
954 if (ImGui::Button(ICON_MD_ADD " Add Routine")) {
955 sprite.userRoutines.emplace_back("New Routine", "; ASM code here\n");
957 }
958 HOVER_HINT("Add a new ASM routine");
959
960 // Routine list
961 if (ImGui::BeginChild("RoutineList", ImVec2(0, 100), true)) {
962 for (size_t i = 0; i < sprite.userRoutines.size(); i++) {
963 auto& routine = sprite.userRoutines[i];
964 if (Selectable(routine.name.c_str(), selected_routine_index_ == (int)i)) {
965 selected_routine_index_ = static_cast<int>(i);
966 }
967 }
968 }
969 ImGui::EndChild();
970
971 // Edit selected routine
972 if (selected_routine_index_ >= 0 &&
973 selected_routine_index_ < (int)sprite.userRoutines.size()) {
974 auto& routine = sprite.userRoutines[selected_routine_index_];
975
976 Separator();
977
978 static char routine_name[128];
979 CopyStringToBuffer(routine.name, routine_name);
980 if (ImGui::InputText(tr("Routine Name"), routine_name,
981 sizeof(routine_name))) {
982 routine.name = routine_name;
984 }
985
986 Text(tr("ASM Code:"));
987
988 // Multiline text input for code
989 static char code_buffer[16384];
990 CopyStringToBuffer(routine.code, code_buffer);
991 if (ImGui::InputTextMultiline("##RoutineCode", code_buffer,
992 sizeof(code_buffer), ImVec2(-1, 200))) {
993 routine.code = code_buffer;
995 }
996
997 if (ImGui::Button(tr("Delete Routine"))) {
998 sprite.userRoutines.erase(sprite.userRoutines.begin() +
1002 }
1003 HOVER_HINT("Delete the selected routine");
1004 }
1005}
1006
1007// ============================================================
1008// Graphics Pipeline
1009// ============================================================
1010
1012 // Combine selected sheets (current_sheets_[0-7]) into single 8BPP buffer
1013 // Layout: 16 tiles per row, 8 rows per sheet, 8 sheets total = 64 tile rows
1014 // Buffer size: 0x10000 bytes (65536)
1015
1016 sprite_gfx_buffer_.resize(0x10000, 0);
1017
1018 // Each sheet is 128x32 pixels (128 bytes per row, 32 rows) = 4096 bytes
1019 // We combine 8 sheets vertically: 128x256 pixels total
1020 constexpr int kSheetWidth = 128;
1021 constexpr int kSheetHeight = 32;
1022 constexpr int kRowStride = 128;
1023
1024 for (int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
1025 uint8_t sheet_id = current_sheets_[sheet_idx];
1026 if (sheet_id >= gfx::Arena::Get().gfx_sheets().size()) {
1027 continue;
1028 }
1029
1030 auto& sheet = gfx::Arena::Get().gfx_sheets().at(sheet_id);
1031 if (!sheet.is_active() || sheet.size() == 0) {
1032 continue;
1033 }
1034
1035 // Copy sheet data to buffer at appropriate offset
1036 // Each sheet occupies 8 tile rows (8 * 8 scanlines = 64 scanlines)
1037 // Offset = sheet_idx * (8 tile rows * 1024 bytes per tile row)
1038 // But sheets are 32 pixels tall (4 tile rows), so:
1039 // Offset = sheet_idx * 4 * 1024 = sheet_idx * 4096
1040 int dest_offset = sheet_idx * (kSheetHeight * kRowStride);
1041
1042 const uint8_t* src_data = sheet.data();
1043 size_t copy_size =
1044 std::min(sheet.size(), static_cast<size_t>(kSheetWidth * kSheetHeight));
1045
1046 if (dest_offset + copy_size <= sprite_gfx_buffer_.size()) {
1047 std::memcpy(sprite_gfx_buffer_.data() + dest_offset, src_data, copy_size);
1048 }
1049 }
1050
1051 // Update drawer with new buffer
1053 gfx_buffer_loaded_ = true;
1054}
1055
1057 // Load sprite palettes from ROM palette groups
1058 // ALTTP sprites use a combination of palette groups:
1059 // - Rows 0-1: Global sprite palettes (shared by all sprites)
1060 // - Rows 2-7: Aux palettes (vary by sprite type)
1061 //
1062 // For simplicity, we load global_sprites which contains the main
1063 // sprite palettes. More accurate rendering would require looking up
1064 // which aux palette group each sprite type uses.
1065
1066 if (!rom_ || !rom_->is_loaded()) {
1067 return;
1068 }
1069
1070 // Build combined sprite palette from global + aux groups
1072
1073 // Add global sprite palettes (typically 2 palettes, 16 colors each)
1074 if (!game_data())
1075 return;
1076 const auto& global = game_data()->palette_groups.global_sprites;
1077 for (size_t i = 0; i < global.size() && i < 8; i++) {
1078 sprite_palettes_.AddPalette(global.palette(i));
1079 }
1080
1081 // If we don't have 8 palettes yet, fill with aux palettes
1082 const auto& aux1 = game_data()->palette_groups.sprites_aux1;
1083 const auto& aux2 = game_data()->palette_groups.sprites_aux2;
1084 const auto& aux3 = game_data()->palette_groups.sprites_aux3;
1085
1086 // Pad to 8 palettes total for proper OAM palette mapping
1087 while (sprite_palettes_.size() < 8) {
1088 if (sprite_palettes_.size() < 4 && aux1.size() > 0) {
1090 aux1.palette(sprite_palettes_.size() % aux1.size()));
1091 } else if (sprite_palettes_.size() < 6 && aux2.size() > 0) {
1093 aux2.palette((sprite_palettes_.size() - 4) % aux2.size()));
1094 } else if (aux3.size() > 0) {
1096 aux3.palette((sprite_palettes_.size() - 6) % aux3.size()));
1097 } else {
1098 // Fallback: add empty palette
1100 }
1101 }
1102
1104}
1105
1106void SpriteEditor::LoadSheetsForSprite(const std::array<uint8_t, 4>& sheets) {
1107 // Load the required sheets for a vanilla sprite
1108 bool changed = false;
1109 for (int i = 0; i < 4; i++) {
1110 if (sheets[i] != 0 && current_sheets_[i] != sheets[i]) {
1111 current_sheets_[i] = sheets[i];
1112 changed = true;
1113 }
1114 }
1115
1116 if (changed) {
1117 gfx_buffer_loaded_ = false;
1119 }
1120}
1121
1123 // Ensure graphics buffer is loaded
1127 }
1128
1129 // Initialize vanilla preview bitmap if needed. The helper also queues a
1130 // CREATE texture command so canvas_rendering doesn't silently skip the
1131 // draw, and stamps the bitmap as kCompositeOutput.
1134
1136 return;
1137 }
1138
1139 // Clear and render
1141
1142 // Origin is center of bitmap
1143 int origin_x = 64;
1144 int origin_y = 64;
1145
1146 // Convert SpriteOamLayout tiles to zsprite::OamTile and draw
1147 for (const auto& entry : layout.tiles) {
1148 zsprite::OamTile tile;
1149 tile.x = static_cast<uint8_t>(entry.x_offset + 128); // Convert to unsigned
1150 tile.y = static_cast<uint8_t>(entry.y_offset + 128);
1151 tile.id = entry.tile_id;
1152 tile.palette = entry.palette;
1153 tile.size = entry.size_16x16;
1154 tile.mirror_x = entry.flip_x;
1155 tile.mirror_y = entry.flip_y;
1156 tile.priority = 0;
1157
1159 origin_y);
1160 }
1161
1162 // Build combined 128-color palette (8 sub-palettes × 16 colors)
1163 // and apply to bitmap for proper color rendering
1164 if (sprite_palettes_.size() > 0) {
1165 gfx::SnesPalette combined_palette;
1166 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1167 pal_idx++) {
1168 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1169 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1170 col_idx++) {
1171 combined_palette.AddColor(sub_pal[col_idx]);
1172 }
1173 // Pad to 16 if sub-palette is smaller
1174 while (combined_palette.size() < (pal_idx + 1) * 16) {
1175 combined_palette.AddColor(gfx::SnesColor(0));
1176 }
1177 }
1178 vanilla_preview_bitmap_.SetPalette(combined_palette);
1179 }
1180
1181 // Surface pixels and palette were just mutated above; queue an UPDATE so
1182 // the GPU texture reflects the new state on the next frame. Without this,
1183 // the texture stays frozen at first-CREATE state forever.
1185
1187}
1188
1189// ============================================================
1190// Canvas Rendering
1191// ============================================================
1192
1196 return;
1197 }
1198
1200 if (frame_index < 0 || frame_index >= (int)sprite.editor.Frames.size()) {
1201 return;
1202 }
1203
1204 auto& frame = sprite.editor.Frames[frame_index];
1205
1206 // Ensure graphics buffer is loaded
1210 }
1211
1212 // The sprite_canvas_ displays a post-render composite (pixels come from
1213 // SpriteDrawer, not direct user paint). Declaring the role lets future
1214 // canvas surfaces (cursor hints, context-menu items) default appropriately.
1216
1217 // Initialize preview bitmap if needed. The helper also queues a CREATE
1218 // texture command so canvas_rendering doesn't silently skip the draw, and
1219 // stamps the bitmap as kCompositeOutput.
1222
1223 // Only render if drawer is ready
1225 // Clear and render to preview bitmap
1227
1228 // Origin is center of canvas (128, 128 for 256x256 bitmap)
1230
1231 // Build combined 128-color palette and apply to bitmap
1232 if (sprite_palettes_.size() > 0) {
1233 gfx::SnesPalette combined_palette;
1234 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1235 pal_idx++) {
1236 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1237 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1238 col_idx++) {
1239 combined_palette.AddColor(sub_pal[col_idx]);
1240 }
1241 // Pad to 16 if sub-palette is smaller
1242 while (combined_palette.size() < (pal_idx + 1) * 16) {
1243 combined_palette.AddColor(gfx::SnesColor(0));
1244 }
1245 }
1246 sprite_preview_bitmap_.SetPalette(combined_palette);
1247 }
1248
1249 // Surface pixels and palette were just mutated above; queue an UPDATE so
1250 // the GPU texture reflects the new frame on the next paint.
1252
1253 // Mark as updated
1254 preview_needs_update_ = false;
1255 }
1256
1257 // Draw the preview bitmap on canvas
1260 }
1261
1262 // Draw tile outlines for selection (over the bitmap)
1263 if (show_tile_grid_) {
1264 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1265 const auto& tile = frame.Tiles[i];
1266 int tile_size = tile.size ? 16 : 8;
1267
1268 // Convert signed tile position to canvas position
1269 int8_t signed_x = static_cast<int8_t>(tile.x);
1270 int8_t signed_y = static_cast<int8_t>(tile.y);
1271
1272 int canvas_x = 128 + signed_x;
1273 int canvas_y = 128 + signed_y;
1274
1275 // Highlight selected tile
1276 ImVec4 color = (selected_tile_index_ == static_cast<int>(i))
1277 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f) // Green for selected
1278 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f); // Yellow for others
1279
1280 sprite_canvas_.DrawRect(canvas_x, canvas_y, tile_size, tile_size, color);
1281 }
1282 }
1283}
1284
1286 if (ImGui::BeginChild(gui::GetID("##ZSpriteCanvas"),
1287 ImGui::GetContentRegionAvail(), true)) {
1290
1291 // Render current frame if we have a sprite selected
1295 }
1296
1299
1300 // Display current frame info
1303 ImGui::SetCursorPos(ImVec2(10, 10));
1304 Text(tr("Frame: %d | Tiles: %d"), current_frame_,
1305 current_frame_ < (int)sprite.editor.Frames.size()
1306 ? (int)sprite.editor.Frames[current_frame_].Tiles.size()
1307 : 0);
1308 }
1309 }
1310 ImGui::EndChild();
1311}
1312
1313// ============================================================
1314// Undo/Redo Helpers
1315// ============================================================
1316
1328
1330 if (snapshot.sprite_index < 0 ||
1331 snapshot.sprite_index >= static_cast<int>(custom_sprites_.size())) {
1332 return;
1333 }
1335 current_frame_ = snapshot.current_frame;
1337 custom_sprites_[snapshot.sprite_index] = snapshot.sprite_data;
1338 preview_needs_update_ = true;
1339 zsm_dirty_ = true;
1340}
1341
1344 return; // Already in a transaction
1345 }
1348}
1349
1352 return;
1353 }
1354
1356 // No mutation happened; discard the pending snapshot so we can
1357 // capture a fresh one next frame.
1358 undo_snapshot_pending_ = false;
1359 return;
1360 }
1361
1362 undo_snapshot_pending_ = false;
1364
1365 auto after = CaptureCurrentSpriteSnapshot();
1366 auto restore_fn = [this](const SpriteSnapshot& snapshot) {
1367 RestoreFromSnapshot(snapshot);
1368 };
1369 undo_manager_.Push(std::make_unique<SpriteEditAction>(
1370 std::move(undo_before_snapshot_), std::move(after),
1371 std::move(restore_fn)));
1372}
1373
1378
1380 if (custom_sprite_paths_.size() < custom_sprites_.size()) {
1382 }
1383}
1384
1385const std::string& SpriteEditor::GetCurrentZsmPath() const {
1386 static const std::string kEmptyPath;
1389 static_cast<int>(custom_sprite_paths_.size())) {
1390 return kEmptyPath;
1391 }
1393}
1394
1395void SpriteEditor::SetCurrentZsmPath(const std::string& path) {
1399 static_cast<int>(custom_sprite_paths_.size())) {
1400 return;
1401 }
1403}
1404
1405} // namespace editor
1406} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:162
bool is_loaded() const
Definition rom.h:144
UndoManager undo_manager_
Definition editor.h:334
zelda3::GameData * game_data() const
Definition editor.h:320
EditorDependencies dependencies_
Definition editor.h:333
void SetPalettes(const gfx::PaletteGroup *palettes)
Set the palette group for color mapping.
void ClearBitmap(gfx::Bitmap &bitmap)
Clear the bitmap with transparent color.
void DrawFrame(gfx::Bitmap &bitmap, const zsprite::Frame &frame, int origin_x, int origin_y)
Draw all tiles in a ZSM frame.
bool IsReady() const
Check if drawer is ready to render.
void DrawOamTile(gfx::Bitmap &bitmap, const zsprite::OamTile &tile, int origin_x, int origin_y)
Draw a single ZSM OAM tile to bitmap.
void SetGraphicsBuffer(const uint8_t *buffer)
Set the graphics buffer for tile lookup.
ImVector< int > active_sprites_
void UpdateAnimationPlayback(float delta_time)
void RenderZSpriteFrame(int frame_index)
void LoadSheetsForSprite(const std::array< uint8_t, 4 > &sheets)
absl::Status Update() override
gfx::PaletteGroup sprite_palettes_
void LoadZsmFile(const std::string &path)
void RenderVanillaSprite(const zelda3::SpriteOamLayout &layout)
const std::string & GetCurrentZsmPath() const
void RestoreFromSnapshot(const SpriteSnapshot &snapshot)
std::vector< zsprite::ZSprite > custom_sprites_
std::vector< uint8_t > sprite_gfx_buffer_
void SetCurrentZsmPath(const std::string &path)
absl::Status Save() override
SpriteSnapshot undo_before_snapshot_
void SaveZsmFile(const std::string &path)
SpriteSnapshot CaptureCurrentSpriteSnapshot() const
absl::Status Load() override
std::vector< std::string > custom_sprite_paths_
void Push(std::unique_ptr< UndoAction > action)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:152
static Arena & Get()
Definition arena.cc:21
bool is_active() const
Definition bitmap.h:405
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
void UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
Definition bitmap.cc:297
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)
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1162
void DrawContextMenu()
Definition canvas.cc:692
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1098
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1427
CanvasConfig & GetConfig()
Definition canvas.h:229
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:602
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1484
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static const SpriteOamLayout * GetLayout(uint8_t sprite_id)
Get the OAM layout for a sprite ID.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_SAVE_AS
Definition icons.h:1646
#define ICON_MD_STOP
Definition icons.h:1862
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_SKIP_NEXT
Definition icons.h:1773
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_SKIP_PREVIOUS
Definition icons.h:1774
#define HOVER_HINT(string)
Definition macro.h:24
int ParseIntOrDefault(const std::string &text, int fallback=0)
void EnsureSpritePreviewBitmapReady(gfx::Bitmap &bmp, int width, int height, int depth, const std::vector< uint8_t > &gfx_buffer)
constexpr ImGuiTabItemFlags kSpriteTabFlags
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:355
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
ImGuiID GetID(const std::string &id)
Definition input.cc:570
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:376
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
const std::string kSpriteDefaultNames[256]
Definition sprite.cc:13
WorkspaceWindowManager * window_manager
Definition editor.h:181
Snapshot of a custom ZSprite's editable state for undo/redo.
std::vector< Frame > Frames
Definition zsprite.h:120
void Reset()
Reset all sprite data to defaults.
Definition zsprite.h:352
absl::Status Load(const std::string &filename)
Load a ZSM file from disk.
Definition zsprite.h:134
std::vector< AnimationGroup > animations
Definition zsprite.h:392
auto palette(int i) const
void AddPalette(SnesPalette pal)
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2341
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
Complete OAM layout for a vanilla sprite.
std::vector< SpriteOamEntry > tiles