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
3#include <algorithm>
4#include <cerrno>
5#include <cstdlib>
6#include <cstring>
7
8#include "absl/strings/str_format.h"
15#include "app/gui/core/icons.h"
16#include "app/gui/core/input.h"
19#include "util/file_util.h"
20#include "util/hex.h"
21#include "util/macro.h"
23
24namespace yaze {
25namespace editor {
26
27using ImGui::BeginTable;
28using ImGui::Button;
29using ImGui::EndTable;
30using ImGui::Selectable;
31using ImGui::Separator;
32using ImGui::TableHeadersRow;
33using ImGui::TableNextColumn;
34using ImGui::TableNextRow;
35using ImGui::TableSetupColumn;
36using ImGui::Text;
37
38namespace {
39template <size_t N>
40void CopyStringToBuffer(const std::string& src, char (&dest)[N]) {
41 std::strncpy(dest, src.c_str(), N - 1);
42 dest[N - 1] = '\0';
43}
44
45int ParseIntOrDefault(const std::string& text, int fallback = 0) {
46 if (text.empty()) {
47 return fallback;
48 }
49 errno = 0;
50 char* end = nullptr;
51 long value = std::strtol(text.c_str(), &end, 0);
52 if (end == text.c_str() || errno == ERANGE) {
53 return fallback;
54 }
55 return static_cast<int>(value);
56}
57} // namespace
58
61 return;
62 auto* window_manager = dependencies_.window_manager;
63
64 // Register WindowContent implementations with callbacks
65 // EditorPanels provide both metadata (icon, name, priority) and drawing logic
66 window_manager->RegisterWindowContent(
67 std::make_unique<VanillaSpriteEditorPanel>([this]() {
68 if (rom_ && rom_->is_loaded()) {
69 DrawVanillaSpriteEditor();
70 } else {
71 ImGui::TextDisabled("Load a ROM to view vanilla sprites");
72 }
73 }));
74
75 window_manager->RegisterWindowContent(std::make_unique<CustomSpriteEditorPanel>(
76 [this]() { DrawCustomSprites(); }));
77}
78
79absl::Status SpriteEditor::Load() {
80 gfx::ScopedTimer timer("SpriteEditor::Load");
81 return absl::OkStatus();
82}
83
84absl::Status SpriteEditor::Update() {
85 if (rom()->is_loaded() && !sheets_loaded_) {
86 sheets_loaded_ = true;
87 }
88
89 // Update animation playback for custom sprites
90 float current_time = ImGui::GetTime();
91 float delta_time = current_time - last_frame_time_;
92 last_frame_time_ = current_time;
93 UpdateAnimationPlayback(delta_time);
94
95 // Handle editor-level shortcuts
97
98 // Panel drawing is handled by WorkspaceWindowManager via registered EditorPanels
99 // Each panel's Draw() callback invokes the appropriate draw method
100
101 // Commit any pending undo transaction at the end of the frame
103
104 return status_.ok() ? absl::OkStatus() : status_;
105}
106
108 // Animation playback shortcuts (when custom sprite panel is active)
109 if (ImGui::IsKeyPressed(ImGuiKey_Space, false) &&
110 !ImGui::GetIO().WantTextInput) {
112 }
113
114 // Frame navigation
115 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket, false)) {
116 if (current_frame_ > 0) {
119 }
120 }
121 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket, false)) {
124 }
125
126 // Sprite navigation
127 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
128 if (current_sprite_id_ > 0) {
131 }
132 }
133 if (ImGui::GetIO().KeyCtrl &&
134 ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
137 }
138}
139
140absl::Status SpriteEditor::Save() {
142 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
143 const std::string& zsm_path = GetCurrentZsmPath();
144 if (zsm_path.empty()) {
146 } else {
147 SaveZsmFile(zsm_path);
148 }
149 }
150 return absl::OkStatus();
151}
152
154 // Sidebar handled by EditorManager for card-based editors
155}
156
157// ============================================================
158// Vanilla Sprite Editor
159// ============================================================
160
162 if (ImGui::BeginTable("##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
163 ImVec2(0, 0))) {
164 TableSetupColumn("Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
165 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch,
166 ImGui::GetContentRegionAvail().x);
167 TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
168 TableHeadersRow();
169 TableNextRow();
170
171 TableNextColumn();
173
174 TableNextColumn();
175 static int next_tab_id = 0;
176
177 if (gui::BeginThemedTabBar("SpriteTabBar", kSpriteTabBarFlags)) {
178 if (ImGui::TabItemButton(ICON_MD_ADD, kSpriteTabFlags)) {
179 if (std::find(active_sprites_.begin(), active_sprites_.end(),
181 next_tab_id++;
182 }
183 active_sprites_.push_back(next_tab_id++);
184 }
185
186 for (int n = 0; n < active_sprites_.Size;) {
187 bool open = true;
188
189 if (active_sprites_[n] > sizeof(zelda3::kSpriteDefaultNames) / 4) {
190 active_sprites_.erase(active_sprites_.Data + n);
191 continue;
192 }
193
194 if (ImGui::BeginTabItem(
196 ImGuiTabItemFlags_None)) {
198 ImGui::EndTabItem();
199 }
200
201 if (!open)
202 active_sprites_.erase(active_sprites_.Data + n);
203 else
204 n++;
205 }
206
208 }
209
210 TableNextColumn();
211 if (sheets_loaded_) {
213 }
214 ImGui::EndTable();
215 }
216}
217
219 static bool flip_x = false;
220 static bool flip_y = false;
221 if (ImGui::BeginChild(gui::GetID("##SpriteCanvas"),
222 ImGui::GetContentRegionAvail(), true)) {
225
226 // Render vanilla sprite if layout exists
227 if (current_sprite_id_ >= 0) {
228 const auto* layout = zelda3::SpriteOamRegistry::GetLayout(
229 static_cast<uint8_t>(current_sprite_id_));
230 if (layout) {
231 // Load required sheets for this sprite
232 LoadSheetsForSprite(layout->required_sheets);
233 RenderVanillaSprite(*layout);
234
235 // Draw the preview bitmap centered on canvas
238 }
239
240 // Show sprite info
241 ImGui::SetCursorPos(ImVec2(10, 10));
242 Text("Sprite: %s (0x%02X)", layout->name, layout->sprite_id);
243 Text("Tiles: %zu", layout->tiles.size());
244 }
245 }
246
249
250 if (ImGui::BeginTable("##OAMTable", 7, ImGuiTableFlags_Resizable,
251 ImVec2(0, 0))) {
252 TableSetupColumn("X", ImGuiTableColumnFlags_WidthStretch);
253 TableSetupColumn("Y", ImGuiTableColumnFlags_WidthStretch);
254 TableSetupColumn("Tile", ImGuiTableColumnFlags_WidthStretch);
255 TableSetupColumn("Palette", ImGuiTableColumnFlags_WidthStretch);
256 TableSetupColumn("Priority", ImGuiTableColumnFlags_WidthStretch);
257 TableSetupColumn("Flip X", ImGuiTableColumnFlags_WidthStretch);
258 TableSetupColumn("Flip Y", ImGuiTableColumnFlags_WidthStretch);
259 TableHeadersRow();
260 TableNextRow();
261
262 TableNextColumn();
264
265 TableNextColumn();
267
268 TableNextColumn();
270
271 TableNextColumn();
273
274 TableNextColumn();
276
277 TableNextColumn();
278 if (ImGui::Checkbox("##XFlip", &flip_x)) {
279 oam_config_.flip_x = flip_x;
280 }
281
282 TableNextColumn();
283 if (ImGui::Checkbox("##YFlip", &flip_y)) {
284 oam_config_.flip_y = flip_y;
285 }
286
287 ImGui::EndTable();
288 }
289
291 }
292 ImGui::EndChild();
293}
294
296 if (ImGui::BeginChild(gui::GetID("sheet_label"),
297 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
298 ImGuiWindowFlags_NoDecoration)) {
299 // Track previous sheet values for change detection
300 static uint8_t prev_sheets[8] = {0};
301 bool sheets_changed = false;
302
303 for (int i = 0; i < 8; i++) {
304 std::string sheet_label = absl::StrFormat("Sheet %d", i);
305 if (gui::InputHexByte(sheet_label.c_str(), &current_sheets_[i])) {
306 sheets_changed = true;
307 }
308 if (i % 2 == 0)
309 ImGui::SameLine();
310 }
311
312 // Reload graphics buffer if sheets changed
313 if (sheets_changed || std::memcmp(prev_sheets, current_sheets_, 8) != 0) {
314 std::memcpy(prev_sheets, current_sheets_, 8);
315 gfx_buffer_loaded_ = false;
317 }
318
322 for (int i = 0; i < 8; i++) {
324 gfx::Arena::Get().gfx_sheets().at(current_sheets_[i]), 1,
325 (i * 0x40) + 1, 2);
326 }
329 }
330 ImGui::EndChild();
331}
332
334 if (ImGui::BeginChild(gui::GetID("##SpritesList"),
335 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
336 ImGuiWindowFlags_NoDecoration)) {
337 int i = 0;
338 for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) {
340 current_sprite_id_ == i, "Sprite Names", util::HexByte(i),
342 if (ImGui::IsItemClicked()) {
343 if (current_sprite_id_ != i) {
346 }
347 if (!active_sprites_.contains(i)) {
348 active_sprites_.push_back(i);
349 }
350 }
351 i++;
352 }
353 }
354 ImGui::EndChild();
355}
356
358 if (ImGui::Button("Add Frame")) {
359 // Add a new frame
360 }
361 if (ImGui::Button("Remove Frame")) {
362 // Remove the current frame
363 }
364}
365
366// ============================================================
367// Custom ZSM Sprite Editor
368// ============================================================
369
371 if (BeginTable("##CustomSpritesTable", 3,
372 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
373 ImVec2(0, 0))) {
374 TableSetupColumn("Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
375 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch);
376 TableSetupColumn("Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
377
378 TableHeadersRow();
379 TableNextRow();
380 TableNextColumn();
381
383
384 TableNextColumn();
386
387 TableNextColumn();
389
390 EndTable();
391 }
392}
393
395 // Capture undo snapshot before any widgets can mutate sprite data.
396 // CommitUndoTransaction() in Update() will push the action only if
397 // a mutation actually occurred (zsm_dirty_ was set).
399 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
401 }
402
403 // File operations toolbar
404 if (ImGui::Button(ICON_MD_ADD " New")) {
406 }
407 ImGui::SameLine();
408 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
409 std::string file_path = util::FileDialogWrapper::ShowOpenFileDialog();
410 if (!file_path.empty()) {
411 LoadZsmFile(file_path);
412 }
413 }
414 ImGui::SameLine();
415 if (ImGui::Button(ICON_MD_SAVE " Save")) {
417 const std::string& zsm_path = GetCurrentZsmPath();
418 if (zsm_path.empty()) {
420 } else {
421 SaveZsmFile(zsm_path);
422 }
423 }
424 }
425 ImGui::SameLine();
426 if (ImGui::Button(ICON_MD_SAVE_AS " Save As")) {
428 }
429
430 Separator();
431
432 // Sprite list
433 Text("Loaded Sprites:");
434 if (ImGui::BeginChild("SpriteList", ImVec2(0, 100), true)) {
435 for (size_t i = 0; i < custom_sprites_.size(); i++) {
436 std::string label = custom_sprites_[i].sprName.empty()
437 ? "Unnamed Sprite"
438 : custom_sprites_[i].sprName;
439 if (Selectable(label.c_str(), current_custom_sprite_index_ == (int)i)) {
440 current_custom_sprite_index_ = static_cast<int>(i);
441 current_frame_ = custom_sprites_[i].editor.Frames.empty() ? -1 : 0;
443 custom_sprites_[i].animations.empty() ? -1 : 0;
446 animation_playing_ = false;
447 frame_timer_ = 0.0f;
449 }
450 }
451 }
452 ImGui::EndChild();
453
454 Separator();
455
456 // Show properties for selected sprite
459 if (gui::BeginThemedTabBar("SpriteDataTabs")) {
460 if (ImGui::BeginTabItem("Properties")) {
462 ImGui::EndTabItem();
463 }
464 if (ImGui::BeginTabItem("Animations")) {
466 ImGui::EndTabItem();
467 }
468 if (ImGui::BeginTabItem("Routines")) {
470 ImGui::EndTabItem();
471 }
473 }
474 } else {
475 Text("No sprite selected");
476 }
477}
478
480 zsprite::ZSprite new_sprite;
481 new_sprite.Reset();
482 new_sprite.sprName = "New Sprite";
483
484 // Add default frame
485 new_sprite.editor.Frames.emplace_back();
486
487 // Add default animation
488 new_sprite.animations.emplace_back(0, 0, 1, "Idle");
489
490 custom_sprites_.push_back(std::move(new_sprite));
491 custom_sprite_paths_.push_back(std::string());
492 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
493 current_frame_ = 0;
497 animation_playing_ = false;
498 frame_timer_ = 0.0f;
499 zsm_dirty_ = true;
501}
502
503void SpriteEditor::LoadZsmFile(const std::string& path) {
504 zsprite::ZSprite sprite;
505 status_ = sprite.Load(path);
506 if (status_.ok()) {
507 custom_sprites_.push_back(std::move(sprite));
508 custom_sprite_paths_.push_back(path);
509 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
510 current_frame_ = custom_sprites_.back().editor.Frames.empty() ? -1 : 0;
512 custom_sprites_.back().animations.empty() ? -1 : 0;
515 animation_playing_ = false;
516 frame_timer_ = 0.0f;
517 zsm_dirty_ = false;
519 }
520}
521
522void SpriteEditor::SaveZsmFile(const std::string& path) {
526 if (status_.ok()) {
527 SetCurrentZsmPath(path);
528 zsm_dirty_ = false;
529 }
530 }
531}
532
535 std::string path =
537 if (!path.empty()) {
538 SaveZsmFile(path);
539 }
540 }
541}
542
543// ============================================================
544// Properties Panel
545// ============================================================
546
549
550 // Basic info
551 Text("Sprite Info");
552 Separator();
553
554 static char name_buf[256];
555 CopyStringToBuffer(sprite.sprName, name_buf);
556 if (ImGui::InputText("Name", name_buf, sizeof(name_buf))) {
557 sprite.sprName = name_buf;
558 sprite.property_sprname.Text = name_buf;
560 }
561
562 static char id_buf[32];
563 CopyStringToBuffer(sprite.property_sprid.Text, id_buf);
564 if (ImGui::InputText("Sprite ID", id_buf, sizeof(id_buf))) {
565 sprite.property_sprid.Text = id_buf;
567 }
568
569 Separator();
571
572 Separator();
574}
575
578
579 Text("Stats");
580
581 // Use InputInt for numeric values
582 int prize = ParseIntOrDefault(sprite.property_prize.Text);
583 if (ImGui::InputInt("Prize", &prize)) {
584 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
586 }
587
588 int palette = ParseIntOrDefault(sprite.property_palette.Text);
589 if (ImGui::InputInt("Palette", &palette)) {
590 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
592 }
593
594 int oamnbr = ParseIntOrDefault(sprite.property_oamnbr.Text);
595 if (ImGui::InputInt("OAM Count", &oamnbr)) {
596 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
598 }
599
600 int hitbox = ParseIntOrDefault(sprite.property_hitbox.Text);
601 if (ImGui::InputInt("Hitbox", &hitbox)) {
602 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
604 }
605
606 int health = ParseIntOrDefault(sprite.property_health.Text);
607 if (ImGui::InputInt("Health", &health)) {
608 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
610 }
611
612 int damage = ParseIntOrDefault(sprite.property_damage.Text);
613 if (ImGui::InputInt("Damage", &damage)) {
614 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
616 }
617}
618
621
622 Text("Behavior Flags");
623
624 // Two columns for boolean properties
625 if (ImGui::BeginTable("BoolProps", 2, ImGuiTableFlags_None)) {
626 // Column 1
627 ImGui::TableNextColumn();
628 if (ImGui::Checkbox("Blockable", &sprite.property_blockable.IsChecked))
630 if (ImGui::Checkbox("Can Fall", &sprite.property_canfall.IsChecked))
632 if (ImGui::Checkbox("Collision Layer",
633 &sprite.property_collisionlayer.IsChecked))
635 if (ImGui::Checkbox("Custom Death", &sprite.property_customdeath.IsChecked))
637 if (ImGui::Checkbox("Damage Sound", &sprite.property_damagesound.IsChecked))
639 if (ImGui::Checkbox("Deflect Arrows",
640 &sprite.property_deflectarrows.IsChecked))
642 if (ImGui::Checkbox("Deflect Projectiles",
643 &sprite.property_deflectprojectiles.IsChecked))
645 if (ImGui::Checkbox("Fast", &sprite.property_fast.IsChecked))
647 if (ImGui::Checkbox("Harmless", &sprite.property_harmless.IsChecked))
649 if (ImGui::Checkbox("Impervious", &sprite.property_impervious.IsChecked))
651
652 // Column 2
653 ImGui::TableNextColumn();
654 if (ImGui::Checkbox("Impervious Arrow",
655 &sprite.property_imperviousarrow.IsChecked))
657 if (ImGui::Checkbox("Impervious Melee",
658 &sprite.property_imperviousmelee.IsChecked))
660 if (ImGui::Checkbox("Interaction", &sprite.property_interaction.IsChecked))
662 if (ImGui::Checkbox("Is Boss", &sprite.property_isboss.IsChecked))
664 if (ImGui::Checkbox("Persist", &sprite.property_persist.IsChecked))
666 if (ImGui::Checkbox("Shadow", &sprite.property_shadow.IsChecked))
668 if (ImGui::Checkbox("Small Shadow", &sprite.property_smallshadow.IsChecked))
670 if (ImGui::Checkbox("Stasis", &sprite.property_statis.IsChecked))
672 if (ImGui::Checkbox("Statue", &sprite.property_statue.IsChecked))
674 if (ImGui::Checkbox("Water Sprite", &sprite.property_watersprite.IsChecked))
676
677 ImGui::EndTable();
678 }
679}
680
681// ============================================================
682// Animation Panel
683// ============================================================
684
687
688 // Playback controls
689 if (animation_playing_) {
690 if (ImGui::Button(ICON_MD_STOP " Stop")) {
691 animation_playing_ = false;
692 }
693 } else {
694 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
695 animation_playing_ = true;
696 frame_timer_ = 0.0f;
697 }
698 }
699 ImGui::SameLine();
700 if (ImGui::Button(ICON_MD_SKIP_PREVIOUS)) {
701 if (current_frame_ > 0)
704 }
705 HOVER_HINT("Previous Frame");
706 ImGui::SameLine();
707 if (ImGui::Button(ICON_MD_SKIP_NEXT)) {
708 if (current_frame_ < (int)sprite.editor.Frames.size() - 1)
711 }
712 HOVER_HINT("Next Frame");
713 ImGui::SameLine();
714 Text("Frame: %d / %d", current_frame_, (int)sprite.editor.Frames.size() - 1);
715
716 Separator();
717
718 // Animation list
719 Text("Animations");
720 if (ImGui::Button(ICON_MD_ADD " Add Animation")) {
721 int frame_count = static_cast<int>(sprite.editor.Frames.size());
722 sprite.animations.emplace_back(0, frame_count > 0 ? frame_count - 1 : 0, 1,
723 "New Animation");
725 }
726 HOVER_HINT("Add a new animation sequence");
727
728 if (ImGui::BeginChild("AnimList", ImVec2(0, 120), true)) {
729 for (size_t i = 0; i < sprite.animations.size(); i++) {
730 auto& anim = sprite.animations[i];
731 std::string label = anim.frame_name.empty() ? "Unnamed" : anim.frame_name;
732 if (Selectable(label.c_str(), current_animation_index_ == (int)i)) {
733 current_animation_index_ = static_cast<int>(i);
734 current_frame_ = anim.frame_start;
736 }
737 }
738 }
739 ImGui::EndChild();
740
741 // Edit selected animation
742 if (current_animation_index_ >= 0 &&
743 current_animation_index_ < (int)sprite.animations.size()) {
744 auto& anim = sprite.animations[current_animation_index_];
745
746 Separator();
747 Text("Animation Properties");
748
749 static char anim_name[128];
750 CopyStringToBuffer(anim.frame_name, anim_name);
751 if (ImGui::InputText("Name##Anim", anim_name, sizeof(anim_name))) {
752 anim.frame_name = anim_name;
754 }
755
756 int start = anim.frame_start;
757 int end = anim.frame_end;
758 int speed = anim.frame_speed;
759
760 if (ImGui::SliderInt("Start Frame", &start, 0,
761 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
762 anim.frame_start = static_cast<uint8_t>(start);
764 }
765 if (ImGui::SliderInt("End Frame", &end, 0,
766 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
767 anim.frame_end = static_cast<uint8_t>(end);
769 }
770 if (ImGui::SliderInt("Speed", &speed, 1, 16)) {
771 anim.frame_speed = static_cast<uint8_t>(speed);
773 }
774
775 if (ImGui::Button("Delete Animation") && sprite.animations.size() > 1) {
776 sprite.animations.erase(sprite.animations.begin() +
779 std::min(current_animation_index_, (int)sprite.animations.size() - 1);
781 }
782 HOVER_HINT("Delete the selected animation");
783 }
784
785 Separator();
787}
788
791
792 Text("Frames");
793 if (ImGui::Button(ICON_MD_ADD " Add Frame")) {
794 sprite.editor.Frames.emplace_back();
796 }
797 HOVER_HINT("Add a new animation frame");
798 ImGui::SameLine();
799 if (ImGui::Button(ICON_MD_DELETE " Delete Frame") &&
800 sprite.editor.Frames.size() > 1 && current_frame_ >= 0) {
801 sprite.editor.Frames.erase(sprite.editor.Frames.begin() + current_frame_);
803 std::min(current_frame_, (int)sprite.editor.Frames.size() - 1);
806 }
807 HOVER_HINT("Delete the current frame");
808
809 // Frame selector
810 if (ImGui::BeginChild("FrameList", ImVec2(0, 80), true,
811 ImGuiWindowFlags_HorizontalScrollbar)) {
812 for (size_t i = 0; i < sprite.editor.Frames.size(); i++) {
813 ImGui::PushID(static_cast<int>(i));
814 std::string label = absl::StrFormat("F%d", i);
815 if (Selectable(label.c_str(), current_frame_ == (int)i,
816 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
817 current_frame_ = static_cast<int>(i);
819 }
820 ImGui::SameLine();
821 ImGui::PopID();
822 }
823 }
824 ImGui::EndChild();
825
826 // Edit tiles in current frame
827 if (current_frame_ >= 0 &&
828 current_frame_ < (int)sprite.editor.Frames.size()) {
829 auto& frame = sprite.editor.Frames[current_frame_];
830
831 Separator();
832 Text("Tiles in Frame %d", current_frame_);
833
834 if (ImGui::Button(ICON_MD_ADD " Add Tile")) {
835 frame.Tiles.emplace_back();
838 }
839 HOVER_HINT("Add a new tile to this frame");
840
841 if (ImGui::BeginChild("TileList", ImVec2(0, 100), true)) {
842 for (size_t i = 0; i < frame.Tiles.size(); i++) {
843 auto& tile = frame.Tiles[i];
844 std::string label = absl::StrFormat("Tile %d (ID: %d)", i, tile.id);
845 if (Selectable(label.c_str(), selected_tile_index_ == (int)i)) {
846 selected_tile_index_ = static_cast<int>(i);
847 }
848 }
849 }
850 ImGui::EndChild();
851
852 // Edit selected tile
853 if (selected_tile_index_ >= 0 &&
854 selected_tile_index_ < (int)frame.Tiles.size()) {
855 auto& tile = frame.Tiles[selected_tile_index_];
856
857 int tile_id = tile.id;
858 if (ImGui::InputInt("Tile ID", &tile_id)) {
859 tile.id = static_cast<uint16_t>(std::clamp(tile_id, 0, 511));
862 }
863
864 int x = tile.x, y = tile.y;
865 if (ImGui::InputInt("X", &x)) {
866 tile.x = static_cast<uint8_t>(std::clamp(x, 0, 251));
869 }
870 if (ImGui::InputInt("Y", &y)) {
871 tile.y = static_cast<uint8_t>(std::clamp(y, 0, 219));
874 }
875
876 int pal = tile.palette;
877 if (ImGui::SliderInt("Palette##Tile", &pal, 0, 7)) {
878 tile.palette = static_cast<uint8_t>(pal);
881 }
882
883 if (ImGui::Checkbox("16x16", &tile.size)) {
886 }
887 ImGui::SameLine();
888 if (ImGui::Checkbox("Flip X", &tile.mirror_x)) {
891 }
892 ImGui::SameLine();
893 if (ImGui::Checkbox("Flip Y", &tile.mirror_y)) {
896 }
897
898 if (ImGui::Button("Delete Tile")) {
899 frame.Tiles.erase(frame.Tiles.begin() + selected_tile_index_);
903 }
904 HOVER_HINT("Delete the selected tile");
905 }
906 }
907}
908
912 return;
913 }
914
916 if (current_animation_index_ < 0 ||
917 current_animation_index_ >= (int)sprite.animations.size()) {
918 return;
919 }
920
921 auto& anim = sprite.animations[current_animation_index_];
922
923 frame_timer_ += delta_time;
924 float frame_duration = anim.frame_speed / 60.0f;
925
926 if (frame_timer_ >= frame_duration) {
927 frame_timer_ = 0;
929 if (current_frame_ > anim.frame_end) {
930 current_frame_ = anim.frame_start;
931 }
933 }
934}
935
936// ============================================================
937// User Routines Panel
938// ============================================================
939
942
943 if (ImGui::Button(ICON_MD_ADD " Add Routine")) {
944 sprite.userRoutines.emplace_back("New Routine", "; ASM code here\n");
946 }
947 HOVER_HINT("Add a new ASM routine");
948
949 // Routine list
950 if (ImGui::BeginChild("RoutineList", ImVec2(0, 100), true)) {
951 for (size_t i = 0; i < sprite.userRoutines.size(); i++) {
952 auto& routine = sprite.userRoutines[i];
953 if (Selectable(routine.name.c_str(), selected_routine_index_ == (int)i)) {
954 selected_routine_index_ = static_cast<int>(i);
955 }
956 }
957 }
958 ImGui::EndChild();
959
960 // Edit selected routine
961 if (selected_routine_index_ >= 0 &&
962 selected_routine_index_ < (int)sprite.userRoutines.size()) {
963 auto& routine = sprite.userRoutines[selected_routine_index_];
964
965 Separator();
966
967 static char routine_name[128];
968 CopyStringToBuffer(routine.name, routine_name);
969 if (ImGui::InputText("Routine Name", routine_name, sizeof(routine_name))) {
970 routine.name = routine_name;
972 }
973
974 Text("ASM Code:");
975
976 // Multiline text input for code
977 static char code_buffer[16384];
978 CopyStringToBuffer(routine.code, code_buffer);
979 if (ImGui::InputTextMultiline("##RoutineCode", code_buffer,
980 sizeof(code_buffer), ImVec2(-1, 200))) {
981 routine.code = code_buffer;
983 }
984
985 if (ImGui::Button("Delete Routine")) {
986 sprite.userRoutines.erase(sprite.userRoutines.begin() +
990 }
991 HOVER_HINT("Delete the selected routine");
992 }
993}
994
995// ============================================================
996// Graphics Pipeline
997// ============================================================
998
1000 // Combine selected sheets (current_sheets_[0-7]) into single 8BPP buffer
1001 // Layout: 16 tiles per row, 8 rows per sheet, 8 sheets total = 64 tile rows
1002 // Buffer size: 0x10000 bytes (65536)
1003
1004 sprite_gfx_buffer_.resize(0x10000, 0);
1005
1006 // Each sheet is 128x32 pixels (128 bytes per row, 32 rows) = 4096 bytes
1007 // We combine 8 sheets vertically: 128x256 pixels total
1008 constexpr int kSheetWidth = 128;
1009 constexpr int kSheetHeight = 32;
1010 constexpr int kRowStride = 128;
1011
1012 for (int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
1013 uint8_t sheet_id = current_sheets_[sheet_idx];
1014 if (sheet_id >= gfx::Arena::Get().gfx_sheets().size()) {
1015 continue;
1016 }
1017
1018 auto& sheet = gfx::Arena::Get().gfx_sheets().at(sheet_id);
1019 if (!sheet.is_active() || sheet.size() == 0) {
1020 continue;
1021 }
1022
1023 // Copy sheet data to buffer at appropriate offset
1024 // Each sheet occupies 8 tile rows (8 * 8 scanlines = 64 scanlines)
1025 // Offset = sheet_idx * (8 tile rows * 1024 bytes per tile row)
1026 // But sheets are 32 pixels tall (4 tile rows), so:
1027 // Offset = sheet_idx * 4 * 1024 = sheet_idx * 4096
1028 int dest_offset = sheet_idx * (kSheetHeight * kRowStride);
1029
1030 const uint8_t* src_data = sheet.data();
1031 size_t copy_size =
1032 std::min(sheet.size(), static_cast<size_t>(kSheetWidth * kSheetHeight));
1033
1034 if (dest_offset + copy_size <= sprite_gfx_buffer_.size()) {
1035 std::memcpy(sprite_gfx_buffer_.data() + dest_offset, src_data, copy_size);
1036 }
1037 }
1038
1039 // Update drawer with new buffer
1041 gfx_buffer_loaded_ = true;
1042}
1043
1045 // Load sprite palettes from ROM palette groups
1046 // ALTTP sprites use a combination of palette groups:
1047 // - Rows 0-1: Global sprite palettes (shared by all sprites)
1048 // - Rows 2-7: Aux palettes (vary by sprite type)
1049 //
1050 // For simplicity, we load global_sprites which contains the main
1051 // sprite palettes. More accurate rendering would require looking up
1052 // which aux palette group each sprite type uses.
1053
1054 if (!rom_ || !rom_->is_loaded()) {
1055 return;
1056 }
1057
1058 // Build combined sprite palette from global + aux groups
1060
1061 // Add global sprite palettes (typically 2 palettes, 16 colors each)
1062 if (!game_data())
1063 return;
1064 const auto& global = game_data()->palette_groups.global_sprites;
1065 for (size_t i = 0; i < global.size() && i < 8; i++) {
1066 sprite_palettes_.AddPalette(global.palette(i));
1067 }
1068
1069 // If we don't have 8 palettes yet, fill with aux palettes
1070 const auto& aux1 = game_data()->palette_groups.sprites_aux1;
1071 const auto& aux2 = game_data()->palette_groups.sprites_aux2;
1072 const auto& aux3 = game_data()->palette_groups.sprites_aux3;
1073
1074 // Pad to 8 palettes total for proper OAM palette mapping
1075 while (sprite_palettes_.size() < 8) {
1076 if (sprite_palettes_.size() < 4 && aux1.size() > 0) {
1078 aux1.palette(sprite_palettes_.size() % aux1.size()));
1079 } else if (sprite_palettes_.size() < 6 && aux2.size() > 0) {
1081 aux2.palette((sprite_palettes_.size() - 4) % aux2.size()));
1082 } else if (aux3.size() > 0) {
1084 aux3.palette((sprite_palettes_.size() - 6) % aux3.size()));
1085 } else {
1086 // Fallback: add empty palette
1088 }
1089 }
1090
1092}
1093
1094void SpriteEditor::LoadSheetsForSprite(const std::array<uint8_t, 4>& sheets) {
1095 // Load the required sheets for a vanilla sprite
1096 bool changed = false;
1097 for (int i = 0; i < 4; i++) {
1098 if (sheets[i] != 0 && current_sheets_[i] != sheets[i]) {
1099 current_sheets_[i] = sheets[i];
1100 changed = true;
1101 }
1102 }
1103
1104 if (changed) {
1105 gfx_buffer_loaded_ = false;
1107 }
1108}
1109
1111 // Ensure graphics buffer is loaded
1115 }
1116
1117 // Initialize vanilla preview bitmap if needed
1121 }
1122
1124 return;
1125 }
1126
1127 // Clear and render
1129
1130 // Origin is center of bitmap
1131 int origin_x = 64;
1132 int origin_y = 64;
1133
1134 // Convert SpriteOamLayout tiles to zsprite::OamTile and draw
1135 for (const auto& entry : layout.tiles) {
1136 zsprite::OamTile tile;
1137 tile.x = static_cast<uint8_t>(entry.x_offset + 128); // Convert to unsigned
1138 tile.y = static_cast<uint8_t>(entry.y_offset + 128);
1139 tile.id = entry.tile_id;
1140 tile.palette = entry.palette;
1141 tile.size = entry.size_16x16;
1142 tile.mirror_x = entry.flip_x;
1143 tile.mirror_y = entry.flip_y;
1144 tile.priority = 0;
1145
1147 origin_y);
1148 }
1149
1150 // Build combined 128-color palette (8 sub-palettes × 16 colors)
1151 // and apply to bitmap for proper color rendering
1152 if (sprite_palettes_.size() > 0) {
1153 gfx::SnesPalette combined_palette;
1154 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1155 pal_idx++) {
1156 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1157 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1158 col_idx++) {
1159 combined_palette.AddColor(sub_pal[col_idx]);
1160 }
1161 // Pad to 16 if sub-palette is smaller
1162 while (combined_palette.size() < (pal_idx + 1) * 16) {
1163 combined_palette.AddColor(gfx::SnesColor(0));
1164 }
1165 }
1166 vanilla_preview_bitmap_.SetPalette(combined_palette);
1167 }
1168
1170}
1171
1172// ============================================================
1173// Canvas Rendering
1174// ============================================================
1175
1179 return;
1180 }
1181
1183 if (frame_index < 0 || frame_index >= (int)sprite.editor.Frames.size()) {
1184 return;
1185 }
1186
1187 auto& frame = sprite.editor.Frames[frame_index];
1188
1189 // Ensure graphics buffer is loaded
1193 }
1194
1195 // Initialize preview bitmap if needed
1199 }
1200
1201 // Only render if drawer is ready
1203 // Clear and render to preview bitmap
1205
1206 // Origin is center of canvas (128, 128 for 256x256 bitmap)
1208
1209 // Build combined 128-color palette and apply to bitmap
1210 if (sprite_palettes_.size() > 0) {
1211 gfx::SnesPalette combined_palette;
1212 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1213 pal_idx++) {
1214 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1215 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1216 col_idx++) {
1217 combined_palette.AddColor(sub_pal[col_idx]);
1218 }
1219 // Pad to 16 if sub-palette is smaller
1220 while (combined_palette.size() < (pal_idx + 1) * 16) {
1221 combined_palette.AddColor(gfx::SnesColor(0));
1222 }
1223 }
1224 sprite_preview_bitmap_.SetPalette(combined_palette);
1225 }
1226
1227 // Mark as updated
1228 preview_needs_update_ = false;
1229 }
1230
1231 // Draw the preview bitmap on canvas
1234 }
1235
1236 // Draw tile outlines for selection (over the bitmap)
1237 if (show_tile_grid_) {
1238 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1239 const auto& tile = frame.Tiles[i];
1240 int tile_size = tile.size ? 16 : 8;
1241
1242 // Convert signed tile position to canvas position
1243 int8_t signed_x = static_cast<int8_t>(tile.x);
1244 int8_t signed_y = static_cast<int8_t>(tile.y);
1245
1246 int canvas_x = 128 + signed_x;
1247 int canvas_y = 128 + signed_y;
1248
1249 // Highlight selected tile
1250 ImVec4 color = (selected_tile_index_ == static_cast<int>(i))
1251 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f) // Green for selected
1252 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f); // Yellow for others
1253
1254 sprite_canvas_.DrawRect(canvas_x, canvas_y, tile_size, tile_size, color);
1255 }
1256 }
1257}
1258
1260 if (ImGui::BeginChild(gui::GetID("##ZSpriteCanvas"),
1261 ImGui::GetContentRegionAvail(), true)) {
1264
1265 // Render current frame if we have a sprite selected
1269 }
1270
1273
1274 // Display current frame info
1277 ImGui::SetCursorPos(ImVec2(10, 10));
1278 Text("Frame: %d | Tiles: %d", current_frame_,
1279 current_frame_ < (int)sprite.editor.Frames.size()
1280 ? (int)sprite.editor.Frames[current_frame_].Tiles.size()
1281 : 0);
1282 }
1283 }
1284 ImGui::EndChild();
1285}
1286
1287// ============================================================
1288// Undo/Redo Helpers
1289// ============================================================
1290
1302
1304 if (snapshot.sprite_index < 0 ||
1305 snapshot.sprite_index >= static_cast<int>(custom_sprites_.size())) {
1306 return;
1307 }
1309 current_frame_ = snapshot.current_frame;
1311 custom_sprites_[snapshot.sprite_index] = snapshot.sprite_data;
1312 preview_needs_update_ = true;
1313 zsm_dirty_ = true;
1314}
1315
1318 return; // Already in a transaction
1319 }
1322}
1323
1326 return;
1327 }
1328
1330 // No mutation happened; discard the pending snapshot so we can
1331 // capture a fresh one next frame.
1332 undo_snapshot_pending_ = false;
1333 return;
1334 }
1335
1336 undo_snapshot_pending_ = false;
1338
1339 auto after = CaptureCurrentSpriteSnapshot();
1340 auto restore_fn = [this](const SpriteSnapshot& snapshot) {
1341 RestoreFromSnapshot(snapshot);
1342 };
1343 undo_manager_.Push(std::make_unique<SpriteEditAction>(
1344 std::move(undo_before_snapshot_), std::move(after),
1345 std::move(restore_fn)));
1346}
1347
1352
1354 if (custom_sprite_paths_.size() < custom_sprites_.size()) {
1356 }
1357}
1358
1359const std::string& SpriteEditor::GetCurrentZsmPath() const {
1360 static const std::string kEmptyPath;
1363 static_cast<int>(custom_sprite_paths_.size())) {
1364 return kEmptyPath;
1365 }
1367}
1368
1369void SpriteEditor::SetCurrentZsmPath(const std::string& path) {
1373 static_cast<int>(custom_sprite_paths_.size())) {
1374 return;
1375 }
1377}
1378
1379} // namespace editor
1380} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:150
bool is_loaded() const
Definition rom.h:132
UndoManager undo_manager_
Definition editor.h:317
zelda3::GameData * game_data() const
Definition editor.h:307
EditorDependencies dependencies_
Definition editor.h:316
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
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
void Reformat(int format)
Reformat the bitmap to use a different pixel format.
Definition bitmap.cc:277
bool is_active() const
Definition bitmap.h:384
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
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:1157
void DrawContextMenu()
Definition canvas.cc:684
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1093
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1423
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:590
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1480
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)
constexpr ImGuiTabItemFlags kSpriteTabFlags
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:354
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:583
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:380
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:176
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:2168
gfx::PaletteGroupMap palette_groups
Definition game_data.h:91
Complete OAM layout for a vanilla sprite.
std::vector< SpriteOamEntry > tiles