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 <cstring>
5
11#include "app/gui/core/icons.h"
12#include "app/gui/core/input.h"
14#include "util/file_util.h"
15#include "util/hex.h"
17
18namespace yaze {
19namespace editor {
20
21using ImGui::BeginTable;
22using ImGui::Button;
23using ImGui::EndTable;
24using ImGui::Selectable;
25using ImGui::Separator;
26using ImGui::TableHeadersRow;
27using ImGui::TableNextColumn;
28using ImGui::TableNextRow;
29using ImGui::TableSetupColumn;
30using ImGui::Text;
31
34 return;
35 auto* panel_manager = dependencies_.panel_manager;
36
37 // Register EditorPanel implementations with callbacks
38 // EditorPanels provide both metadata (icon, name, priority) and drawing logic
39 panel_manager->RegisterEditorPanel(std::make_unique<VanillaSpriteEditorPanel>(
40 [this]() {
41 if (rom_ && rom_->is_loaded()) {
42 DrawVanillaSpriteEditor();
43 } else {
44 ImGui::TextDisabled("Load a ROM to view vanilla sprites");
45 }
46 }));
47
48 panel_manager->RegisterEditorPanel(std::make_unique<CustomSpriteEditorPanel>(
49 [this]() { DrawCustomSprites(); }));
50}
51
52absl::Status SpriteEditor::Load() {
53 gfx::ScopedTimer timer("SpriteEditor::Load");
54 return absl::OkStatus();
55}
56
57absl::Status SpriteEditor::Update() {
58 if (rom()->is_loaded() && !sheets_loaded_) {
59 sheets_loaded_ = true;
60 }
61
62 // Update animation playback for custom sprites
63 float current_time = ImGui::GetTime();
64 float delta_time = current_time - last_frame_time_;
65 last_frame_time_ = current_time;
66 UpdateAnimationPlayback(delta_time);
67
68 // Handle editor-level shortcuts
70
71 // Panel drawing is handled by PanelManager via registered EditorPanels
72 // Each panel's Draw() callback invokes the appropriate draw method
73
74 return status_.ok() ? absl::OkStatus() : status_;
75}
76
78 // Animation playback shortcuts (when custom sprite panel is active)
79 if (ImGui::IsKeyPressed(ImGuiKey_Space, false) && !ImGui::GetIO().WantTextInput) {
81 }
82
83 // Frame navigation
84 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket, false)) {
85 if (current_frame_ > 0) {
88 }
89 }
90 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket, false)) {
93 }
94
95 // Sprite navigation
96 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
97 if (current_sprite_id_ > 0) {
100 }
101 }
102 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
105 }
106}
107
108absl::Status SpriteEditor::Save() {
110 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
111 if (current_zsm_path_.empty()) {
113 } else {
115 }
116 }
117 return absl::OkStatus();
118}
119
121 // Sidebar handled by EditorManager for card-based editors
122}
123
124// ============================================================
125// Vanilla Sprite Editor
126// ============================================================
127
129 if (ImGui::BeginTable("##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
130 ImVec2(0, 0))) {
131 TableSetupColumn("Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
132 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch,
133 ImGui::GetContentRegionAvail().x);
134 TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
135 TableHeadersRow();
136 TableNextRow();
137
138 TableNextColumn();
140
141 TableNextColumn();
142 static int next_tab_id = 0;
143
144 if (ImGui::BeginTabBar("SpriteTabBar", kSpriteTabBarFlags)) {
145 if (ImGui::TabItemButton(ICON_MD_ADD, kSpriteTabBarFlags)) {
146 if (std::find(active_sprites_.begin(), active_sprites_.end(),
148 next_tab_id++;
149 }
150 active_sprites_.push_back(next_tab_id++);
151 }
152
153 for (int n = 0; n < active_sprites_.Size;) {
154 bool open = true;
155
156 if (active_sprites_[n] > sizeof(zelda3::kSpriteDefaultNames) / 4) {
157 active_sprites_.erase(active_sprites_.Data + n);
158 continue;
159 }
160
161 if (ImGui::BeginTabItem(
163 ImGuiTabItemFlags_None)) {
165 ImGui::EndTabItem();
166 }
167
168 if (!open)
169 active_sprites_.erase(active_sprites_.Data + n);
170 else
171 n++;
172 }
173
174 ImGui::EndTabBar();
175 }
176
177 TableNextColumn();
178 if (sheets_loaded_) {
180 }
181 ImGui::EndTable();
182 }
183}
184
186 static bool flip_x = false;
187 static bool flip_y = false;
188 if (ImGui::BeginChild(gui::GetID("##SpriteCanvas"),
189 ImGui::GetContentRegionAvail(), true)) {
192
193 // Render vanilla sprite if layout exists
194 if (current_sprite_id_ >= 0) {
195 const auto* layout =
197 if (layout) {
198 // Load required sheets for this sprite
199 LoadSheetsForSprite(layout->required_sheets);
200 RenderVanillaSprite(*layout);
201
202 // Draw the preview bitmap centered on canvas
205 }
206
207 // Show sprite info
208 ImGui::SetCursorPos(ImVec2(10, 10));
209 Text("Sprite: %s (0x%02X)", layout->name, layout->sprite_id);
210 Text("Tiles: %zu", layout->tiles.size());
211 }
212 }
213
216
217 if (ImGui::BeginTable("##OAMTable", 7, ImGuiTableFlags_Resizable,
218 ImVec2(0, 0))) {
219 TableSetupColumn("X", ImGuiTableColumnFlags_WidthStretch);
220 TableSetupColumn("Y", ImGuiTableColumnFlags_WidthStretch);
221 TableSetupColumn("Tile", ImGuiTableColumnFlags_WidthStretch);
222 TableSetupColumn("Palette", ImGuiTableColumnFlags_WidthStretch);
223 TableSetupColumn("Priority", ImGuiTableColumnFlags_WidthStretch);
224 TableSetupColumn("Flip X", ImGuiTableColumnFlags_WidthStretch);
225 TableSetupColumn("Flip Y", ImGuiTableColumnFlags_WidthStretch);
226 TableHeadersRow();
227 TableNextRow();
228
229 TableNextColumn();
231
232 TableNextColumn();
234
235 TableNextColumn();
237
238 TableNextColumn();
240
241 TableNextColumn();
243
244 TableNextColumn();
245 if (ImGui::Checkbox("##XFlip", &flip_x)) {
246 oam_config_.flip_x = flip_x;
247 }
248
249 TableNextColumn();
250 if (ImGui::Checkbox("##YFlip", &flip_y)) {
251 oam_config_.flip_y = flip_y;
252 }
253
254 ImGui::EndTable();
255 }
256
258 }
259 ImGui::EndChild();
260}
261
263 if (ImGui::BeginChild(gui::GetID("sheet_label"),
264 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
265 ImGuiWindowFlags_NoDecoration)) {
266 // Track previous sheet values for change detection
267 static uint8_t prev_sheets[8] = {0};
268 bool sheets_changed = false;
269
270 for (int i = 0; i < 8; i++) {
271 std::string sheet_label = absl::StrFormat("Sheet %d", i);
272 if (gui::InputHexByte(sheet_label.c_str(), &current_sheets_[i])) {
273 sheets_changed = true;
274 }
275 if (i % 2 == 0)
276 ImGui::SameLine();
277 }
278
279 // Reload graphics buffer if sheets changed
280 if (sheets_changed || std::memcmp(prev_sheets, current_sheets_, 8) != 0) {
281 std::memcpy(prev_sheets, current_sheets_, 8);
282 gfx_buffer_loaded_ = false;
284 }
285
289 for (int i = 0; i < 8; i++) {
291 gfx::Arena::Get().gfx_sheets().at(current_sheets_[i]), 1,
292 (i * 0x40) + 1, 2);
293 }
296 }
297 ImGui::EndChild();
298}
299
301 if (ImGui::BeginChild(gui::GetID("##SpritesList"),
302 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
303 ImGuiWindowFlags_NoDecoration)) {
304 int i = 0;
305 for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) {
307 current_sprite_id_ == i, "Sprite Names", util::HexByte(i),
309 if (ImGui::IsItemClicked()) {
310 if (current_sprite_id_ != i) {
313 }
314 if (!active_sprites_.contains(i)) {
315 active_sprites_.push_back(i);
316 }
317 }
318 i++;
319 }
320 }
321 ImGui::EndChild();
322}
323
325 if (ImGui::Button("Add Frame")) {
326 // Add a new frame
327 }
328 if (ImGui::Button("Remove Frame")) {
329 // Remove the current frame
330 }
331}
332
333// ============================================================
334// Custom ZSM Sprite Editor
335// ============================================================
336
338 if (BeginTable("##CustomSpritesTable", 3,
339 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
340 ImVec2(0, 0))) {
341 TableSetupColumn("Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
342 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch);
343 TableSetupColumn("Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
344
345 TableHeadersRow();
346 TableNextRow();
347 TableNextColumn();
348
350
351 TableNextColumn();
353
354 TableNextColumn();
356
357 EndTable();
358 }
359}
360
362 // File operations toolbar
363 if (ImGui::Button(ICON_MD_ADD " New")) {
365 }
366 ImGui::SameLine();
367 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
368 std::string file_path = util::FileDialogWrapper::ShowOpenFileDialog();
369 if (!file_path.empty()) {
370 LoadZsmFile(file_path);
371 }
372 }
373 ImGui::SameLine();
374 if (ImGui::Button(ICON_MD_SAVE " Save")) {
376 if (current_zsm_path_.empty()) {
378 } else {
380 }
381 }
382 }
383 ImGui::SameLine();
384 if (ImGui::Button(ICON_MD_SAVE_AS " Save As")) {
386 }
387
388 Separator();
389
390 // Sprite list
391 Text("Loaded Sprites:");
392 if (ImGui::BeginChild("SpriteList", ImVec2(0, 100), true)) {
393 for (size_t i = 0; i < custom_sprites_.size(); i++) {
394 std::string label = custom_sprites_[i].sprName.empty()
395 ? "Unnamed Sprite"
396 : custom_sprites_[i].sprName;
397 if (Selectable(label.c_str(), current_custom_sprite_index_ == (int)i)) {
398 current_custom_sprite_index_ = static_cast<int>(i);
400 }
401 }
402 }
403 ImGui::EndChild();
404
405 Separator();
406
407 // Show properties for selected sprite
410 if (ImGui::BeginTabBar("SpriteDataTabs")) {
411 if (ImGui::BeginTabItem("Properties")) {
413 ImGui::EndTabItem();
414 }
415 if (ImGui::BeginTabItem("Animations")) {
417 ImGui::EndTabItem();
418 }
419 if (ImGui::BeginTabItem("Routines")) {
421 ImGui::EndTabItem();
422 }
423 ImGui::EndTabBar();
424 }
425 } else {
426 Text("No sprite selected");
427 }
428}
429
431 zsprite::ZSprite new_sprite;
432 new_sprite.Reset();
433 new_sprite.sprName = "New Sprite";
434
435 // Add default frame
436 new_sprite.editor.Frames.emplace_back();
437
438 // Add default animation
439 new_sprite.animations.emplace_back(0, 0, 1, "Idle");
440
441 custom_sprites_.push_back(std::move(new_sprite));
442 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
443 current_zsm_path_.clear();
444 zsm_dirty_ = true;
446}
447
448void SpriteEditor::LoadZsmFile(const std::string& path) {
449 zsprite::ZSprite sprite;
450 status_ = sprite.Load(path);
451 if (status_.ok()) {
452 custom_sprites_.push_back(std::move(sprite));
453 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
454 current_zsm_path_ = path;
455 zsm_dirty_ = false;
457 }
458}
459
460void SpriteEditor::SaveZsmFile(const std::string& path) {
464 if (status_.ok()) {
465 current_zsm_path_ = path;
466 zsm_dirty_ = false;
467 }
468 }
469}
470
473 std::string path =
475 if (!path.empty()) {
476 SaveZsmFile(path);
477 }
478 }
479}
480
481// ============================================================
482// Properties Panel
483// ============================================================
484
487
488 // Basic info
489 Text("Sprite Info");
490 Separator();
491
492 static char name_buf[256];
493 strncpy(name_buf, sprite.sprName.c_str(), sizeof(name_buf) - 1);
494 if (ImGui::InputText("Name", name_buf, sizeof(name_buf))) {
495 sprite.sprName = name_buf;
496 sprite.property_sprname.Text = name_buf;
497 zsm_dirty_ = true;
498 }
499
500 static char id_buf[32];
501 strncpy(id_buf, sprite.property_sprid.Text.c_str(), sizeof(id_buf) - 1);
502 if (ImGui::InputText("Sprite ID", id_buf, sizeof(id_buf))) {
503 sprite.property_sprid.Text = id_buf;
504 zsm_dirty_ = true;
505 }
506
507 Separator();
509
510 Separator();
512}
513
516
517 Text("Stats");
518
519 // Use InputInt for numeric values
520 int prize = sprite.property_prize.Text.empty()
521 ? 0
522 : std::stoi(sprite.property_prize.Text);
523 if (ImGui::InputInt("Prize", &prize)) {
524 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
525 zsm_dirty_ = true;
526 }
527
528 int palette = sprite.property_palette.Text.empty()
529 ? 0
530 : std::stoi(sprite.property_palette.Text);
531 if (ImGui::InputInt("Palette", &palette)) {
532 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
533 zsm_dirty_ = true;
534 }
535
536 int oamnbr = sprite.property_oamnbr.Text.empty()
537 ? 0
538 : std::stoi(sprite.property_oamnbr.Text);
539 if (ImGui::InputInt("OAM Count", &oamnbr)) {
540 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
541 zsm_dirty_ = true;
542 }
543
544 int hitbox = sprite.property_hitbox.Text.empty()
545 ? 0
546 : std::stoi(sprite.property_hitbox.Text);
547 if (ImGui::InputInt("Hitbox", &hitbox)) {
548 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
549 zsm_dirty_ = true;
550 }
551
552 int health = sprite.property_health.Text.empty()
553 ? 0
554 : std::stoi(sprite.property_health.Text);
555 if (ImGui::InputInt("Health", &health)) {
556 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
557 zsm_dirty_ = true;
558 }
559
560 int damage = sprite.property_damage.Text.empty()
561 ? 0
562 : std::stoi(sprite.property_damage.Text);
563 if (ImGui::InputInt("Damage", &damage)) {
564 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
565 zsm_dirty_ = true;
566 }
567}
568
571
572 Text("Behavior Flags");
573
574 // Two columns for boolean properties
575 if (ImGui::BeginTable("BoolProps", 2, ImGuiTableFlags_None)) {
576 // Column 1
577 ImGui::TableNextColumn();
578 if (ImGui::Checkbox("Blockable", &sprite.property_blockable.IsChecked))
579 zsm_dirty_ = true;
580 if (ImGui::Checkbox("Can Fall", &sprite.property_canfall.IsChecked))
581 zsm_dirty_ = true;
582 if (ImGui::Checkbox("Collision Layer",
583 &sprite.property_collisionlayer.IsChecked))
584 zsm_dirty_ = true;
585 if (ImGui::Checkbox("Custom Death", &sprite.property_customdeath.IsChecked))
586 zsm_dirty_ = true;
587 if (ImGui::Checkbox("Damage Sound", &sprite.property_damagesound.IsChecked))
588 zsm_dirty_ = true;
589 if (ImGui::Checkbox("Deflect Arrows",
590 &sprite.property_deflectarrows.IsChecked))
591 zsm_dirty_ = true;
592 if (ImGui::Checkbox("Deflect Projectiles",
593 &sprite.property_deflectprojectiles.IsChecked))
594 zsm_dirty_ = true;
595 if (ImGui::Checkbox("Fast", &sprite.property_fast.IsChecked))
596 zsm_dirty_ = true;
597 if (ImGui::Checkbox("Harmless", &sprite.property_harmless.IsChecked))
598 zsm_dirty_ = true;
599 if (ImGui::Checkbox("Impervious", &sprite.property_impervious.IsChecked))
600 zsm_dirty_ = true;
601
602 // Column 2
603 ImGui::TableNextColumn();
604 if (ImGui::Checkbox("Impervious Arrow",
605 &sprite.property_imperviousarrow.IsChecked))
606 zsm_dirty_ = true;
607 if (ImGui::Checkbox("Impervious Melee",
608 &sprite.property_imperviousmelee.IsChecked))
609 zsm_dirty_ = true;
610 if (ImGui::Checkbox("Interaction", &sprite.property_interaction.IsChecked))
611 zsm_dirty_ = true;
612 if (ImGui::Checkbox("Is Boss", &sprite.property_isboss.IsChecked))
613 zsm_dirty_ = true;
614 if (ImGui::Checkbox("Persist", &sprite.property_persist.IsChecked))
615 zsm_dirty_ = true;
616 if (ImGui::Checkbox("Shadow", &sprite.property_shadow.IsChecked))
617 zsm_dirty_ = true;
618 if (ImGui::Checkbox("Small Shadow",
619 &sprite.property_smallshadow.IsChecked))
620 zsm_dirty_ = true;
621 if (ImGui::Checkbox("Stasis", &sprite.property_statis.IsChecked))
622 zsm_dirty_ = true;
623 if (ImGui::Checkbox("Statue", &sprite.property_statue.IsChecked))
624 zsm_dirty_ = true;
625 if (ImGui::Checkbox("Water Sprite",
626 &sprite.property_watersprite.IsChecked))
627 zsm_dirty_ = true;
628
629 ImGui::EndTable();
630 }
631}
632
633// ============================================================
634// Animation Panel
635// ============================================================
636
639
640 // Playback controls
641 if (animation_playing_) {
642 if (ImGui::Button(ICON_MD_STOP " Stop")) {
643 animation_playing_ = false;
644 }
645 } else {
646 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
647 animation_playing_ = true;
648 frame_timer_ = 0.0f;
649 }
650 }
651 ImGui::SameLine();
652 if (ImGui::Button(ICON_MD_SKIP_PREVIOUS)) {
655 }
656 ImGui::SameLine();
657 if (ImGui::Button(ICON_MD_SKIP_NEXT)) {
658 if (current_frame_ < (int)sprite.editor.Frames.size() - 1) current_frame_++;
660 }
661 ImGui::SameLine();
662 Text("Frame: %d / %d", current_frame_,
663 (int)sprite.editor.Frames.size() - 1);
664
665 Separator();
666
667 // Animation list
668 Text("Animations");
669 if (ImGui::Button(ICON_MD_ADD " Add Animation")) {
670 int frame_count = static_cast<int>(sprite.editor.Frames.size());
671 sprite.animations.emplace_back(0, frame_count > 0 ? frame_count - 1 : 0, 1,
672 "New Animation");
673 zsm_dirty_ = true;
674 }
675
676 if (ImGui::BeginChild("AnimList", ImVec2(0, 120), true)) {
677 for (size_t i = 0; i < sprite.animations.size(); i++) {
678 auto& anim = sprite.animations[i];
679 std::string label =
680 anim.frame_name.empty() ? "Unnamed" : anim.frame_name;
681 if (Selectable(label.c_str(), current_animation_index_ == (int)i)) {
682 current_animation_index_ = static_cast<int>(i);
683 current_frame_ = anim.frame_start;
685 }
686 }
687 }
688 ImGui::EndChild();
689
690 // Edit selected animation
691 if (current_animation_index_ >= 0 &&
692 current_animation_index_ < (int)sprite.animations.size()) {
693 auto& anim = sprite.animations[current_animation_index_];
694
695 Separator();
696 Text("Animation Properties");
697
698 static char anim_name[128];
699 strncpy(anim_name, anim.frame_name.c_str(), sizeof(anim_name) - 1);
700 if (ImGui::InputText("Name##Anim", anim_name, sizeof(anim_name))) {
701 anim.frame_name = anim_name;
702 zsm_dirty_ = true;
703 }
704
705 int start = anim.frame_start;
706 int end = anim.frame_end;
707 int speed = anim.frame_speed;
708
709 if (ImGui::SliderInt("Start Frame", &start, 0,
710 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
711 anim.frame_start = static_cast<uint8_t>(start);
712 zsm_dirty_ = true;
713 }
714 if (ImGui::SliderInt("End Frame", &end, 0,
715 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
716 anim.frame_end = static_cast<uint8_t>(end);
717 zsm_dirty_ = true;
718 }
719 if (ImGui::SliderInt("Speed", &speed, 1, 16)) {
720 anim.frame_speed = static_cast<uint8_t>(speed);
721 zsm_dirty_ = true;
722 }
723
724 if (ImGui::Button("Delete Animation") && sprite.animations.size() > 1) {
725 sprite.animations.erase(sprite.animations.begin() +
729 (int)sprite.animations.size() - 1);
730 zsm_dirty_ = true;
731 }
732 }
733
734 Separator();
736}
737
740
741 Text("Frames");
742 if (ImGui::Button(ICON_MD_ADD " Add Frame")) {
743 sprite.editor.Frames.emplace_back();
744 zsm_dirty_ = true;
745 }
746 ImGui::SameLine();
747 if (ImGui::Button(ICON_MD_DELETE " Delete Frame") &&
748 sprite.editor.Frames.size() > 1 && current_frame_ >= 0) {
749 sprite.editor.Frames.erase(sprite.editor.Frames.begin() + current_frame_);
751 std::min(current_frame_, (int)sprite.editor.Frames.size() - 1);
752 zsm_dirty_ = true;
754 }
755
756 // Frame selector
757 if (ImGui::BeginChild("FrameList", ImVec2(0, 80), true,
758 ImGuiWindowFlags_HorizontalScrollbar)) {
759 for (size_t i = 0; i < sprite.editor.Frames.size(); i++) {
760 ImGui::PushID(static_cast<int>(i));
761 std::string label = absl::StrFormat("F%d", i);
762 if (Selectable(label.c_str(), current_frame_ == (int)i,
763 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
764 current_frame_ = static_cast<int>(i);
766 }
767 ImGui::SameLine();
768 ImGui::PopID();
769 }
770 }
771 ImGui::EndChild();
772
773 // Edit tiles in current frame
774 if (current_frame_ >= 0 && current_frame_ < (int)sprite.editor.Frames.size()) {
775 auto& frame = sprite.editor.Frames[current_frame_];
776
777 Separator();
778 Text("Tiles in Frame %d", current_frame_);
779
780 if (ImGui::Button(ICON_MD_ADD " Add Tile")) {
781 frame.Tiles.emplace_back();
782 zsm_dirty_ = true;
784 }
785
786 if (ImGui::BeginChild("TileList", ImVec2(0, 100), true)) {
787 for (size_t i = 0; i < frame.Tiles.size(); i++) {
788 auto& tile = frame.Tiles[i];
789 std::string label = absl::StrFormat("Tile %d (ID: %d)", i, tile.id);
790 if (Selectable(label.c_str(), selected_tile_index_ == (int)i)) {
791 selected_tile_index_ = static_cast<int>(i);
792 }
793 }
794 }
795 ImGui::EndChild();
796
797 // Edit selected tile
798 if (selected_tile_index_ >= 0 &&
799 selected_tile_index_ < (int)frame.Tiles.size()) {
800 auto& tile = frame.Tiles[selected_tile_index_];
801
802 int tile_id = tile.id;
803 if (ImGui::InputInt("Tile ID", &tile_id)) {
804 tile.id = static_cast<uint16_t>(std::clamp(tile_id, 0, 511));
805 zsm_dirty_ = true;
807 }
808
809 int x = tile.x, y = tile.y;
810 if (ImGui::InputInt("X", &x)) {
811 tile.x = static_cast<uint8_t>(std::clamp(x, 0, 251));
812 zsm_dirty_ = true;
814 }
815 if (ImGui::InputInt("Y", &y)) {
816 tile.y = static_cast<uint8_t>(std::clamp(y, 0, 219));
817 zsm_dirty_ = true;
819 }
820
821 int pal = tile.palette;
822 if (ImGui::SliderInt("Palette##Tile", &pal, 0, 7)) {
823 tile.palette = static_cast<uint8_t>(pal);
824 zsm_dirty_ = true;
826 }
827
828 if (ImGui::Checkbox("16x16", &tile.size)) {
829 zsm_dirty_ = true;
831 }
832 ImGui::SameLine();
833 if (ImGui::Checkbox("Flip X", &tile.mirror_x)) {
834 zsm_dirty_ = true;
836 }
837 ImGui::SameLine();
838 if (ImGui::Checkbox("Flip Y", &tile.mirror_y)) {
839 zsm_dirty_ = true;
841 }
842
843 if (ImGui::Button("Delete Tile")) {
844 frame.Tiles.erase(frame.Tiles.begin() + selected_tile_index_);
846 zsm_dirty_ = true;
848 }
849 }
850 }
851}
852
856 return;
857 }
858
860 if (current_animation_index_ < 0 ||
861 current_animation_index_ >= (int)sprite.animations.size()) {
862 return;
863 }
864
865 auto& anim = sprite.animations[current_animation_index_];
866
867 frame_timer_ += delta_time;
868 float frame_duration = anim.frame_speed / 60.0f;
869
870 if (frame_timer_ >= frame_duration) {
871 frame_timer_ = 0;
873 if (current_frame_ > anim.frame_end) {
874 current_frame_ = anim.frame_start;
875 }
877 }
878}
879
880// ============================================================
881// User Routines Panel
882// ============================================================
883
886
887 if (ImGui::Button(ICON_MD_ADD " Add Routine")) {
888 sprite.userRoutines.emplace_back("New Routine", "; ASM code here\n");
889 zsm_dirty_ = true;
890 }
891
892 // Routine list
893 if (ImGui::BeginChild("RoutineList", ImVec2(0, 100), true)) {
894 for (size_t i = 0; i < sprite.userRoutines.size(); i++) {
895 auto& routine = sprite.userRoutines[i];
896 if (Selectable(routine.name.c_str(), selected_routine_index_ == (int)i)) {
897 selected_routine_index_ = static_cast<int>(i);
898 }
899 }
900 }
901 ImGui::EndChild();
902
903 // Edit selected routine
904 if (selected_routine_index_ >= 0 &&
905 selected_routine_index_ < (int)sprite.userRoutines.size()) {
906 auto& routine = sprite.userRoutines[selected_routine_index_];
907
908 Separator();
909
910 static char routine_name[128];
911 strncpy(routine_name, routine.name.c_str(), sizeof(routine_name) - 1);
912 if (ImGui::InputText("Routine Name", routine_name, sizeof(routine_name))) {
913 routine.name = routine_name;
914 zsm_dirty_ = true;
915 }
916
917 Text("ASM Code:");
918
919 // Multiline text input for code
920 static char code_buffer[16384];
921 strncpy(code_buffer, routine.code.c_str(), sizeof(code_buffer) - 1);
922 code_buffer[sizeof(code_buffer) - 1] = '\0';
923 if (ImGui::InputTextMultiline("##RoutineCode", code_buffer,
924 sizeof(code_buffer), ImVec2(-1, 200))) {
925 routine.code = code_buffer;
926 zsm_dirty_ = true;
927 }
928
929 if (ImGui::Button("Delete Routine")) {
930 sprite.userRoutines.erase(sprite.userRoutines.begin() +
933 zsm_dirty_ = true;
934 }
935 }
936}
937
938// ============================================================
939// Graphics Pipeline
940// ============================================================
941
943 // Combine selected sheets (current_sheets_[0-7]) into single 8BPP buffer
944 // Layout: 16 tiles per row, 8 rows per sheet, 8 sheets total = 64 tile rows
945 // Buffer size: 0x10000 bytes (65536)
946
947 sprite_gfx_buffer_.resize(0x10000, 0);
948
949 // Each sheet is 128x32 pixels (128 bytes per row, 32 rows) = 4096 bytes
950 // We combine 8 sheets vertically: 128x256 pixels total
951 constexpr int kSheetWidth = 128;
952 constexpr int kSheetHeight = 32;
953 constexpr int kRowStride = 128;
954
955 for (int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
956 uint8_t sheet_id = current_sheets_[sheet_idx];
957 if (sheet_id >= gfx::Arena::Get().gfx_sheets().size()) {
958 continue;
959 }
960
961 auto& sheet = gfx::Arena::Get().gfx_sheets().at(sheet_id);
962 if (!sheet.is_active() || sheet.size() == 0) {
963 continue;
964 }
965
966 // Copy sheet data to buffer at appropriate offset
967 // Each sheet occupies 8 tile rows (8 * 8 scanlines = 64 scanlines)
968 // Offset = sheet_idx * (8 tile rows * 1024 bytes per tile row)
969 // But sheets are 32 pixels tall (4 tile rows), so:
970 // Offset = sheet_idx * 4 * 1024 = sheet_idx * 4096
971 int dest_offset = sheet_idx * (kSheetHeight * kRowStride);
972
973 const uint8_t* src_data = sheet.data();
974 size_t copy_size =
975 std::min(sheet.size(), static_cast<size_t>(kSheetWidth * kSheetHeight));
976
977 if (dest_offset + copy_size <= sprite_gfx_buffer_.size()) {
978 std::memcpy(sprite_gfx_buffer_.data() + dest_offset, src_data, copy_size);
979 }
980 }
981
982 // Update drawer with new buffer
984 gfx_buffer_loaded_ = true;
985}
986
988 // Load sprite palettes from ROM palette groups
989 // ALTTP sprites use a combination of palette groups:
990 // - Rows 0-1: Global sprite palettes (shared by all sprites)
991 // - Rows 2-7: Aux palettes (vary by sprite type)
992 //
993 // For simplicity, we load global_sprites which contains the main
994 // sprite palettes. More accurate rendering would require looking up
995 // which aux palette group each sprite type uses.
996
997 if (!rom_ || !rom_->is_loaded()) {
998 return;
999 }
1000
1001 // Build combined sprite palette from global + aux groups
1003
1004 // Add global sprite palettes (typically 2 palettes, 16 colors each)
1005 if (!game_data()) return;
1006 const auto& global = game_data()->palette_groups.global_sprites;
1007 for (size_t i = 0; i < global.size() && i < 8; i++) {
1008 sprite_palettes_.AddPalette(global.palette(i));
1009 }
1010
1011 // If we don't have 8 palettes yet, fill with aux palettes
1012 const auto& aux1 = game_data()->palette_groups.sprites_aux1;
1013 const auto& aux2 = game_data()->palette_groups.sprites_aux2;
1014 const auto& aux3 = game_data()->palette_groups.sprites_aux3;
1015
1016 // Pad to 8 palettes total for proper OAM palette mapping
1017 while (sprite_palettes_.size() < 8) {
1018 if (sprite_palettes_.size() < 4 && aux1.size() > 0) {
1019 sprite_palettes_.AddPalette(aux1.palette(sprite_palettes_.size() % aux1.size()));
1020 } else if (sprite_palettes_.size() < 6 && aux2.size() > 0) {
1021 sprite_palettes_.AddPalette(aux2.palette((sprite_palettes_.size() - 4) % aux2.size()));
1022 } else if (aux3.size() > 0) {
1023 sprite_palettes_.AddPalette(aux3.palette((sprite_palettes_.size() - 6) % aux3.size()));
1024 } else {
1025 // Fallback: add empty palette
1027 }
1028 }
1029
1031}
1032
1033void SpriteEditor::LoadSheetsForSprite(const std::array<uint8_t, 4>& sheets) {
1034 // Load the required sheets for a vanilla sprite
1035 bool changed = false;
1036 for (int i = 0; i < 4; i++) {
1037 if (sheets[i] != 0 && current_sheets_[i] != sheets[i]) {
1038 current_sheets_[i] = sheets[i];
1039 changed = true;
1040 }
1041 }
1042
1043 if (changed) {
1044 gfx_buffer_loaded_ = false;
1046 }
1047}
1048
1050 // Ensure graphics buffer is loaded
1054 }
1055
1056 // Initialize vanilla preview bitmap if needed
1060 }
1061
1063 return;
1064 }
1065
1066 // Clear and render
1068
1069 // Origin is center of bitmap
1070 int origin_x = 64;
1071 int origin_y = 64;
1072
1073 // Convert SpriteOamLayout tiles to zsprite::OamTile and draw
1074 for (const auto& entry : layout.tiles) {
1075 zsprite::OamTile tile;
1076 tile.x = static_cast<uint8_t>(entry.x_offset + 128); // Convert to unsigned
1077 tile.y = static_cast<uint8_t>(entry.y_offset + 128);
1078 tile.id = entry.tile_id;
1079 tile.palette = entry.palette;
1080 tile.size = entry.size_16x16;
1081 tile.mirror_x = entry.flip_x;
1082 tile.mirror_y = entry.flip_y;
1083 tile.priority = 0;
1084
1085 sprite_drawer_.DrawOamTile(vanilla_preview_bitmap_, tile, origin_x, origin_y);
1086 }
1087
1088 // Build combined 128-color palette (8 sub-palettes × 16 colors)
1089 // and apply to bitmap for proper color rendering
1090 if (sprite_palettes_.size() > 0) {
1091 gfx::SnesPalette combined_palette;
1092 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size(); pal_idx++) {
1093 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1094 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size(); col_idx++) {
1095 combined_palette.AddColor(sub_pal[col_idx]);
1096 }
1097 // Pad to 16 if sub-palette is smaller
1098 while (combined_palette.size() < (pal_idx + 1) * 16) {
1099 combined_palette.AddColor(gfx::SnesColor(0));
1100 }
1101 }
1102 vanilla_preview_bitmap_.SetPalette(combined_palette);
1103 }
1104
1106}
1107
1108// ============================================================
1109// Canvas Rendering
1110// ============================================================
1111
1115 return;
1116 }
1117
1119 if (frame_index < 0 || frame_index >= (int)sprite.editor.Frames.size()) {
1120 return;
1121 }
1122
1123 auto& frame = sprite.editor.Frames[frame_index];
1124
1125 // Ensure graphics buffer is loaded
1129 }
1130
1131 // Initialize preview bitmap if needed
1135 }
1136
1137 // Only render if drawer is ready
1139 // Clear and render to preview bitmap
1141
1142 // Origin is center of canvas (128, 128 for 256x256 bitmap)
1144
1145 // Build combined 128-color palette and apply to bitmap
1146 if (sprite_palettes_.size() > 0) {
1147 gfx::SnesPalette combined_palette;
1148 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size(); pal_idx++) {
1149 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1150 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size(); col_idx++) {
1151 combined_palette.AddColor(sub_pal[col_idx]);
1152 }
1153 // Pad to 16 if sub-palette is smaller
1154 while (combined_palette.size() < (pal_idx + 1) * 16) {
1155 combined_palette.AddColor(gfx::SnesColor(0));
1156 }
1157 }
1158 sprite_preview_bitmap_.SetPalette(combined_palette);
1159 }
1160
1161 // Mark as updated
1162 preview_needs_update_ = false;
1163 }
1164
1165 // Draw the preview bitmap on canvas
1168 }
1169
1170 // Draw tile outlines for selection (over the bitmap)
1171 if (show_tile_grid_) {
1172 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1173 const auto& tile = frame.Tiles[i];
1174 int tile_size = tile.size ? 16 : 8;
1175
1176 // Convert signed tile position to canvas position
1177 int8_t signed_x = static_cast<int8_t>(tile.x);
1178 int8_t signed_y = static_cast<int8_t>(tile.y);
1179
1180 int canvas_x = 128 + signed_x;
1181 int canvas_y = 128 + signed_y;
1182
1183 // Highlight selected tile
1184 ImVec4 color = (selected_tile_index_ == static_cast<int>(i))
1185 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f) // Green for selected
1186 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f); // Yellow for others
1187
1188 sprite_canvas_.DrawRect(canvas_x, canvas_y, tile_size, tile_size, color);
1189 }
1190 }
1191}
1192
1194 if (ImGui::BeginChild(gui::GetID("##ZSpriteCanvas"),
1195 ImGui::GetContentRegionAvail(), true)) {
1198
1199 // Render current frame if we have a sprite selected
1203 }
1204
1207
1208 // Display current frame info
1211 ImGui::SetCursorPos(ImVec2(10, 10));
1212 Text("Frame: %d | Tiles: %d", current_frame_,
1213 current_frame_ < (int)sprite.editor.Frames.size()
1214 ? (int)sprite.editor.Frames[current_frame_].Tiles.size()
1215 : 0);
1216 }
1217 }
1218 ImGui::EndChild();
1219}
1220
1221} // namespace editor
1222} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:146
bool is_loaded() const
Definition rom.h:128
zelda3::GameData * game_data() const
Definition editor.h:228
EditorDependencies dependencies_
Definition editor.h:237
void RegisterEditorPanel(std::unique_ptr< EditorPanel > panel)
Register an EditorPanel instance for central drawing.
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)
std::vector< zsprite::ZSprite > custom_sprites_
std::vector< uint8_t > sprite_gfx_buffer_
absl::Status Save() override
void SaveZsmFile(const std::string &path)
absl::Status Load() override
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:101
static Arena & Get()
Definition arena.cc:19
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:199
void Reformat(int format)
Reformat the bitmap to use a different pixel format.
Definition bitmap.cc:275
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:382
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:1075
void DrawContextMenu()
Definition canvas.cc:602
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1011
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1341
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:547
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1398
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
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:344
ImGuiID GetID(const std::string &id)
Definition input.cc:573
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:370
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
const std::string kSpriteDefaultNames[256]
Definition sprite.cc:13
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:1294
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89
Complete OAM layout for a vanilla sprite.
std::vector< SpriteOamEntry > tiles