Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
AnimationEditorWindow.cpp
Go to the documentation of this file.
1/**
2 * @file AnimationEditorWindow.cpp
3 * @brief Implementation of Animation Editor window
4 * @author Olympe Engine - Animation System
5 * @date 2025
6 */
7
9#include "../DataManager.h"
10#include "../system/system_utils.h"
11#include "../json_helper.h"
12#include "../GameEngine.h"
13#include "../third_party/imgui/imgui.h"
14#include "../third_party/imgui/backends/imgui_impl_sdl3.h"
15#include "../third_party/imgui/backends/imgui_impl_sdlrenderer3.h"
16#include <SDL3/SDL.h>
17#include <algorithm>
18#include <ctime>
19
20#ifdef _WIN32
21 #include <windows.h>
22#else
23 #include <dirent.h>
24 #include <sys/stat.h>
25#endif
26
28
29namespace Olympe
30{
31
32// ========================================================================
33// Constructor / Destructor
34// ========================================================================
35
37{
38 SYSTEM_LOG << "AnimationEditorWindow: Initialized\n";
39}
40
42{
44 SYSTEM_LOG << "AnimationEditorWindow: Destroyed\n";
45}
46
47// ========================================================================
48// Public API
49// ========================================================================
50
52{
54
55 if (m_isOpen)
56 {
57 // Create separate window if it doesn't exist
59 {
61 }
62
63 // Show the window
65 {
67 }
68
69 SYSTEM_LOG << "AnimationEditorWindow: Opened\n";
70
71 // Load initial bank if no bank is loaded
72 if (!m_hasBankLoaded)
73 {
74 // Try to load banks from directory
75 auto bankFiles = ScanBankDirectory("GameData/Animations/Banks");
76 if (!bankFiles.empty())
77 {
78 SYSTEM_LOG << "AnimationEditorWindow: Found " << bankFiles.size() << " banks\n";
79 }
80 }
81 }
82 else
83 {
84 // Hide the window
86 {
88 }
89
90 SYSTEM_LOG << "AnimationEditorWindow: Closed\n";
91
92 // Prompt for unsaved changes
93 if (m_isDirty)
94 {
96 }
97 }
98}
99
101{
103 return;
104
106 return;
107
108 // Get selected sequence
109 auto it = m_currentBank.animations.begin();
110 std::advance(it, m_selectedSequenceIndex);
111 const AnimationSequence& seq = it->second;
112
113 // Update frame timer
114 m_previewFrameTimer += deltaTime * m_previewSpeed;
115
116 if (m_previewFrameTimer >= seq.frameDuration)
117 {
118 m_previewFrameTimer = 0.0f;
120
121 int maxFrame = seq.startFrame + seq.frameCount - 1;
122
124 {
125 if (seq.loop)
126 {
127 m_previewCurrentFrame = seq.startFrame;
128 }
129 else
130 {
132 m_isPreviewPlaying = false;
133 }
134 }
135 }
136}
137
139{
140 if (!m_isOpen)
141 return;
142
143 // ===== CORRECTION: Follow BT Debugger pattern =====
144 // Force ImGui window to fill entire SDL window
145 ImGuiIO& io = ImGui::GetIO();
146
147 // Position (0, 0) and size = SDL window size
148 ImGui::SetNextWindowPos(ImVec2(0, 0));
149 ImGui::SetNextWindowSize(io.DisplaySize);
150
151 // Flags to remove borders and make window "invisible" (no title bar)
153 ImGuiWindowFlags_NoTitleBar | // No title bar
154 ImGuiWindowFlags_NoResize | // Not resizable (follows SDL window)
155 ImGuiWindowFlags_NoMove | // Not movable
156 ImGuiWindowFlags_NoCollapse | // No collapse button
157 ImGuiWindowFlags_NoBringToFrontOnFocus | // No automatic focus
158 ImGuiWindowFlags_MenuBar; // Menu bar at top
159
160 // Add unsaved document flag if dirty
161 if (m_isDirty)
163
164 // Begin main window
165 if (!ImGui::Begin("Animation Editor", nullptr, windowFlags))
166 {
167 ImGui::End();
168 return;
169 }
170
171 // Render menu bar
173
174 // 3-Column Layout
175 ImGui::BeginChild("LeftPanel", ImVec2(200, 0), true);
177 ImGui::EndChild();
178
179 ImGui::SameLine();
180
181 ImGui::BeginChild("MiddlePanel", ImVec2(600, 0), true);
182 // Tabs for Spritesheets and Sequences
183 if (ImGui::BeginTabBar("EditorTabs"))
184 {
185 if (ImGui::BeginTabItem("Spritesheets"))
186 {
187 m_activeTab = 0;
189 ImGui::EndTabItem();
190 }
191
192 if (ImGui::BeginTabItem("Sequences"))
193 {
194 m_activeTab = 1;
196 ImGui::EndTabItem();
197 }
198
199 ImGui::EndTabBar();
200 }
201 ImGui::EndChild();
202
203 ImGui::SameLine();
204
205 ImGui::BeginChild("RightPanel", ImVec2(0, 0), true);
207 ImGui::Separator();
209 ImGui::EndChild();
210
211 ImGui::End();
212}
213
214// ========================================================================
215// UI Panel Rendering
216// ========================================================================
217
219{
220 if (ImGui::BeginMenuBar())
221 {
222 if (ImGui::BeginMenu("File"))
223 {
224 if (ImGui::MenuItem("New Bank", "Ctrl+N"))
225 {
226 NewBank();
227 }
228
229 if (ImGui::MenuItem("Open Bank", "Ctrl+O"))
230 {
232 }
233
234 ImGui::Separator();
235
236 if (ImGui::MenuItem("Save", "Ctrl+S", false, m_hasBankLoaded))
237 {
238 SaveBank();
239 }
240
241 if (ImGui::MenuItem("Save As", "Ctrl+Shift+S", false, m_hasBankLoaded))
242 {
243 SaveBankAs();
244 }
245
246 ImGui::Separator();
247
248 if (ImGui::MenuItem("Close", "Ctrl+W"))
249 {
250 m_isOpen = false;
251 }
252
253 ImGui::EndMenu();
254 }
255
256 if (ImGui::BeginMenu("Edit"))
257 {
258 if (ImGui::MenuItem("Add Spritesheet", "Ctrl+Shift+A", false, m_hasBankLoaded))
259 {
261 }
262
263 if (ImGui::MenuItem("Add Sequence", "Ctrl+A", false, m_hasBankLoaded))
264 {
265 AddSequence();
266 }
267
268 ImGui::Separator();
269
270 if (ImGui::MenuItem("Remove Selected", "Del", false, m_selectedSpritesheetIndex >= 0 || m_selectedSequenceIndex >= 0))
271 {
273 {
275 }
276 else if (m_activeTab == 1 && m_selectedSequenceIndex >= 0)
277 {
279 }
280 }
281
282 ImGui::EndMenu();
283 }
284
285 if (ImGui::BeginMenu("View"))
286 {
287 ImGui::Checkbox("Show Grid", &m_showGrid);
288
289 ImGui::EndMenu();
290 }
291
292 ImGui::EndMenuBar();
293 }
294}
295
297{
298 ImGui::Text("Animation Banks");
299 ImGui::Separator();
300
301 if (ImGui::Button("+ New Bank", ImVec2(-1, 0)))
302 {
303 NewBank();
304 }
305
306 // List available banks
307 auto bankFiles = ScanBankDirectory("GameData/Animations/Banks");
308
309 ImGui::BeginChild("BankList", ImVec2(0, 0), false);
310
311 for (const auto& filepath : bankFiles)
312 {
313 // Extract filename
314 size_t lastSlash = filepath.find_last_of("/\\");
315 std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath;
316
317 bool isSelected = (filepath == m_currentBankPath);
318
319 if (ImGui::Selectable(filename.c_str(), isSelected))
320 {
321 if (m_isDirty)
322 {
324 {
325 OpenBank(filepath);
326 }
327 }
328 else
329 {
330 OpenBank(filepath);
331 }
332 }
333 }
334
335 ImGui::EndChild();
336}
337
339{
340 if (!m_hasBankLoaded)
341 {
342 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No bank loaded. Create or open a bank.");
343 return;
344 }
345
346 ImGui::Text("Spritesheets (%d)", static_cast<int>(m_currentBank.spritesheets.size()));
347
348 if (ImGui::Button("+ Add Spritesheet"))
349 {
351 }
352
353 ImGui::Separator();
354
355 // List spritesheets
356 ImGui::BeginChild("SpritesheetList", ImVec2(250, 0), true);
357
358 for (int i = 0; i < static_cast<int>(m_currentBank.spritesheets.size()); ++i)
359 {
360 const auto& sheet = m_currentBank.spritesheets[i];
361 bool isSelected = (i == m_selectedSpritesheetIndex);
362
363 if (ImGui::Selectable(sheet.id.c_str(), isSelected))
364 {
366 }
367 }
368
369 ImGui::EndChild();
370
371 ImGui::SameLine();
372
373 // Properties editor
374 ImGui::BeginChild("SpritesheetProperties", ImVec2(0, 0), true);
375
377 {
379
380 ImGui::Text("Spritesheet Properties");
381 ImGui::Separator();
382
383 // ID
384 char idBuf[256];
385 strncpy_s(idBuf, sheet.id.c_str(), sizeof(idBuf) - 1);
386 idBuf[sizeof(idBuf) - 1] = '\0';
387 if (ImGui::InputText("ID", idBuf, sizeof(idBuf)))
388 {
389 sheet.id = idBuf;
390 MarkDirty();
391 }
392
393 // Path
394 char pathBuf[512];
395 strncpy_s(pathBuf, sheet.path.c_str(), sizeof(pathBuf) - 1);
396 pathBuf[sizeof(pathBuf) - 1] = '\0';
397 if (ImGui::InputText("Path", pathBuf, sizeof(pathBuf)))
398 {
399 sheet.path = pathBuf;
400 MarkDirty();
401 }
402
403 // Description
404 char descBuf[512];
405 strncpy_s(descBuf, sheet.description.c_str(), sizeof(descBuf) - 1);
406 descBuf[sizeof(descBuf) - 1] = '\0';
407 if (ImGui::InputText("Description", descBuf, sizeof(descBuf)))
408 {
409 sheet.description = descBuf;
410 MarkDirty();
411 }
412
413 ImGui::Separator();
414 ImGui::Text("Grid Layout");
415
416 if (ImGui::InputInt("Frame Width", &sheet.frameWidth))
417 {
418 MarkDirty();
419 }
420
421 if (ImGui::InputInt("Frame Height", &sheet.frameHeight))
422 {
423 MarkDirty();
424 }
425
426 if (ImGui::InputInt("Columns", &sheet.columns))
427 {
428 MarkDirty();
429 }
430
431 if (ImGui::InputInt("Rows", &sheet.rows))
432 {
433 MarkDirty();
434 }
435
436 if (ImGui::InputInt("Total Frames", &sheet.totalFrames))
437 {
438 MarkDirty();
439 }
440
441 if (ImGui::InputInt("Spacing", &sheet.spacing))
442 {
443 MarkDirty();
444 }
445
446 if (ImGui::InputInt("Margin", &sheet.margin))
447 {
448 MarkDirty();
449 }
450
451 if (ImGui::Button("Auto-Detect Grid"))
452 {
454 }
455
456 ImGui::Separator();
457 ImGui::Text("Hotspot");
458
459 if (ImGui::InputFloat("Hotspot X", &sheet.hotspot.x))
460 {
461 MarkDirty();
462 }
463
464 if (ImGui::InputFloat("Hotspot Y", &sheet.hotspot.y))
465 {
466 MarkDirty();
467 }
468
469 // Preview spritesheet image
470 ImGui::Separator();
471 ImGui::Text("Preview");
472
474 if (tex)
475 {
476 // Get texture dimensions
477 float texW = 0.f;
478 float texH = 0.f;
480
481 // Calculate preview size
482 float previewW = static_cast<float>(texW) * m_spritesheetZoom;
483 float previewH = static_cast<float>(texH) * m_spritesheetZoom;
484
485 // Render texture
486 ImGui::Image((ImTextureID)(intptr_t)tex, ImVec2(previewW, previewH));
487
488 // Grid overlay
489 if (m_showGrid && sheet.frameWidth > 0 && sheet.frameHeight > 0 && sheet.columns > 0)
490 {
491 ImDrawList* drawList = ImGui::GetWindowDrawList();
492 ImVec2 p = ImGui::GetItemRectMin();
493
494 ImU32 gridColor = IM_COL32(255, 255, 0, 128);
495
496 // Vertical lines
497 for (int col = 0; col <= sheet.columns; ++col)
498 {
499 float x = p.x + (sheet.margin + col * (sheet.frameWidth + sheet.spacing)) * m_spritesheetZoom;
500 drawList->AddLine(ImVec2(x, p.y), ImVec2(x, p.y + previewH), gridColor);
501 }
502
503 // Horizontal lines
504 for (int row = 0; row <= sheet.rows; ++row)
505 {
506 float y = p.y + (sheet.margin + row * (sheet.frameHeight + sheet.spacing)) * m_spritesheetZoom;
507 drawList->AddLine(ImVec2(p.x, y), ImVec2(p.x + previewW, y), gridColor);
508 }
509 }
510 }
511 else
512 {
513 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Texture not loaded");
514 }
515
516 // Zoom controls
517 ImGui::Separator();
518 ImGui::SliderFloat("Zoom", &m_spritesheetZoom, 0.1f, 4.0f);
519 }
520 else
521 {
522 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Select a spritesheet to edit");
523 }
524
525 ImGui::EndChild();
526}
527
529{
530 if (!m_hasBankLoaded)
531 {
532 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No bank loaded. Create or open a bank.");
533 return;
534 }
535
536 ImGui::Text("Sequences (%d)", static_cast<int>(m_currentBank.animations.size()));
537
538 if (ImGui::Button("+ Add Sequence"))
539 {
540 AddSequence();
541 }
542
543 ImGui::Separator();
544
545 // List sequences
546 ImGui::BeginChild("SequenceList", ImVec2(250, 0), true);
547
548 int index = 0;
549 for (auto it = m_currentBank.animations.begin(); it != m_currentBank.animations.end(); ++it, ++index)
550 {
551 const auto& seq = it->second;
552 bool isSelected = (index == m_selectedSequenceIndex);
553
554 if (ImGui::Selectable(seq.name.c_str(), isSelected))
555 {
557 ResetPreview();
558 }
559 }
560
561 ImGui::EndChild();
562
563 ImGui::SameLine();
564
565 // Properties editor
566 ImGui::BeginChild("SequenceProperties", ImVec2(0, 0), true);
567
568 if (m_selectedSequenceIndex >= 0 && m_selectedSequenceIndex < static_cast<int>(m_currentBank.animations.size()))
569 {
570 auto it = m_currentBank.animations.begin();
571 std::advance(it, m_selectedSequenceIndex);
572 auto& seq = it->second;
573
574 ImGui::Text("Sequence Properties");
575 ImGui::Separator();
576
577 // Name
578 char nameBuf[256];
579 strncpy_s(nameBuf, sizeof(nameBuf), seq.name.c_str(), _TRUNCATE);
580 nameBuf[sizeof(nameBuf) - 1] = '\0';
581 if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
582 {
583 std::string oldName = seq.name;
584 seq.name = nameBuf;
585
586 // Update key in map
587 if (oldName != seq.name)
588 {
590 if (findIt != m_currentBank.animations.end())
591 {
592 AnimationSequence moved = std::move(findIt->second);
594 m_currentBank.animations[seq.name] = std::move(moved);
595 }
596 }
597
598 MarkDirty();
599 }
600
601 // Spritesheet selector
602 ImGui::Text("Spritesheet");
603
604 if (ImGui::BeginCombo("##SpritesheetSelector", seq.spritesheetId.c_str()))
605 {
606 for (const auto& sheet : m_currentBank.spritesheets)
607 {
608 bool isSelected = (seq.spritesheetId == sheet.id);
609 if (ImGui::Selectable(sheet.id.c_str(), isSelected))
610 {
611 seq.spritesheetId = sheet.id;
612 MarkDirty();
613 }
614
615 if (isSelected)
616 {
617 ImGui::SetItemDefaultFocus();
618 }
619 }
620 ImGui::EndCombo();
621 }
622
623 ImGui::Separator();
624 ImGui::Text("Frame Range");
625
626 if (ImGui::InputInt("Start Frame", &seq.startFrame))
627 {
628 if (seq.startFrame < 0) seq.startFrame = 0;
629 MarkDirty();
630 }
631
632 if (ImGui::InputInt("Frame Count", &seq.frameCount))
633 {
634 if (seq.frameCount < 1) seq.frameCount = 1;
635 MarkDirty();
636 }
637
638 ImGui::Separator();
639 ImGui::Text("Playback Settings");
640
641 if (ImGui::InputFloat("Frame Duration (s)", &seq.frameDuration))
642 {
643 if (seq.frameDuration < 0.001f) seq.frameDuration = 0.001f;
644 MarkDirty();
645 }
646
647 if (ImGui::Checkbox("Loop", &seq.loop))
648 {
649 MarkDirty();
650 }
651
652 if (ImGui::SliderFloat("Speed", &seq.speed, 0.1f, 5.0f))
653 {
654 MarkDirty();
655 }
656
657 // Next animation
658 char nextAnimBuf[256];
659 strncpy_s(nextAnimBuf, sizeof(nextAnimBuf), seq.nextAnimation.c_str(), _TRUNCATE);
660 nextAnimBuf[sizeof(nextAnimBuf) - 1] = '\0';
661 if (ImGui::InputText("Next Animation", nextAnimBuf, sizeof(nextAnimBuf)))
662 {
663 seq.nextAnimation = nextAnimBuf;
664 MarkDirty();
665 }
666
667 ImGui::Separator();
668 ImGui::Text("Stats");
669 ImGui::Text("Total Duration: %.2f s", seq.GetTotalDuration());
670 ImGui::Text("Effective FPS: %.2f", seq.GetEffectiveFPS());
671 }
672 else
673 {
674 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Select a sequence to edit");
675 }
676
677 ImGui::EndChild();
678}
679
681{
682 ImGui::Text("Preview");
683 ImGui::Separator();
684
685 if (!m_hasBankLoaded)
686 {
687 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No bank loaded");
688 return;
689 }
690
692 {
693 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No sequence selected");
694 return;
695 }
696
697 // Get selected sequence
698 auto it = m_currentBank.animations.begin();
699 std::advance(it, m_selectedSequenceIndex);
700 const AnimationSequence& seq = it->second;
701
702 // Playback controls
703 if (ImGui::Button(m_isPreviewPlaying && !m_isPreviewPaused ? "Pause" : "Play"))
704 {
706 {
707 PausePreview();
708 }
709 else
710 {
711 StartPreview();
712 }
713 }
714
715 ImGui::SameLine();
716
717 if (ImGui::Button("Stop"))
718 {
719 StopPreview();
720 }
721
722 ImGui::SameLine();
723
724 ImGui::SliderFloat("Speed", &m_previewSpeed, 0.1f, 5.0f);
725
726 // Frame scrubber
727 int maxFrame = seq.startFrame + seq.frameCount - 1;
728 if (ImGui::SliderInt("Frame", &m_previewCurrentFrame, seq.startFrame, maxFrame))
729 {
730 // Manual frame seek
731 m_previewFrameTimer = 0.0f;
732 }
733
734 ImGui::Text("Frame: %d / %d", m_previewCurrentFrame - seq.startFrame + 1, seq.frameCount);
735
736 ImGui::Separator();
737
738 // Render current frame
740}
741
743{
745 return;
746
747 // Get selected sequence
748 auto it = m_currentBank.animations.begin();
749 std::advance(it, m_selectedSequenceIndex);
750 const AnimationSequence& seq = it->second;
751
752 // Get spritesheet
753 const SpritesheetInfo* sheet = m_currentBank.GetSpritesheet(seq.spritesheetId);
754 if (!sheet)
755 {
756 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Spritesheet not found: %s", seq.spritesheetId.c_str());
757 return;
758 }
759
760 // Load texture
762 if (!tex)
763 {
764 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Failed to load texture: %s", sheet->path.c_str());
765 return;
766 }
767
768 // Calculate srcRect for current frame
770 if (frameIndex < 0) frameIndex = 0;
771 if (frameIndex >= sheet->totalFrames) frameIndex = sheet->totalFrames - 1;
772
773 int row = frameIndex / sheet->columns;
774 int col = frameIndex % sheet->columns;
775
776 float srcX = static_cast<float>(sheet->margin + col * (sheet->frameWidth + sheet->spacing));
777 float srcY = static_cast<float>(sheet->margin + row * (sheet->frameHeight + sheet->spacing));
778 float srcW = static_cast<float>(sheet->frameWidth);
779 float srcH = static_cast<float>(sheet->frameHeight);
780
781 // Get texture dimensions for UV calculation
782 float texW = 0.f;
783 float texH = 0.f;
785
786 // Calculate UV coordinates
787 ImVec2 uv0(srcX / texW, srcY / texH);
788 ImVec2 uv1((srcX + srcW) / texW, (srcY + srcH) / texH);
789
790 // Render with 2x scale
791 ImVec2 previewSize(srcW * 2.0f, srcH * 2.0f);
792
793 // Center the preview
794 ImVec2 availSize = ImGui::GetContentRegionAvail();
795 ImVec2 cursorPos = ImGui::GetCursorPos();
797
799
800 ImGui::SetCursorPos(centeredPos);
801 ImGui::Image((ImTextureID)(intptr_t)tex, previewSize, uv0, uv1);
802}
803
805{
806 ImGui::Text("Bank Properties");
807 ImGui::Separator();
808
809 if (!m_hasBankLoaded)
810 {
811 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No bank loaded");
812 return;
813 }
814
815 // Bank ID
816 char bankIdBuf[256];
817 strncpy_s(bankIdBuf, m_currentBank.bankId.c_str(), sizeof(bankIdBuf) - 1);
818 bankIdBuf[sizeof(bankIdBuf) - 1] = '\0';
819 if (ImGui::InputText("Bank ID", bankIdBuf, sizeof(bankIdBuf)))
820 {
822 MarkDirty();
823 }
824
825 // Description
826 char descBuf[1024];
827 strncpy_s(descBuf, m_currentBank.description.c_str(), sizeof(descBuf) - 1);
828 descBuf[sizeof(descBuf) - 1] = '\0';
829 if (ImGui::InputTextMultiline("Description", descBuf, sizeof(descBuf), ImVec2(-1, 80)))
830 {
832 MarkDirty();
833 }
834
835 // Author
836 char authorBuf[256];
837 strncpy_s(authorBuf, m_currentBank.author.c_str(), sizeof(authorBuf) - 1);
838 authorBuf[sizeof(authorBuf) - 1] = '\0';
839 if (ImGui::InputText("Author", authorBuf, sizeof(authorBuf)))
840 {
842 MarkDirty();
843 }
844
845 // Dates (read-only)
846 ImGui::Text("Created: %s", m_currentBank.createdDate.c_str());
847 ImGui::Text("Modified: %s", m_currentBank.lastModifiedDate.c_str());
848}
849
850// ========================================================================
851// File Operations
852// ========================================================================
853
855{
856 if (m_isDirty)
857 {
859 return;
860 }
861
863 m_currentBank.bankId = "new_bank";
864 m_currentBank.author = "Olympe Engine";
865
866 // Set created date
867 time_t now = time(nullptr);
868 char dateBuf[64];
869 struct tm timeInfo;
871 strftime(dateBuf, sizeof(dateBuf), "%Y-%m-%dT%H:%M:%SZ", &timeInfo);
874
876 m_hasBankLoaded = true;
879
880 MarkDirty();
881
882 SYSTEM_LOG << "AnimationEditorWindow: Created new bank\n";
883}
884
885void AnimationEditorWindow::OpenBank(const std::string& filepath)
886{
887 ImportBankJSON(filepath);
888}
889
891{
892 if (m_currentBankPath.empty())
893 {
894 SaveBankAs();
895 return;
896 }
897
899 ClearDirty();
900}
901
903{
904 // For now, use a simple path
905 std::string filepath = "GameData/Animations/Banks/" + m_currentBank.bankId + ".json";
906
907 ExportBankJSON(filepath);
908 m_currentBankPath = filepath;
909 ClearDirty();
910}
911
912void AnimationEditorWindow::ImportBankJSON(const std::string& filepath)
913{
914 json j;
915 if (!JsonHelper::LoadJsonFromFile(filepath, j))
916 {
917 SYSTEM_LOG << "AnimationEditorWindow: Failed to load JSON from " << filepath << "\n";
918 return;
919 }
920
921 try
922 {
924
925 // Parse basic info
926 bank.bankId = JsonHelper::GetString(j, "bankId", "unknown");
927 bank.description = JsonHelper::GetString(j, "description", "");
928
929 // Parse metadata
930 if (j.contains("metadata"))
931 {
932 const json& meta = j["metadata"];
933 bank.author = JsonHelper::GetString(meta, "author", "");
934 bank.createdDate = JsonHelper::GetString(meta, "created", "");
935 bank.lastModifiedDate = JsonHelper::GetString(meta, "lastModified", "");
936
937 if (meta.contains("tags") && meta["tags"].is_array())
938 {
939 for (const auto& tag : meta["tags"])
940 {
941 if (tag.is_string())
942 {
943 bank.tags.push_back(tag.get<std::string>());
944 }
945 }
946 }
947 }
948
949 // Parse spritesheets
950 if (j.contains("spritesheets") && j["spritesheets"].is_array())
951 {
952 for (const auto& sheetJson : j["spritesheets"])
953 {
956 sheet.path = JsonHelper::GetString(sheetJson, "path", "");
957 sheet.description = JsonHelper::GetString(sheetJson, "description", "");
958 sheet.frameWidth = JsonHelper::GetInt(sheetJson, "frameWidth", 32);
959 sheet.frameHeight = JsonHelper::GetInt(sheetJson, "frameHeight", 32);
960 sheet.columns = JsonHelper::GetInt(sheetJson, "columns", 1);
961 sheet.rows = JsonHelper::GetInt(sheetJson, "rows", 1);
962 sheet.totalFrames = JsonHelper::GetInt(sheetJson, "totalFrames", 1);
963 sheet.spacing = JsonHelper::GetInt(sheetJson, "spacing", 0);
964 sheet.margin = JsonHelper::GetInt(sheetJson, "margin", 0);
965
966 if (sheetJson.contains("hotspot"))
967 {
968 sheet.hotspot.x = JsonHelper::GetFloat(sheetJson["hotspot"], "x", 0.0f);
969 sheet.hotspot.y = JsonHelper::GetFloat(sheetJson["hotspot"], "y", 0.0f);
970 }
971
972 bank.spritesheets.push_back(sheet);
973 }
974 }
975
976 // Parse sequences
977 if (j.contains("sequences") && j["sequences"].is_array())
978 {
979 for (const auto& seqJson : j["sequences"])
980 {
982 seq.name = JsonHelper::GetString(seqJson, "name", "");
983 seq.spritesheetId = JsonHelper::GetString(seqJson, "spritesheetId", "");
984
985 if (seqJson.contains("frames"))
986 {
987 seq.startFrame = JsonHelper::GetInt(seqJson["frames"], "start", 0);
988 seq.frameCount = JsonHelper::GetInt(seqJson["frames"], "count", 1);
989 }
990
991 seq.frameDuration = JsonHelper::GetFloat(seqJson, "frameDuration", 0.1f);
992 seq.loop = JsonHelper::GetBool(seqJson, "loop", true);
993 seq.speed = JsonHelper::GetFloat(seqJson, "speed", 1.0f);
994 seq.nextAnimation = JsonHelper::GetString(seqJson, "nextAnimation", "");
995
996 bank.animations[seq.name] = seq;
997 }
998 }
999
1001 m_currentBankPath = filepath;
1002 m_hasBankLoaded = true;
1005 ClearDirty();
1006
1007 SYSTEM_LOG << "AnimationEditorWindow: Loaded bank from " << filepath << "\n";
1008 }
1009 catch (const std::exception& e)
1010 {
1011 SYSTEM_LOG << "AnimationEditorWindow: Error parsing JSON: " << e.what() << "\n";
1012 }
1013}
1014
1015void AnimationEditorWindow::ExportBankJSON(const std::string& filepath)
1016{
1017 try
1018 {
1019 json j = json::object();
1020
1021 j["schema_version"] = 2;
1022 j["type"] = "AnimationBank";
1023 j["bankId"] = m_currentBank.bankId;
1024 j["description"] = m_currentBank.description;
1025
1026 // Metadata
1027 json meta = json::object();
1028 meta["author"] = m_currentBank.author;
1029 meta["created"] = m_currentBank.createdDate;
1030
1031 // Update last modified date
1032 time_t now = time(nullptr);
1033 char dateBuf[64];
1034 struct tm timeInfo;
1035 gmtime_s(&timeInfo, &now);
1036 strftime(dateBuf, sizeof(dateBuf), "%Y-%m-%dT%H:%M:%SZ", &timeInfo);
1037 meta["lastModified"] = dateBuf;
1039
1040 json tagsArray = json::array();
1041 for (const auto& tag : m_currentBank.tags)
1042 {
1043 tagsArray.push_back(tag);
1044 }
1045 meta["tags"] = tagsArray;
1046
1047 j["metadata"] = meta;
1048
1049 // Spritesheets
1050 json sheetsArray = json::array();
1051 for (const auto& sheet : m_currentBank.spritesheets)
1052 {
1053 json sheetJson = json::object();
1054 sheetJson["id"] = sheet.id;
1055 sheetJson["path"] = sheet.path;
1056 sheetJson["description"] = sheet.description;
1057 sheetJson["frameWidth"] = sheet.frameWidth;
1058 sheetJson["frameHeight"] = sheet.frameHeight;
1059 sheetJson["columns"] = sheet.columns;
1060 sheetJson["rows"] = sheet.rows;
1061 sheetJson["totalFrames"] = sheet.totalFrames;
1062 sheetJson["spacing"] = sheet.spacing;
1063 sheetJson["margin"] = sheet.margin;
1064
1065 json hotspot = json::object();
1066 hotspot["x"] = sheet.hotspot.x;
1067 hotspot["y"] = sheet.hotspot.y;
1068 sheetJson["hotspot"] = hotspot;
1069
1070 sheetsArray.push_back(sheetJson);
1071 }
1072 j["spritesheets"] = sheetsArray;
1073
1074 // Sequences
1075 json seqsArray = json::array();
1076 for (const auto& pair : m_currentBank.animations)
1077 {
1078 const auto& seq = pair.second;
1079 json seqJson = json::object();
1080 seqJson["name"] = seq.name;
1081 seqJson["spritesheetId"] = seq.spritesheetId;
1082
1083 json frames = json::object();
1084 frames["start"] = seq.startFrame;
1085 frames["count"] = seq.frameCount;
1086 seqJson["frames"] = frames;
1087
1088 seqJson["frameDuration"] = seq.frameDuration;
1089 seqJson["loop"] = seq.loop;
1090 seqJson["speed"] = seq.speed;
1091 seqJson["nextAnimation"] = seq.nextAnimation;
1092
1093 seqsArray.push_back(seqJson);
1094 }
1095 j["sequences"] = seqsArray;
1096
1097 // Write to file
1098 JsonHelper::SaveJsonToFile(filepath, j);
1099
1100 SYSTEM_LOG << "AnimationEditorWindow: Saved bank to " << filepath << "\n";
1101 }
1102 catch (const std::exception& e)
1103 {
1104 SYSTEM_LOG << "AnimationEditorWindow: Error exporting JSON: " << e.what() << "\n";
1105 }
1106}
1107
1108std::vector<std::string> AnimationEditorWindow::ScanBankDirectory(const std::string& dirPath)
1109{
1110 std::vector<std::string> files;
1111
1112#ifdef _WIN32
1114 std::string searchPath = dirPath + "/*.json";
1116
1118 {
1119 do
1120 {
1121 if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
1122 {
1123 files.push_back(dirPath + "/" + findData.cFileName);
1124 }
1125 } while (FindNextFileA(hFind, &findData));
1126
1128 }
1129#else
1130 DIR* dir = opendir(dirPath.c_str());
1131 if (dir)
1132 {
1133 struct dirent* entry;
1134 while ((entry = readdir(dir)) != nullptr)
1135 {
1136 std::string filename = entry->d_name;
1137 if (filename.size() > 5 && filename.substr(filename.size() - 5) == ".json")
1138 {
1139 files.push_back(dirPath + "/" + filename);
1140 }
1141 }
1142 closedir(dir);
1143 }
1144#endif
1145
1146 return files;
1147}
1148
1149// ========================================================================
1150// Spritesheet Operations
1151// ========================================================================
1152
1154{
1156 sheet.id = "new_spritesheet_" + std::to_string(m_currentBank.spritesheets.size());
1157 sheet.path = "";
1158 sheet.frameWidth = 32;
1159 sheet.frameHeight = 32;
1160 sheet.columns = 1;
1161 sheet.rows = 1;
1162 sheet.totalFrames = 1;
1163 sheet.spacing = 0;
1164 sheet.margin = 0;
1165 sheet.hotspot.x = 16.0f;
1166 sheet.hotspot.y = 16.0f;
1167
1168 m_currentBank.spritesheets.push_back(sheet);
1169 m_selectedSpritesheetIndex = static_cast<int>(m_currentBank.spritesheets.size()) - 1;
1170
1171 MarkDirty();
1172
1173 SYSTEM_LOG << "AnimationEditorWindow: Added spritesheet\n";
1174}
1175
1177{
1178 if (index < 0 || index >= static_cast<int>(m_currentBank.spritesheets.size()))
1179 return;
1180
1183
1184 MarkDirty();
1185
1186 SYSTEM_LOG << "AnimationEditorWindow: Removed spritesheet\n";
1187}
1188
1190{
1191 // Load texture to get dimensions
1193 if (!tex)
1194 {
1195 SYSTEM_LOG << "AnimationEditorWindow: Cannot auto-detect grid - texture not loaded\n";
1196 return;
1197 }
1198
1199 float texWf = 0.f;
1200 float texHf = 0.f;
1202 int texW = static_cast<int>(texWf);
1203 int texH = static_cast<int>(texHf);
1204
1205 // Try to calculate columns/rows based on frame size
1206 if (sheet.frameWidth > 0 && sheet.frameHeight > 0)
1207 {
1208 sheet.columns = (texW - 2 * sheet.margin + sheet.spacing) / (sheet.frameWidth + sheet.spacing);
1209 sheet.rows = (texH - 2 * sheet.margin + sheet.spacing) / (sheet.frameHeight + sheet.spacing);
1210 sheet.totalFrames = sheet.columns * sheet.rows;
1211
1212 MarkDirty();
1213
1214 SYSTEM_LOG << "AnimationEditorWindow: Auto-detected grid: " << sheet.columns << "x" << sheet.rows << " = " << sheet.totalFrames << " frames\n";
1215 }
1216}
1217
1219{
1220 if (path.empty())
1221 return nullptr;
1222
1223 // Use DataManager to load texture
1224 auto* sprite = DataManager::Get().GetSprite(path, path);
1225 return sprite ? sprite : nullptr;
1226}
1227
1228// ========================================================================
1229// Sequence Operations
1230// ========================================================================
1231
1233{
1235 seq.name = "new_sequence_" + std::to_string(m_currentBank.animations.size());
1236 seq.spritesheetId = m_currentBank.spritesheets.empty() ? "" : m_currentBank.spritesheets[0].id;
1237 seq.startFrame = 0;
1238 seq.frameCount = 1;
1239 seq.frameDuration = 0.1f;
1240 seq.loop = true;
1241 seq.speed = 1.0f;
1242 seq.nextAnimation = "";
1243
1245 m_selectedSequenceIndex = static_cast<int>(m_currentBank.animations.size()) - 1;
1246
1247 MarkDirty();
1248
1249 SYSTEM_LOG << "AnimationEditorWindow: Added sequence\n";
1250}
1251
1253{
1254 if (index < 0 || index >= static_cast<int>(m_currentBank.animations.size()))
1255 return;
1256
1257 auto it = m_currentBank.animations.begin();
1258 std::advance(it, index);
1261
1262 MarkDirty();
1263
1264 SYSTEM_LOG << "AnimationEditorWindow: Removed sequence\n";
1265}
1266
1267// ========================================================================
1268// Preview Operations
1269// ========================================================================
1270
1272{
1274 return;
1275
1276 auto it = m_currentBank.animations.begin();
1277 std::advance(it, m_selectedSequenceIndex);
1278 const AnimationSequence& seq = it->second;
1279
1281 {
1283 }
1284
1285 m_isPreviewPlaying = true;
1286 m_isPreviewPaused = false;
1287 m_previewFrameTimer = 0.0f;
1288}
1289
1291{
1292 m_isPreviewPlaying = false;
1293 m_isPreviewPaused = false;
1294 ResetPreview();
1295}
1296
1301
1303{
1305 {
1307 return;
1308 }
1309
1310 auto it = m_currentBank.animations.begin();
1311 std::advance(it, m_selectedSequenceIndex);
1312 const AnimationSequence& seq = it->second;
1313
1315 m_previewFrameTimer = 0.0f;
1316}
1317
1318// ========================================================================
1319// Helper Methods
1320// ========================================================================
1321
1323{
1324 m_isDirty = true;
1325}
1326
1328{
1329 m_isDirty = false;
1330}
1331
1333{
1334 // For now, just return true (user accepts closing)
1335 // In a full implementation, this would show a dialog
1336 SYSTEM_LOG << "AnimationEditorWindow: Unsaved changes detected\n";
1337 return true;
1338}
1339
1341{
1342 // Title is set in Render() with m_isDirty flag
1343}
1344
1345// ========================================================================
1346// Standalone Window Management
1347// ========================================================================
1348
1350{
1351 if (m_separateWindow)
1352 {
1353 SYSTEM_LOG << "[AnimationEditor] Separate window already exists\n";
1354 return; // Already created
1355 }
1356
1357 // Save current context
1358 ImGuiContext* mainContext = ImGui::GetCurrentContext();
1359
1360 // Create SDL window (1280x720, resizable)
1362 "Animation Editor - Olympe Engine",
1363 1280,
1364 720,
1368 {
1369 SYSTEM_LOG << "[AnimationEditor] Failed to create window: " << SDL_GetError() << "\n";
1370 return;
1371 }
1372
1373 // Create separate ImGui context
1374 m_separateImGuiContext = ImGui::CreateContext();
1375 ImGui::SetCurrentContext(m_separateImGuiContext);
1376
1377 // Setup ImGui style
1378 ImGui::StyleColorsDark();
1379
1380 // Initialize ImGui backends
1383
1384 // Restore main context
1385 ImGui::SetCurrentContext(mainContext);
1386
1387 SYSTEM_LOG << "[AnimationEditor] Standalone window created\n";
1388}
1389
1391{
1392 if (!m_separateWindow)
1393 return;
1394
1395 // Save current context
1396 ImGuiContext* mainContext = ImGui::GetCurrentContext();
1397
1399 {
1400 ImGui::SetCurrentContext(m_separateImGuiContext);
1403 ImGui::DestroyContext(m_separateImGuiContext);
1404 m_separateImGuiContext = nullptr;
1405 }
1406
1407 // Restore main context
1408 ImGui::SetCurrentContext(mainContext);
1409
1411 {
1413 m_separateRenderer = nullptr;
1414 }
1415
1416 if (m_separateWindow)
1417 {
1419 m_separateWindow = nullptr;
1420 }
1421
1422 SYSTEM_LOG << "[AnimationEditor] Separate window destroyed\n";
1423}
1424
1426{
1428 return;
1429
1430 // Switch to separate ImGui context
1431 ImGuiContext* mainContext = ImGui::GetCurrentContext();
1432 ImGui::SetCurrentContext(m_separateImGuiContext);
1433
1434 // Clear window
1437
1438 // ImGui frame
1441 ImGui::NewFrame();
1442
1443 // Render Animation Editor UI
1444 Render();
1445
1446 // Present
1447 ImGui::Render();
1450
1451 // Restore main context
1452 ImGui::SetCurrentContext(mainContext);
1453}
1454
1456{
1457 if (!m_separateWindow || !m_isOpen)
1458 return;
1459
1460 // Check if event is for our window
1462 {
1464 if (event->window.windowID == windowID)
1465 {
1466 Toggle(); // Close window
1467 return;
1468 }
1469 }
1470
1471 // Forward event to ImGui (separate context)
1472 ImGuiContext* mainContext = ImGui::GetCurrentContext();
1473 ImGui::SetCurrentContext(m_separateImGuiContext);
1475 ImGui::SetCurrentContext(mainContext);
1476}
1477
1479{
1480 if (!m_isOpen)
1481 return;
1482
1483 // Update preview animation
1484 UpdatePreview(deltaTime);
1485
1486 // Render separate window
1488}
1489
1490} // namespace Olympe
Animation Editor window for creating and editing animation banks.
nlohmann::json json
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static DataManager & Get()
Definition DataManager.h:87
Sprite * GetSprite(const std::string &id, const std::string &path, ResourceCategory category=ResourceCategory::GameEntity)
void Toggle()
Toggle window visibility.
void ExportBankJSON(const std::string &filepath)
void AutoDetectGrid(SpritesheetInfo &sheet)
std::vector< std::string > ScanBankDirectory(const std::string &dirPath)
void UpdatePreview(float deltaTime)
Update preview animation (call every frame with deltaTime)
void Update(float deltaTime)
Update and render the editor window (separate window)
void ImportBankJSON(const std::string &filepath)
void OpenBank(const std::string &filepath)
void Render()
Render the editor window.
void ProcessEvent(SDL_Event *event)
Process SDL events for the separate window.
SDL_Texture * LoadSpritesheetTexture(const std::string &path)
std::string GetString(const json &j, const std::string &key, const std::string &defaultValue="")
Safely get a string value from JSON.
bool LoadJsonFromFile(const std::string &filepath, json &j)
Load and parse a JSON file.
Definition json_helper.h:42
int GetInt(const json &j, const std::string &key, int defaultValue=0)
Safely get an integer value from JSON.
float GetFloat(const json &j, const std::string &key, float defaultValue=0.0f)
Safely get a float value from JSON.
bool SaveJsonToFile(const std::string &filepath, const json &j, int indent=4)
Save a JSON object to a file with formatting.
Definition json_helper.h:73
bool GetBool(const json &j, const std::string &key, bool defaultValue=false)
Safely get a boolean value from JSON.
nlohmann::json json
nlohmann::json json
Collection of animations for an entity with multi-spritesheet support.
std::string bankId
Unique identifier for this animation bank.
std::vector< SpritesheetInfo > spritesheets
const SpritesheetInfo * GetSpritesheet(const std::string &id) const
Get spritesheet by ID.
std::vector< std::string > tags
std::unordered_map< std::string, AnimationSequence > animations
Defines a complete animation sequence.
int startFrame
Starting frame index (0-based)
std::string name
Animation name (e.g., "idle", "walk")
Metadata for a single spritesheet within an animation bank.
std::string id
Unique identifier (e.g., "thesee_idle")
#define SYSTEM_LOG