From 62cd94ca78e3cfbef199473618106745ecac2f0b Mon Sep 17 00:00:00 2001 From: shweet Date: Sat, 1 Nov 2025 19:51:19 -0400 Subject: [PATCH] Big refactor, shuffling a lot of files around --- CMakeLists.txt | 38 +- src/animation_preview.h | 19 - src/animations.h | 18 - src/anm2.cpp | 1453 ----------------- src/anm2.h | 264 --- src/anm2/animation.cpp | 206 +++ src/anm2/animation.h | 35 + src/anm2/animations.cpp | 155 ++ src/anm2/animations.h | 22 + src/anm2/anm2.cpp | 376 +++++ src/anm2/anm2.h | 72 + src/anm2/content.cpp | 256 +++ src/anm2/content.h | 46 + src/anm2/event.cpp | 36 + src/anm2/event.h | 20 + src/anm2/frame.cpp | 126 ++ src/anm2/frame.h | 91 ++ src/anm2/info.cpp | 40 + src/anm2/info.h | 25 + src/anm2/item.cpp | 200 +++ src/anm2/item.h | 26 + src/anm2/layer.cpp | 39 + src/anm2/layer.h | 22 + src/anm2/null.cpp | 38 + src/anm2/null.h | 22 + src/anm2/sound.cpp | 59 + src/anm2/sound.h | 30 + src/anm2/spritesheet.cpp | 70 + src/anm2/spritesheet.h | 30 + src/canvas.cpp | 13 +- src/canvas.h | 16 +- src/clipboard.cpp | 2 +- src/clipboard.h | 2 +- src/dialog.cpp | 54 +- src/dialog.h | 86 +- src/dockspace.h | 39 - src/document.cpp | 245 ++- src/document.h | 72 +- src/events.h | 16 - src/frame_properties.cpp | 124 -- src/{ => imgui}/dockspace.cpp | 24 +- src/imgui/dockspace.h | 41 + src/{ => imgui}/documents.cpp | 11 +- src/{ => imgui}/documents.h | 4 +- src/{imgui.cpp => imgui/imgui_.cpp} | 196 +-- src/{imgui.h => imgui/imgui_.h} | 49 +- src/{ => imgui}/taskbar.cpp | 138 +- src/imgui/taskbar.h | 31 + src/{ => imgui}/toast.cpp | 6 +- src/{ => imgui}/toast.h | 6 +- src/{ => imgui/window}/animation_preview.cpp | 131 +- src/imgui/window/animation_preview.h | 21 + src/{ => imgui/window}/animations.cpp | 55 +- src/imgui/window/animations.h | 17 + src/imgui/window/events.cpp | 160 ++ src/imgui/window/events.h | 18 + src/imgui/window/frame_properties.cpp | 125 ++ src/{ => imgui/window}/frame_properties.h | 4 +- src/{ => imgui/window}/layers.cpp | 34 +- src/{ => imgui/window}/layers.h | 4 +- src/{ => imgui/window}/nulls.cpp | 30 +- src/{ => imgui/window}/nulls.h | 4 +- src/imgui/window/onionskin.cpp | 55 + src/{ => imgui/window}/onionskin.h | 4 +- src/{events.cpp => imgui/window/sounds.cpp} | 74 +- src/imgui/window/sounds.h | 16 + src/{ => imgui/window}/spritesheet_editor.cpp | 18 +- src/{ => imgui/window}/spritesheet_editor.h | 6 +- src/{ => imgui/window}/spritesheets.cpp | 34 +- src/imgui/window/spritesheets.h | 16 + src/{ => imgui/window}/timeline.cpp | 74 +- src/imgui/window/timeline.h | 39 + src/{ => imgui/window}/tools.cpp | 46 +- src/{ => imgui/window}/tools.h | 6 +- src/{ => imgui/window}/welcome.cpp | 32 +- src/imgui/window/welcome.h | 16 + src/loader.cpp | 55 +- src/loader.h | 6 +- src/log.cpp | 17 +- src/log.h | 9 +- src/main.cpp | 7 +- src/manager.cpp | 14 +- src/manager.h | 15 +- src/onionskin.cpp | 45 - src/playback.cpp | 2 +- src/playback.h | 2 +- src/render.cpp | 96 ++ src/render.h | 32 + src/resource/audio.cpp | 49 + src/resource/audio.h | 24 + src/{ => resource}/font.cpp | 2 +- src/{ => resource}/font.h | 12 +- src/{ => resource}/icon.h | 2 +- src/{ => resource}/shader.cpp | 4 +- src/{ => resource}/shader.h | 7 +- src/{ => resource}/texture.cpp | 96 +- src/{ => resource}/texture.h | 16 +- src/resources.cpp | 7 +- src/resources.h | 8 +- src/settings.cpp | 11 +- src/settings.h | 12 +- src/snapshots.cpp | 18 +- src/snapshots.h | 9 +- src/spritesheets.h | 17 - src/state.cpp | 15 +- src/state.h | 24 +- src/taskbar.h | 31 - src/timeline.h | 44 - src/tool.h | 26 +- src/types.h | 35 +- src/{filesystem.cpp => util/filesystem_.cpp} | 9 +- src/{filesystem.h => util/filesystem_.h} | 3 +- src/util/map_.h | 24 + src/{math.cpp => util/math_.cpp} | 4 +- src/{math.h => util/math_.h} | 9 +- src/{util.cpp => util/string_.cpp} | 30 +- src/util/string_.h | 11 + src/util/time_.cpp | 16 + src/util/time_.h | 10 + src/util/unordered_map_.h | 11 + src/{util.h => util/vector_.h} | 47 - src/util/xml_.cpp | 38 + src/{xml.h => util/xml_.h} | 2 +- src/welcome.h | 16 - src/xml.cpp | 37 - 125 files changed, 4073 insertions(+), 3011 deletions(-) delete mode 100644 src/animation_preview.h delete mode 100644 src/animations.h delete mode 100644 src/anm2.cpp delete mode 100644 src/anm2.h create mode 100644 src/anm2/animation.cpp create mode 100644 src/anm2/animation.h create mode 100644 src/anm2/animations.cpp create mode 100644 src/anm2/animations.h create mode 100644 src/anm2/anm2.cpp create mode 100644 src/anm2/anm2.h create mode 100644 src/anm2/content.cpp create mode 100644 src/anm2/content.h create mode 100644 src/anm2/event.cpp create mode 100644 src/anm2/event.h create mode 100644 src/anm2/frame.cpp create mode 100644 src/anm2/frame.h create mode 100644 src/anm2/info.cpp create mode 100644 src/anm2/info.h create mode 100644 src/anm2/item.cpp create mode 100644 src/anm2/item.h create mode 100644 src/anm2/layer.cpp create mode 100644 src/anm2/layer.h create mode 100644 src/anm2/null.cpp create mode 100644 src/anm2/null.h create mode 100644 src/anm2/sound.cpp create mode 100644 src/anm2/sound.h create mode 100644 src/anm2/spritesheet.cpp create mode 100644 src/anm2/spritesheet.h delete mode 100644 src/dockspace.h delete mode 100644 src/events.h delete mode 100644 src/frame_properties.cpp rename src/{ => imgui}/dockspace.cpp (83%) create mode 100644 src/imgui/dockspace.h rename src/{ => imgui}/documents.cpp (94%) rename src/{ => imgui}/documents.h (70%) rename src/{imgui.cpp => imgui/imgui_.cpp} (87%) rename src/{imgui.h => imgui/imgui_.h} (87%) rename src/{ => imgui}/taskbar.cpp (80%) create mode 100644 src/imgui/taskbar.h rename src/{ => imgui}/toast.cpp (97%) rename src/{ => imgui}/toast.h (85%) rename src/{ => imgui/window}/animation_preview.cpp (80%) create mode 100644 src/imgui/window/animation_preview.h rename src/{ => imgui/window}/animations.cpp (80%) create mode 100644 src/imgui/window/animations.h create mode 100644 src/imgui/window/events.cpp create mode 100644 src/imgui/window/events.h create mode 100644 src/imgui/window/frame_properties.cpp rename src/{ => imgui/window}/frame_properties.h (53%) rename src/{ => imgui/window}/layers.cpp (79%) rename src/{ => imgui/window}/layers.h (52%) rename src/{ => imgui/window}/nulls.cpp (82%) rename src/{ => imgui/window}/nulls.h (52%) create mode 100644 src/imgui/window/onionskin.cpp rename src/{ => imgui/window}/onionskin.h (54%) rename src/{events.cpp => imgui/window/sounds.cpp} (54%) create mode 100644 src/imgui/window/sounds.h rename src/{ => imgui/window}/spritesheet_editor.cpp (96%) rename src/{ => imgui/window}/spritesheet_editor.h (55%) rename src/{ => imgui/window}/spritesheets.cpp (90%) create mode 100644 src/imgui/window/spritesheets.h rename src/{ => imgui/window}/timeline.cpp (96%) create mode 100644 src/imgui/window/timeline.h rename src/{ => imgui/window}/tools.cpp (66%) rename src/{ => imgui/window}/tools.h (51%) rename src/{ => imgui/window}/welcome.cpp (69%) create mode 100644 src/imgui/window/welcome.h delete mode 100644 src/onionskin.cpp create mode 100644 src/render.cpp create mode 100644 src/render.h create mode 100644 src/resource/audio.cpp create mode 100644 src/resource/audio.h rename src/{ => resource}/font.cpp (95%) rename src/{ => resource}/font.h (99%) rename src/{ => resource}/icon.h (99%) rename src/{ => resource}/shader.cpp (96%) rename src/{ => resource}/shader.h (98%) rename src/{ => resource}/texture.cpp (64%) rename src/{ => resource}/texture.h (74%) delete mode 100644 src/spritesheets.h delete mode 100644 src/taskbar.h delete mode 100644 src/timeline.h rename src/{filesystem.cpp => util/filesystem_.cpp} (87%) rename src/{filesystem.h => util/filesystem_.h} (82%) create mode 100644 src/util/map_.h rename src/{math.cpp => util/math_.cpp} (98%) rename src/{math.h => util/math_.h} (88%) rename src/{util.cpp => util/string_.cpp} (55%) create mode 100644 src/util/string_.h create mode 100644 src/util/time_.cpp create mode 100644 src/util/time_.h create mode 100644 src/util/unordered_map_.h rename src/{util.h => util/vector_.h} (67%) create mode 100644 src/util/xml_.cpp rename src/{xml.h => util/xml_.h} (93%) delete mode 100644 src/welcome.h delete mode 100644 src/xml.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9316794..dd30ade 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,10 +19,30 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) -set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) +set(SDL_SHARED OFF CACHE BOOL "" FORCE) +set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) +set(SDL_SENSOR OFF CACHE BOOL "" FORCE) +set(SDL_HIDAPI OFF CACHE BOOL "" FORCE) +set(SDL_CAMERA OFF CACHE BOOL "" FORCE) +set(SDL_TRAY OFF CACHE BOOL "" FORCE) add_subdirectory(external/SDL EXCLUDE_FROM_ALL) +set(SDLMIXER_DEPS_SHARED OFF CACHE BOOL "" FORCE) +set(SDLMIXER_FLAC_LIBFLAC OFF CACHE BOOL "" FORCE) +set(SDLMIXER_GME OFF CACHE BOOL "" FORCE) +set(SDLMIXER_MOD_XMP OFF CACHE BOOL "" FORCE) +set(SDLMIXER_MP3_MPG123 OFF CACHE BOOL "" FORCE) +set(SDLMIXER_MIDI_FLUIDSYNTH OFF CACHE BOOL "" FORCE) +set(SDLMIXER_OPUS OFF CACHE BOOL "" FORCE) +set(SDLMIXER_VORBIS_VORBISFILE OFF CACHE BOOL "" FORCE) +set(SDLMIXER_VORBIS_TREMOR OFF CACHE BOOL "" FORCE) +set(SDLMIXER_WAVPACK OFF CACHE BOOL "" FORCE) +set(SDLMIXER_TEST OFF CACHE BOOL "" FORCE) +set(SDLMIXER_INSTALL OFF CACHE BOOL "" FORCE) +add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL) + add_subdirectory(external/lunasvg) set(GLAD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp) @@ -39,6 +59,18 @@ set(IMGUI_SRC set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp) file(GLOB PROJECT_SRC CONFIGURE_DEPENDS + src/anm2/*.cpp + src/anm2/*.h + src/resource/*.cpp + src/resource/*.h + src/imgui/*.cpp + src/imgui/*.h + src/imgui/window/*.cpp + src/imgui/window/*.h + src/util/*.cpp + src/util/*.h + src/window/*.cpp + src/window/*.h src/*.cpp src/*.h ) @@ -88,6 +120,8 @@ target_include_directories(${PROJECT_NAME} PRIVATE external/glm external/tinyxml2 external/lunasvg + external/SDL + external/SDL_mixer include include/glad src @@ -96,7 +130,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE src/util ) -target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static lunasvg) +target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mixer lunasvg) message(STATUS "System: ${CMAKE_SYSTEM_NAME}") message(STATUS "Project: ${PROJECT_NAME}") diff --git a/src/animation_preview.h b/src/animation_preview.h deleted file mode 100644 index 65a1a18..0000000 --- a/src/animation_preview.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "canvas.h" -#include "manager.h" -#include "resources.h" -#include "settings.h" - -namespace anm2ed::animation_preview -{ - class AnimationPreview : public canvas::Canvas - { - bool isPreviewHovered{}; - glm::ivec2 mousePos{}; - - public: - AnimationPreview(); - void update(manager::Manager&, settings::Settings&, resources::Resources&); - }; -} diff --git a/src/animations.h b/src/animations.h deleted file mode 100644 index e03e7e9..0000000 --- a/src/animations.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "clipboard.h" -#include "imgui.h" -#include "manager.h" -#include "resources.h" -#include "settings.h" - -namespace anm2ed::animations -{ - class Animations - { - imgui::PopupHelper mergePopup{imgui::PopupHelper("Merge Animations")}; - - public: - void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); - }; -} diff --git a/src/anm2.cpp b/src/anm2.cpp deleted file mode 100644 index f7e644b..0000000 --- a/src/anm2.cpp +++ /dev/null @@ -1,1453 +0,0 @@ -#include "anm2.h" - -#include - -#include "filesystem.h" -#include "math.h" -#include "texture.h" -#include "util.h" -#include "xml.h" - -using namespace tinyxml2; -using namespace anm2ed::filesystem; -using namespace anm2ed::texture; -using namespace anm2ed::types; -using namespace anm2ed::util; -using namespace glm; - -namespace anm2ed::anm2 -{ - Info::Info() = default; - - Info::Info(XMLElement* element) - { - if (!element) return; - xml::query_string_attribute(element, "CreatedBy", &createdBy); - xml::query_string_attribute(element, "CreatedOn", &createdOn); - element->QueryIntAttribute("Fps", &fps); - element->QueryIntAttribute("Version", &version); - } - - void Info::serialize(XMLDocument& document, XMLElement* parent) - { - auto infoElement = document.NewElement("Info"); - infoElement->SetAttribute("CreatedBy", createdBy.c_str()); - infoElement->SetAttribute("CreatedOn", createdOn.c_str()); - infoElement->SetAttribute("Fps", fps); - infoElement->SetAttribute("Version", version); - parent->InsertEndChild(infoElement); - } - - Spritesheet::Spritesheet() = default; - - Spritesheet::Spritesheet(XMLElement* element, int& id) - { - if (!element) return; - element->QueryIntAttribute("Id", &id); - xml::query_path_attribute(element, "Path", &path); - // Spritesheet paths from Isaac Rebirth are made with the assumption that - // the paths are case-insensitive (as the game was developed on Windows) - // However when using the resource dumper, the spritesheet paths are all lowercase (on Linux anyway) - // If the check doesn't work, set the spritesheet path to lowercase - // If that check doesn't work, replace backslashes with slashes - // At the minimum this should make all textures be able to be loaded on Linux - // If it doesn't work beyond that then that's on the user :^) - if (!path_is_exist(path)) path = string::to_lower(path); - if (!path_is_exist(path)) path = string::replace_backslash(path); - texture = Texture(path); - } - - Spritesheet::Spritesheet(const std::string& directory, const std::string& path) - { - this->path = !path.empty() ? path : this->path.string(); - WorkingDirectory workingDirectory(directory); - texture = Texture(this->path); - } - - bool Spritesheet::save(const std::string& directory, const std::string& path) - { - this->path = !path.empty() ? path : this->path.string(); - WorkingDirectory workingDirectory(directory); - return texture.write_png(this->path); - } - - void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id) - { - auto element = document.NewElement("Spritesheet"); - element->SetAttribute("Id", id); - element->SetAttribute("Path", path.c_str()); - parent->InsertEndChild(element); - } - - void Spritesheet::reload(const std::string& directory) - { - *this = Spritesheet(directory, this->path); - } - - bool Spritesheet::is_valid() - { - return texture.is_valid(); - } - - std::string Spritesheet::to_string(int id) - { - XMLDocument document{}; - - auto* element = document.NewElement("Spritesheet"); - - element->SetAttribute("Id", id); - element->SetAttribute("Path", path.c_str()); - - document.InsertFirstChild(element); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - - Layer::Layer() = default; - - Layer::Layer(XMLElement* element, int& id) - { - if (!element) return; - element->QueryIntAttribute("Id", &id); - xml::query_string_attribute(element, "Name", &name); - element->QueryIntAttribute("SpritesheetId", &spritesheetID); - } - - void Layer::serialize(XMLDocument& document, XMLElement* parent, int id) - { - auto element = document.NewElement("Layer"); - element->SetAttribute("Id", id); - element->SetAttribute("Name", name.c_str()); - element->SetAttribute("SpritesheetId", spritesheetID); - parent->InsertEndChild(element); - } - - std::string Layer::to_string(int id) - { - XMLDocument document{}; - - auto* element = document.NewElement("Layer"); - - element->SetAttribute("Id", id); - element->SetAttribute("Name", name.c_str()); - element->SetAttribute("SpritesheetId", spritesheetID); - - document.InsertFirstChild(element); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - - Null::Null() = default; - - Null::Null(XMLElement* element, int& id) - { - if (!element) return; - element->QueryIntAttribute("Id", &id); - xml::query_string_attribute(element, "Name", &name); - element->QueryBoolAttribute("ShowRect", &isShowRect); - } - - void Null::serialize(XMLDocument& document, XMLElement* parent, int id) - { - auto element = document.NewElement("Null"); - element->SetAttribute("Id", id); - element->SetAttribute("Name", name.c_str()); - if (isShowRect) element->SetAttribute("ShowRect", isShowRect); - - parent->InsertEndChild(element); - } - - std::string Null::to_string(int id) - { - XMLDocument document{}; - - auto* element = document.NewElement("Null"); - - element->SetAttribute("Id", id); - element->SetAttribute("Name", name.c_str()); - - document.InsertFirstChild(element); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - - Event::Event() = default; - - Event::Event(XMLElement* element, int& id) - { - if (!element) return; - element->QueryIntAttribute("Id", &id); - xml::query_string_attribute(element, "Name", &name); - } - - void Event::serialize(XMLDocument& document, XMLElement* parent, int id) - { - auto element = document.NewElement("Event"); - element->SetAttribute("Id", id); - element->SetAttribute("Name", name.c_str()); - parent->InsertEndChild(element); - } - - std::string Event::to_string(int id) - { - XMLDocument document{}; - - auto* element = document.NewElement("Event"); - - element->SetAttribute("Id", id); - element->SetAttribute("Name", name.c_str()); - - document.InsertFirstChild(element); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - - Content::Content() = default; - - void Content::serialize(XMLDocument& document, XMLElement* parent) - { - auto element = document.NewElement("Content"); - - auto spritesheetsElement = document.NewElement("Spritesheets"); - for (auto& [id, spritesheet] : spritesheets) - spritesheet.serialize(document, spritesheetsElement, id); - element->InsertEndChild(spritesheetsElement); - - auto layersElement = document.NewElement("Layers"); - for (auto& [id, layer] : layers) - layer.serialize(document, layersElement, id); - element->InsertEndChild(layersElement); - - auto nullsElement = document.NewElement("Nulls"); - for (auto& [id, null] : nulls) - null.serialize(document, nullsElement, id); - element->InsertEndChild(nullsElement); - - auto eventsElement = document.NewElement("Events"); - for (auto& [id, event] : events) - event.serialize(document, eventsElement, id); - element->InsertEndChild(eventsElement); - - parent->InsertEndChild(element); - } - - Content::Content(XMLElement* element) - { - int id{}; - - if (auto spritesheetsElement = element->FirstChildElement("Spritesheets")) - { - for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child; - child = child->NextSiblingElement("Spritesheet")) - spritesheets[id] = Spritesheet(child, id); - } - - if (auto layersElement = element->FirstChildElement("Layers")) - { - for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer")) - layers[id] = Layer(child, id); - } - - if (auto nullsElement = element->FirstChildElement("Nulls")) - { - for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null")) - nulls[id] = Null(child, id); - } - - if (auto eventsElement = element->FirstChildElement("Events")) - { - for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event")) - events[id] = Event(child, id); - } - } - - std::set Content::spritesheets_unused() - { - std::set used; - for (auto& layer : layers | std::views::values) - if (layer.spritesheetID != -1) used.insert(layer.spritesheetID); - - std::set unused; - for (auto& id : spritesheets | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - void Content::layer_add(int& id) - { - id = map::next_id_get(layers); - layers[id] = Layer(); - } - - void Content::null_add(int& id) - { - id = map::next_id_get(nulls); - nulls[id] = Null(); - } - - void Content::event_add(int& id) - { - id = map::next_id_get(events); - events[id] = Event(); - } - - bool Content::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type, - std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Spritesheet")) - { - if (errorString) *errorString = "No valid spritesheet(s)."; - return false; - } - - WorkingDirectory workingDirectory(directory); - - for (auto element = document.FirstChildElement("Spritesheet"); element; - element = element->NextSiblingElement("Spritesheet")) - { - auto spritesheet = Spritesheet(element, id); - - if (type == merge::APPEND) id = map::next_id_get(spritesheets); - - spritesheets[id] = std::move(spritesheet); - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Content::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Layer")) - { - if (errorString) *errorString = "No valid layer(s)."; - return false; - } - - for (auto element = document.FirstChildElement("Layer"); element; element = element->NextSiblingElement("Layer")) - { - auto layer = Layer(element, id); - - if (type == merge::APPEND) id = map::next_id_get(layers); - - layers[id] = layer; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Content::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Null")) - { - if (errorString) *errorString = "No valid null(s)."; - return false; - } - - for (auto element = document.FirstChildElement("Null"); element; element = element->NextSiblingElement("Null")) - { - auto layer = Null(element, id); - - if (type == merge::APPEND) id = map::next_id_get(nulls); - - nulls[id] = layer; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Content::events_deserialize(const std::string& string, merge::Type type, std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Event")) - { - if (errorString) *errorString = "No valid event(s)."; - return false; - } - - for (auto element = document.FirstChildElement("Event"); element; element = element->NextSiblingElement("Event")) - { - auto layer = Event(element, id); - - if (type == merge::APPEND) id = map::next_id_get(events); - - events[id] = layer; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - Frame::Frame() = default; - - Frame::Frame(XMLElement* element, Type type) - { - if (!element) return; - - if (type != TRIGGER) - { - element->QueryFloatAttribute("XPosition", &position.x); - element->QueryFloatAttribute("YPosition", &position.y); - if (type == LAYER) - { - element->QueryFloatAttribute("XPivot", &pivot.x); - element->QueryFloatAttribute("YPivot", &pivot.y); - element->QueryFloatAttribute("XCrop", &crop.x); - element->QueryFloatAttribute("YCrop", &crop.y); - element->QueryFloatAttribute("Width", &size.x); - element->QueryFloatAttribute("Height", &size.y); - } - element->QueryFloatAttribute("XScale", &scale.x); - element->QueryFloatAttribute("YScale", &scale.y); - element->QueryIntAttribute("Delay", &delay); - element->QueryBoolAttribute("Visible", &isVisible); - xml::query_color_attribute(element, "RedTint", tint.r); - xml::query_color_attribute(element, "GreenTint", tint.g); - xml::query_color_attribute(element, "BlueTint", tint.b); - xml::query_color_attribute(element, "AlphaTint", tint.a); - xml::query_color_attribute(element, "RedOffset", colorOffset.r); - xml::query_color_attribute(element, "GreenOffset", colorOffset.g); - xml::query_color_attribute(element, "BlueOffset", colorOffset.b); - element->QueryFloatAttribute("Rotation", &rotation); - element->QueryBoolAttribute("Interpolated", &isInterpolated); - } - else - { - element->QueryIntAttribute("EventId", &eventID); - element->QueryIntAttribute("AtFrame", &atFrame); - } - } - - void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type) - { - auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame"); - - switch (type) - { - case ROOT: - case NULL_: - element->SetAttribute("XPosition", position.x); - element->SetAttribute("YPosition", position.y); - element->SetAttribute("Delay", delay); - element->SetAttribute("Visible", isVisible); - element->SetAttribute("XScale", scale.x); - element->SetAttribute("YScale", scale.y); - element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); - element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); - element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); - element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); - element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); - element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); - element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); - element->SetAttribute("Rotation", rotation); - element->SetAttribute("Interpolated", isInterpolated); - break; - case LAYER: - element->SetAttribute("XPosition", position.x); - element->SetAttribute("YPosition", position.y); - element->SetAttribute("XPivot", pivot.x); - element->SetAttribute("YPivot", pivot.y); - element->SetAttribute("XCrop", crop.x); - element->SetAttribute("YCrop", crop.y); - element->SetAttribute("Width", size.x); - element->SetAttribute("Height", size.y); - element->SetAttribute("XScale", scale.x); - element->SetAttribute("YScale", scale.y); - element->SetAttribute("Delay", delay); - element->SetAttribute("Visible", isVisible); - element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); - element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); - element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); - element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); - element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); - element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); - element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); - element->SetAttribute("Rotation", rotation); - element->SetAttribute("Interpolated", isInterpolated); - break; - case TRIGGER: - element->SetAttribute("EventId", eventID); - element->SetAttribute("AtFrame", atFrame); - break; - default: - break; - } - - parent->InsertEndChild(element); - } - - std::string Frame::to_string(Type type) - { - XMLDocument document; - auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame"); - - switch (type) - { - case ROOT: - case NULL_: - element->SetAttribute("XPosition", position.x); - element->SetAttribute("YPosition", position.y); - element->SetAttribute("Delay", delay); - element->SetAttribute("Visible", isVisible); - element->SetAttribute("XScale", scale.x); - element->SetAttribute("YScale", scale.y); - element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); - element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); - element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); - element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); - element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); - element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); - element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); - element->SetAttribute("Rotation", rotation); - element->SetAttribute("Interpolated", isInterpolated); - break; - case LAYER: - element->SetAttribute("XPosition", position.x); - element->SetAttribute("YPosition", position.y); - element->SetAttribute("XPivot", pivot.x); - element->SetAttribute("YPivot", pivot.y); - element->SetAttribute("XCrop", crop.x); - element->SetAttribute("YCrop", crop.y); - element->SetAttribute("Width", size.x); - element->SetAttribute("Height", size.y); - element->SetAttribute("XScale", scale.x); - element->SetAttribute("YScale", scale.y); - element->SetAttribute("Delay", delay); - element->SetAttribute("Visible", isVisible); - element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); - element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); - element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); - element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); - element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); - element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); - element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); - element->SetAttribute("Rotation", rotation); - element->SetAttribute("Interpolated", isInterpolated); - break; - case TRIGGER: - element->SetAttribute("EventId", eventID); - element->SetAttribute("AtFrame", atFrame); - break; - default: - break; - } - - document.InsertFirstChild(element); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - - void Frame::shorten() - { - delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); - } - - void Frame::extend() - { - delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); - } - - Item::Item() = default; - - Item::Item(XMLElement* element, Type type, int* id) - { - if (type == LAYER && id) element->QueryIntAttribute("LayerId", id); - if (type == NULL_ && id) element->QueryIntAttribute("NullId", id); - - element->QueryBoolAttribute("Visible", &isVisible); - - for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame"); - child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame")) - frames.push_back(Frame(child, type)); - } - - void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id) - { - auto typeString = type == ROOT ? "RootAnimation" - : type == LAYER ? "LayerAnimation" - : type == NULL_ ? "NullAnimation" - : "Triggers"; - - auto element = document.NewElement(typeString); - - if (type == LAYER) element->SetAttribute("LayerId", id); - if (type == NULL_) element->SetAttribute("NullId", id); - if (type == LAYER || type == NULL_) element->SetAttribute("Visible", isVisible); - - for (auto& frame : frames) - frame.serialize(document, element, type); - parent->InsertEndChild(element); - } - - int Item::length(Type type) - { - int length{}; - - if (type == TRIGGER) - for (auto& frame : frames) - length = frame.atFrame > length ? frame.atFrame : length; - else - for (auto& frame : frames) - length += frame.delay; - - return length; - } - - Frame Item::frame_generate(float time, Type type) - { - Frame frame{}; - frame.isVisible = false; - - if (frames.empty()) return frame; - - Frame* frameNext = nullptr; - int delayCurrent = 0; - int delayNext = 0; - - for (auto [i, iFrame] : std::views::enumerate(frames)) - { - if (type == TRIGGER) - { - if ((int)time == iFrame.atFrame) - { - frame = iFrame; - break; - } - } - else - { - frame = iFrame; - - delayNext += frame.delay; - - if (time >= delayCurrent && time < delayNext) - { - if (i + 1 < (int)frames.size()) - frameNext = &frames[i + 1]; - else - frameNext = nullptr; - break; - } - - delayCurrent += frame.delay; - } - } - - if (type != TRIGGER && frame.isInterpolated && frameNext && frame.delay > 1) - { - auto interpolation = (time - delayCurrent) / (delayNext - delayCurrent); - - frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation); - frame.position = glm::mix(frame.position, frameNext->position, interpolation); - frame.scale = glm::mix(frame.scale, frameNext->scale, interpolation); - frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, interpolation); - frame.tint = glm::mix(frame.tint, frameNext->tint, interpolation); - } - - return frame; - } - - void Item::frames_change(anm2::FrameChange& change, frame_change::Type type, int start, int numberFrames) - { - auto useStart = numberFrames > -1 ? start : 0; - auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size(); - vector::clamp_in_bounds(frames, useStart); - end = glm::clamp(end, start, (int)frames.size()); - - for (int i = useStart; i < end; i++) - { - Frame& frame = frames[i]; - - if (change.isVisible) frame.isVisible = *change.isVisible; - if (change.isInterpolated) frame.isInterpolated = *change.isInterpolated; - - switch (type) - { - case frame_change::ADJUST: - if (change.rotation) frame.rotation = *change.rotation; - if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, *change.delay); - if (change.crop) frame.crop = *change.crop; - if (change.pivot) frame.pivot = *change.pivot; - if (change.position) frame.position = *change.position; - if (change.size) frame.size = *change.size; - if (change.scale) frame.scale = *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(*change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(*change.tint, 0.0f, 1.0f); - break; - - case frame_change::ADD: - if (change.rotation) frame.rotation += *change.rotation; - if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay + *change.delay); - if (change.crop) frame.crop += *change.crop; - if (change.pivot) frame.pivot += *change.pivot; - if (change.position) frame.position += *change.position; - if (change.size) frame.size += *change.size; - if (change.scale) frame.scale += *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset + *change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(frame.tint + *change.tint, 0.0f, 1.0f); - break; - - case frame_change::SUBTRACT: - if (change.rotation) frame.rotation -= *change.rotation; - if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay - *change.delay); - if (change.crop) frame.crop -= *change.crop; - if (change.pivot) frame.pivot -= *change.pivot; - if (change.position) frame.position -= *change.position; - if (change.size) frame.size -= *change.size; - if (change.scale) frame.scale -= *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset - *change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(frame.tint - *change.tint, 0.0f, 1.0f); - break; - } - } - } - - bool Item::frames_deserialize(const std::string& string, Type type, int start, std::set& indices, - std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - if (!document.FirstChildElement("Frame")) - { - if (errorString) *errorString = "No valid frame(s)."; - return false; - } - - int count{}; - for (auto element = document.FirstChildElement("Frame"); element; element = element->NextSiblingElement("Frame")) - { - auto index = start + count; - frames.insert(frames.begin() + start + count, Frame(element, type)); - indices.insert(index); - count++; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - Animation::Animation() = default; - - Animation::Animation(XMLElement* element) - { - int id{}; - - xml::query_string_attribute(element, "Name", &name); - element->QueryIntAttribute("FrameNum", &frameNum); - element->QueryBoolAttribute("Loop", &isLoop); - - if (auto rootAnimationElement = element->FirstChildElement("RootAnimation")) - rootAnimation = Item(rootAnimationElement, ROOT); - - if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations")) - { - for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child; - child = child->NextSiblingElement("LayerAnimation")) - { - layerAnimations[id] = Item(child, LAYER, &id); - layerOrder.push_back(id); - } - } - - if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations")) - { - for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child; - child = child->NextSiblingElement("NullAnimation")) - nullAnimations[id] = Item(child, NULL_, &id); - } - - if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER); - } - - Item* Animation::item_get(Type type, int id) - { - switch (type) - { - case ROOT: - return &rootAnimation; - case LAYER: - return unordered_map::find(layerAnimations, id); - case NULL_: - return map::find(nullAnimations, id); - case TRIGGER: - return &triggers; - default: - return nullptr; - } - return nullptr; - } - - void Animation::item_remove(Type type, int id) - { - switch (type) - { - case LAYER: - layerAnimations.erase(id); - for (auto [i, value] : std::views::enumerate(layerOrder)) - if (value == id) layerOrder.erase(layerOrder.begin() + i); - break; - case NULL_: - nullAnimations.erase(id); - break; - case ROOT: - case TRIGGER: - default: - break; - } - } - - void Animation::serialize(XMLDocument& document, XMLElement* parent) - { - auto element = document.NewElement("Animation"); - element->SetAttribute("Name", name.c_str()); - element->SetAttribute("FrameNum", frameNum); - element->SetAttribute("Loop", isLoop); - - rootAnimation.serialize(document, element, ROOT); - - auto layerAnimationsElement = document.NewElement("LayerAnimations"); - for (auto& i : layerOrder) - { - Item& layerAnimation = layerAnimations.at(i); - layerAnimation.serialize(document, layerAnimationsElement, LAYER, i); - } - element->InsertEndChild(layerAnimationsElement); - - auto nullAnimationsElement = document.NewElement("NullAnimations"); - for (auto& [id, nullAnimation] : nullAnimations) - nullAnimation.serialize(document, nullAnimationsElement, NULL_, id); - element->InsertEndChild(nullAnimationsElement); - - triggers.serialize(document, element, TRIGGER); - - parent->InsertEndChild(element); - } - - int Animation::length() - { - int length{}; - - if (int rootAnimationLength = rootAnimation.length(ROOT); rootAnimationLength > length) - length = rootAnimationLength; - - for (auto& layerAnimation : layerAnimations | std::views::values) - if (int layerAnimationLength = layerAnimation.length(LAYER); layerAnimationLength > length) - length = layerAnimationLength; - - for (auto& nullAnimation : nullAnimations | std::views::values) - if (int nullAnimationLength = nullAnimation.length(NULL_); nullAnimationLength > length) - length = nullAnimationLength; - - if (int triggersLength = triggers.length(TRIGGER); triggersLength > length) length = triggersLength; - - return length; - } - - std::string Animation::to_string() - { - XMLDocument document{}; - - auto* element = document.NewElement("Animation"); - document.InsertFirstChild(element); - - element->SetAttribute("Name", name.c_str()); - element->SetAttribute("FrameNum", frameNum); - element->SetAttribute("Loop", isLoop); - - rootAnimation.serialize(document, element, ROOT); - - auto layerAnimationsElement = document.NewElement("LayerAnimations"); - for (auto& i : layerOrder) - { - Item& layerAnimation = layerAnimations.at(i); - layerAnimation.serialize(document, layerAnimationsElement, LAYER, i); - } - element->InsertEndChild(layerAnimationsElement); - - auto nullAnimationsElement = document.NewElement("NullAnimations"); - for (auto& [id, nullAnimation] : nullAnimations) - nullAnimation.serialize(document, nullAnimationsElement, NULL_, id); - element->InsertEndChild(nullAnimationsElement); - - triggers.serialize(document, element, TRIGGER); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - - vec4 Animation::rect(bool isRootTransform) - { - f32 minX = std::numeric_limits::infinity(); - f32 minY = std::numeric_limits::infinity(); - f32 maxX = -std::numeric_limits::infinity(); - f32 maxY = -std::numeric_limits::infinity(); - bool any = false; - - constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; - - for (float t = 0.0f; t < (float)frameNum; t += 1.0f) - { - mat4 transform(1.0f); - - if (isRootTransform) - { - auto root = rootAnimation.frame_generate(t, anm2::ROOT); - transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); - } - - for (auto& [id, layerAnimation] : layerAnimations) - { - auto frame = layerAnimation.frame_generate(t, anm2::LAYER); - - if (frame.size == vec2() || !frame.isVisible) continue; - - auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot, - math::percent_to_unit(frame.scale), frame.rotation); - for (auto& corner : CORNERS) - { - vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f); - minX = std::min(minX, world.x); - minY = std::min(minY, world.y); - maxX = std::max(maxX, world.x); - maxY = std::max(maxY, world.y); - any = true; - } - } - } - - if (!any) return vec4(-1.0f); - return {minX, minY, maxX - minX, maxY - minY}; - } - - Animations::Animations() = default; - - Animations::Animations(XMLElement* element) - { - xml::query_string_attribute(element, "DefaultAnimation", &defaultAnimation); - - for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation")) - items.push_back(Animation(child)); - } - - void Animations::serialize(XMLDocument& document, XMLElement* parent) - { - auto element = document.NewElement("Animations"); - element->SetAttribute("DefaultAnimation", defaultAnimation.c_str()); - for (auto& animation : items) - animation.serialize(document, element); - parent->InsertEndChild(element); - } - - int Animations::length() - { - int length{}; - - for (auto& animation : items) - if (int animationLength = animation.length(); animationLength > length) length = animationLength; - - return length; - } - - int Animations::merge(int target, std::set& sources, merge::Type type, bool isDeleteAfter) - { - Animation& animation = items.at(target); - - if (!animation.name.ends_with(MERGED_STRING)) animation.name = animation.name + " " + MERGED_STRING; - - auto merge_item = [&](Item& destination, Item& source) - { - switch (type) - { - case merge::APPEND: - destination.frames.insert(destination.frames.end(), source.frames.begin(), source.frames.end()); - break; - case merge::PREPEND: - destination.frames.insert(destination.frames.begin(), source.frames.begin(), source.frames.end()); - break; - case merge::REPLACE: - if (destination.frames.size() < source.frames.size()) destination.frames.resize(source.frames.size()); - for (int i = 0; i < (int)source.frames.size(); i++) - destination.frames[i] = source.frames[i]; - break; - case merge::IGNORE: - default: - break; - } - }; - - for (auto& i : sources) - { - if (i == target) continue; - if (i < 0 || i >= (int)items.size()) continue; - - auto& source = items.at(i); - - merge_item(animation.rootAnimation, source.rootAnimation); - - for (auto& [id, layerAnimation] : source.layerAnimations) - { - if (!animation.layerAnimations.contains(id)) - { - animation.layerAnimations[id] = layerAnimation; - animation.layerOrder.emplace_back(id); - } - merge_item(animation.layerAnimations[id], layerAnimation); - } - - for (auto& [id, nullAnimation] : source.nullAnimations) - { - if (!animation.nullAnimations.contains(id)) animation.nullAnimations[id] = nullAnimation; - merge_item(animation.nullAnimations[id], nullAnimation); - } - - merge_item(animation.triggers, source.triggers); - } - - if (isDeleteAfter) - { - for (auto& source : std::ranges::reverse_view(sources)) - { - if (source == target) continue; - items.erase(items.begin() + source); - } - } - - int finalIndex = target; - - if (isDeleteAfter) - { - int numDeletedBefore = 0; - for (auto& idx : sources) - { - if (idx == target) continue; - if (idx >= 0 && idx < target) ++numDeletedBefore; - } - finalIndex -= numDeletedBefore; - } - - return finalIndex; - } - - bool Animations::animations_deserialize(const std::string& string, int start, std::set& indices, - std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - if (!document.FirstChildElement("Animation")) - { - if (errorString) *errorString = "No valid animation(s)."; - return false; - } - - int count{}; - for (auto element = document.FirstChildElement("Animation"); element; - element = element->NextSiblingElement("Animation")) - { - auto index = start + count; - items.insert(items.begin() + start + count, Animation(element)); - indices.insert(index); - count++; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - Anm2::Anm2() - { - info.createdOn = time::get("%d-%B-%Y %I:%M:%S"); - } - - bool Anm2::serialize(const std::string& path, std::string* errorString) - { - XMLDocument document; - - auto* element = document.NewElement("AnimatedActor"); - document.InsertFirstChild(element); - - info.serialize(document, element); - content.serialize(document, element); - animations.serialize(document, element); - - if (document.SaveFile(path.c_str()) != XML_SUCCESS) - { - if (errorString) *errorString = document.ErrorStr(); - return false; - } - return true; - } - - std::string Anm2::to_string() - { - XMLDocument document; - - auto* element = document.NewElement("AnimatedActor"); - document.InsertFirstChild(element); - - info.serialize(document, element); - content.serialize(document, element); - animations.serialize(document, element); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - - Anm2::Anm2(const std::string& path, std::string* errorString) - { - XMLDocument document; - - if (document.LoadFile(path.c_str()) != XML_SUCCESS) - { - if (errorString) *errorString = document.ErrorStr(); - return; - } - - WorkingDirectory workingDirectory(path, true); - - const XMLElement* element = document.RootElement(); - - if (auto infoElement = element->FirstChildElement("Info")) info = Info((XMLElement*)infoElement); - - if (auto contentElement = element->FirstChildElement("Content")) content = Content((XMLElement*)contentElement); - - if (auto animationsElement = element->FirstChildElement("Animations")) - animations = Animations((XMLElement*)animationsElement); - } - - uint64_t Anm2::hash() - { - return std::hash{}(to_string()); - } - - Animation* Anm2::animation_get(Reference reference) - { - return vector::find(animations.items, reference.animationIndex); - } - - Item* Anm2::item_get(Reference reference) - { - if (Animation* animation = animation_get(reference)) - { - switch (reference.itemType) - { - case ROOT: - return &animation->rootAnimation; - case LAYER: - return unordered_map::find(animation->layerAnimations, reference.itemID); - case NULL_: - return map::find(animation->nullAnimations, reference.itemID); - case TRIGGER: - return &animation->triggers; - default: - return nullptr; - } - } - return nullptr; - } - - Frame* Anm2::frame_get(Reference reference) - { - Item* item = item_get(reference); - if (!item) return nullptr; - return vector::find(item->frames, reference.frameIndex); - return nullptr; - } - - bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id) - { - Spritesheet spritesheet(directory, path); - if (!spritesheet.is_valid()) return false; - id = map::next_id_get(content.spritesheets); - content.spritesheets[id] = std::move(spritesheet); - return true; - } - - void Anm2::spritesheet_remove(int id) - { - content.spritesheets.erase(id); - } - - Spritesheet* Anm2::spritesheet_get(int id) - { - return map::find(content.spritesheets, id); - } - - std::set Anm2::spritesheets_unused() - { - return content.spritesheets_unused(); - } - - Reference Anm2::layer_add(Reference reference, std::string name, int spritesheetID, locale::Type locale) - { - auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID; - auto& layer = content.layers[id]; - - layer.name = !name.empty() ? name : layer.name; - layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0; - - auto add = [&](Animation* animation, int id) - { - animation->layerAnimations[id] = Item(); - animation->layerOrder.push_back(id); - }; - - if (locale == locale::GLOBAL) - { - for (auto& animation : animations.items) - if (!animation.layerAnimations.contains(id)) add(&animation, id); - } - else if (locale == locale::LOCAL) - { - if (auto animation = animation_get(reference)) - if (!animation->layerAnimations.contains(id)) add(animation, id); - } - - return {reference.animationIndex, LAYER, id}; - } - - Reference Anm2::null_add(Reference reference, std::string name, locale::Type locale) - { - auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID; - auto& null = content.nulls[id]; - - null.name = !name.empty() ? name : null.name; - - auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); }; - - if (locale == locale::GLOBAL) - { - for (auto& animation : animations.items) - if (!animation.nullAnimations.contains(id)) add(&animation, id); - } - else if (locale == locale::LOCAL) - { - if (auto animation = animation_get(reference)) - if (!animation->nullAnimations.contains(id)) add(animation, id); - } - - return {reference.animationIndex, LAYER, id}; - } - - void Anm2::event_add(int& id) - { - content.event_add(id); - } - - std::set Anm2::events_unused(Reference reference) - { - std::set used{}; - std::set unused{}; - - if (auto animation = animation_get(reference); animation) - for (auto& frame : animation->triggers.frames) - used.insert(frame.eventID); - else - for (auto& animation : animations.items) - for (auto& frame : animation.triggers.frames) - used.insert(frame.eventID); - - for (auto& id : content.events | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - std::set Anm2::layers_unused(Reference reference) - { - std::set used{}; - std::set unused{}; - - if (auto animation = animation_get(reference); animation) - for (auto& id : animation->layerAnimations | std::views::keys) - used.insert(id); - else - for (auto& animation : animations.items) - for (auto& id : animation.layerAnimations | std::views::keys) - used.insert(id); - - for (auto& id : content.layers | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - std::set Anm2::nulls_unused(Reference reference) - { - std::set used{}; - std::set unused{}; - - if (auto animation = animation_get(reference); animation) - for (auto& id : animation->nullAnimations | std::views::keys) - used.insert(id); - else - for (auto& animation : animations.items) - for (auto& id : animation.nullAnimations | std::views::keys) - used.insert(id); - - for (auto& id : content.nulls | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - std::vector Anm2::event_names_get() - { - std::vector names{}; - for (auto& event : content.events | std::views::values) - names.push_back(event.name); - return names; - } - - std::vector Anm2::animation_names_get() - { - std::vector names{}; - for (auto& animation : animations.items) - names.push_back(animation.name); - return names; - } - - std::vector Anm2::spritesheet_names_get() - { - std::vector names{}; - for (auto& [id, spritesheet] : content.spritesheets) - names.push_back(std::format(SPRITESHEET_FORMAT, id, spritesheet.path.c_str())); - return names; - } - - void Anm2::bake(Reference reference, int interval, bool isRoundScale, bool isRoundRotation) - { - Item* item = item_get(reference); - if (!item) return; - - Frame* frame = frame_get(reference); - if (!frame) return; - - if (frame->delay == FRAME_DELAY_MIN) return; - - Reference referenceNext = reference; - referenceNext.frameIndex = reference.frameIndex + 1; - - Frame* frameNext = frame_get(referenceNext); - if (!frameNext) frameNext = frame; - - Frame baseFrame = *frame; - Frame baseFrameNext = *frameNext; - - int delay{}; - int index = reference.frameIndex; - - while (delay < baseFrame.delay) - { - float interpolation = (float)delay / baseFrame.delay; - - Frame baked = baseFrame; - baked.delay = std::min(interval, baseFrame.delay - delay); - baked.isInterpolated = (index == reference.frameIndex) ? baseFrame.isInterpolated : false; - - baked.rotation = glm::mix(baseFrame.rotation, baseFrameNext.rotation, interpolation); - baked.position = glm::mix(baseFrame.position, baseFrameNext.position, interpolation); - baked.scale = glm::mix(baseFrame.scale, baseFrameNext.scale, interpolation); - baked.colorOffset = glm::mix(baseFrame.colorOffset, baseFrameNext.colorOffset, interpolation); - baked.tint = glm::mix(baseFrame.tint, baseFrameNext.tint, interpolation); - - if (isRoundScale) baked.scale = vec2(ivec2(baked.scale)); - if (isRoundRotation) baked.rotation = (int)baked.rotation; - - if (index == reference.frameIndex) - item->frames[index] = baked; - else - item->frames.insert(item->frames.begin() + index, baked); - index++; - - delay += baked.delay; - } - } - - void Anm2::generate_from_grid(Reference reference, ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, - int count, int delay) - { - auto item = item_get(reference); - if (!item) return; - - for (int i = 0; i < count; i++) - { - auto row = i / columns; - auto column = i % columns; - - Frame frame{}; - - frame.delay = delay; - frame.pivot = pivot; - frame.size = size; - frame.crop = startPosition + ivec2(size.x * column, size.y * row); - - item->frames.emplace_back(frame); - } - } -} \ No newline at end of file diff --git a/src/anm2.h b/src/anm2.h deleted file mode 100644 index afcdea9..0000000 --- a/src/anm2.h +++ /dev/null @@ -1,264 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "texture.h" -#include "types.h" - -namespace anm2ed::anm2 -{ - constexpr auto FRAME_NUM_MIN = 1; - constexpr auto FRAME_NUM_MAX = 100000000; - constexpr auto FRAME_DELAY_MIN = 1; - constexpr auto FRAME_DELAY_MAX = FRAME_NUM_MAX; - constexpr auto FPS_MIN = 1; - constexpr auto FPS_MAX = 120; - - constexpr auto MERGED_STRING = "(Merged)"; - - constexpr auto NO_PATH = "(No Path)"; - constexpr auto LAYER_FORMAT = "#{} {} (Spritesheet: #{})"; - constexpr auto NULL_FORMAT = "#{} {}"; - constexpr auto SPRITESHEET_FORMAT_C = "#%d %s"; - constexpr auto SPRITESHEET_FORMAT = "#{} {}"; - - enum Type - { - NONE, - ROOT, - LAYER, - NULL_, - TRIGGER - }; - - class Reference - { - public: - int animationIndex{-1}; - Type itemType{NONE}; - int itemID{-1}; - int frameIndex{-1}; - int frameTime{-1}; - - auto operator<=>(const Reference&) const = default; - }; - - constexpr anm2::Reference REFERENCE_DEFAULT = {-1, anm2::NONE, -1, -1, -1}; - - class Info - { - public: - std::string createdBy{"robot"}; - std::string createdOn{}; - int fps = 30; - int version{}; - - Info(); - Info(tinyxml2::XMLElement*); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); - }; - - class Spritesheet - { - public: - std::filesystem::path path{}; - texture::Texture texture; - - Spritesheet(); - Spritesheet(tinyxml2::XMLElement*, int&); - Spritesheet(const std::string&, const std::string& = {}); - bool save(const std::string&, const std::string& = {}); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); - void reload(const std::string&); - bool is_valid(); - std::string to_string(int id); - }; - - class Layer - { - public: - std::string name{"New Layer"}; - int spritesheetID{}; - - Layer(); - Layer(tinyxml2::XMLElement*, int&); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); - std::string to_string(int); - }; - - class Null - { - public: - std::string name{"New Null"}; - bool isShowRect{}; - - Null(); - Null(tinyxml2::XMLElement*, int&); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); - std::string to_string(int); - }; - - class Event - { - public: - std::string name{"New Event"}; - - Event(); - Event(tinyxml2::XMLElement*, int&); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); - std::string to_string(int); - }; - - struct Content - { - std::map spritesheets{}; - std::map layers{}; - std::map nulls{}; - std::map events{}; - - Content(); - - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); - Content(tinyxml2::XMLElement*); - bool spritesheet_add(const std::string&, const std::string&, int&); - void spritesheet_remove(int&); - std::set spritesheets_unused(); - void layer_add(int&); - void null_add(int&); - void event_add(int&); - bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr); - bool layers_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); - bool nulls_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); - bool events_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); - }; - -#define MEMBERS \ - X(isVisible, bool, true) \ - X(isInterpolated, bool, false) \ - X(rotation, float, 0.0f) \ - X(delay, int, FRAME_DELAY_MIN) \ - X(atFrame, int, -1) \ - X(eventID, int, -1) \ - X(pivot, glm::vec2, {}) \ - X(crop, glm::vec2, {}) \ - X(position, glm::vec2, {}) \ - X(size, glm::vec2, {}) \ - X(scale, glm::vec2, glm::vec2(100.0f)) \ - X(colorOffset, glm::vec3, types::color::TRANSPARENT) \ - X(tint, glm::vec4, types::color::WHITE) - - class Frame - { - public: -#define X(name, type, ...) type name = __VA_ARGS__; - MEMBERS -#undef X - - Frame(); - Frame(tinyxml2::XMLElement*, Type); - std::string to_string(Type type); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type); - void shorten(); - void extend(); - }; - - struct FrameChange - { -#define X(name, type, ...) std::optional name{}; - MEMBERS -#undef X - }; - -#undef MEMBERS - - class Item - { - public: - std::vector frames{}; - bool isVisible{true}; - - Item(); - - Item(tinyxml2::XMLElement*, Type, int* = nullptr); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1); - int length(Type); - Frame frame_generate(float, Type); - void frames_change(anm2::FrameChange&, types::frame_change::Type, int, int = 0); - bool frames_deserialize(const std::string&, Type, int, std::set&, std::string*); - }; - - class Animation - { - public: - std::string name{"New Animation"}; - int frameNum{FRAME_NUM_MIN}; - bool isLoop{true}; - Item rootAnimation; - std::unordered_map layerAnimations{}; - std::vector layerOrder{}; - std::map nullAnimations{}; - Item triggers; - - Animation(); - Animation(tinyxml2::XMLElement*); - Item* item_get(Type, int = -1); - void item_remove(Type, int = -1); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); - int length(); - glm::vec4 rect(bool); - std::string to_string(); - }; - - struct Animations - { - std::string defaultAnimation{}; - std::vector items{}; - - Animations(); - - Animations(tinyxml2::XMLElement*); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); - int length(); - int merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); - bool animations_deserialize(const std::string&, int, std::set&, std::string* = nullptr); - }; - - class Anm2 - { - public: - Info info{}; - Content content{}; - Animations animations{}; - - Anm2(); - bool serialize(const std::string&, std::string* = nullptr); - std::string to_string(); - Anm2(const std::string&, std::string* = nullptr); - uint64_t hash(); - Animation* animation_get(Reference); - Item* item_get(Reference); - Frame* frame_get(Reference); - bool spritesheet_add(const std::string&, const std::string&, int&); - Spritesheet* spritesheet_get(int); - void spritesheet_remove(int); - std::set spritesheets_unused(); - int layer_add(); - Reference layer_add(Reference = REFERENCE_DEFAULT, std::string = {}, int = 0, - types::locale::Type = types::locale::GLOBAL); - Reference null_add(Reference = REFERENCE_DEFAULT, std::string = {}, types::locale::Type = types::locale::GLOBAL); - void event_add(int&); - std::set events_unused(Reference = REFERENCE_DEFAULT); - std::set layers_unused(Reference = REFERENCE_DEFAULT); - std::set nulls_unused(Reference = REFERENCE_DEFAULT); - std::vector animation_names_get(); - std::vector spritesheet_names_get(); - std::vector event_names_get(); - void bake(Reference, int = 1, bool = true, bool = true); - void generate_from_grid(Reference, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); - }; -} diff --git a/src/anm2/animation.cpp b/src/anm2/animation.cpp new file mode 100644 index 0000000..a275dbc --- /dev/null +++ b/src/anm2/animation.cpp @@ -0,0 +1,206 @@ +#include "animation.h" + +#include "map_.h" +#include "math_.h" +#include "unordered_map_.h" +#include "xml_.h" +#include + +using namespace anm2ed::util; +using namespace glm; + +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Animation::Animation(XMLElement* element) + { + int id{}; + + xml::query_string_attribute(element, "Name", &name); + element->QueryIntAttribute("FrameNum", &frameNum); + element->QueryBoolAttribute("Loop", &isLoop); + + if (auto rootAnimationElement = element->FirstChildElement("RootAnimation")) + rootAnimation = Item(rootAnimationElement, ROOT); + + if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations")) + { + for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child; + child = child->NextSiblingElement("LayerAnimation")) + { + layerAnimations[id] = Item(child, LAYER, &id); + layerOrder.push_back(id); + } + } + + if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations")) + for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child; + child = child->NextSiblingElement("NullAnimation")) + nullAnimations[id] = Item(child, NULL_, &id); + + if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER); + } + + Item* Animation::item_get(Type type, int id) + { + switch (type) + { + case ROOT: + return &rootAnimation; + case LAYER: + return unordered_map::find(layerAnimations, id); + case NULL_: + return map::find(nullAnimations, id); + case TRIGGER: + return &triggers; + default: + return nullptr; + } + return nullptr; + } + + void Animation::item_remove(Type type, int id) + { + switch (type) + { + case LAYER: + layerAnimations.erase(id); + for (auto [i, value] : std::views::enumerate(layerOrder)) + if (value == id) layerOrder.erase(layerOrder.begin() + i); + break; + case NULL_: + nullAnimations.erase(id); + break; + case ROOT: + case TRIGGER: + default: + break; + } + } + + void Animation::serialize(XMLDocument& document, XMLElement* parent) + { + auto element = document.NewElement("Animation"); + element->SetAttribute("Name", name.c_str()); + element->SetAttribute("FrameNum", frameNum); + element->SetAttribute("Loop", isLoop); + + rootAnimation.serialize(document, element, ROOT); + + auto layerAnimationsElement = document.NewElement("LayerAnimations"); + for (auto& i : layerOrder) + { + Item& layerAnimation = layerAnimations.at(i); + layerAnimation.serialize(document, layerAnimationsElement, LAYER, i); + } + element->InsertEndChild(layerAnimationsElement); + + auto nullAnimationsElement = document.NewElement("NullAnimations"); + for (auto& [id, nullAnimation] : nullAnimations) + nullAnimation.serialize(document, nullAnimationsElement, NULL_, id); + element->InsertEndChild(nullAnimationsElement); + + triggers.serialize(document, element, TRIGGER); + + parent->InsertEndChild(element); + } + + int Animation::length() + { + int length{}; + + if (int rootAnimationLength = rootAnimation.length(ROOT); rootAnimationLength > length) + length = rootAnimationLength; + + for (auto& layerAnimation : layerAnimations | std::views::values) + if (int layerAnimationLength = layerAnimation.length(LAYER); layerAnimationLength > length) + length = layerAnimationLength; + + for (auto& nullAnimation : nullAnimations | std::views::values) + if (int nullAnimationLength = nullAnimation.length(NULL_); nullAnimationLength > length) + length = nullAnimationLength; + + if (int triggersLength = triggers.length(TRIGGER); triggersLength > length) length = triggersLength; + + return length; + } + + std::string Animation::to_string() + { + XMLDocument document{}; + + auto* element = document.NewElement("Animation"); + document.InsertFirstChild(element); + + element->SetAttribute("Name", name.c_str()); + element->SetAttribute("FrameNum", frameNum); + element->SetAttribute("Loop", isLoop); + + rootAnimation.serialize(document, element, ROOT); + + auto layerAnimationsElement = document.NewElement("LayerAnimations"); + for (auto& i : layerOrder) + { + Item& layerAnimation = layerAnimations.at(i); + layerAnimation.serialize(document, layerAnimationsElement, LAYER, i); + } + element->InsertEndChild(layerAnimationsElement); + + auto nullAnimationsElement = document.NewElement("NullAnimations"); + for (auto& [id, nullAnimation] : nullAnimations) + nullAnimation.serialize(document, nullAnimationsElement, NULL_, id); + element->InsertEndChild(nullAnimationsElement); + + triggers.serialize(document, element, TRIGGER); + + XMLPrinter printer; + document.Print(&printer); + return std::string(printer.CStr()); + } + + vec4 Animation::rect(bool isRootTransform) + { + float minX = std::numeric_limits::infinity(); + float minY = std::numeric_limits::infinity(); + float maxX = -std::numeric_limits::infinity(); + float maxY = -std::numeric_limits::infinity(); + bool any = false; + + constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; + + for (float t = 0.0f; t < (float)frameNum; t += 1.0f) + { + mat4 transform(1.0f); + + if (isRootTransform) + { + auto root = rootAnimation.frame_generate(t, anm2::ROOT); + transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); + } + + for (auto& [id, layerAnimation] : layerAnimations) + { + auto frame = layerAnimation.frame_generate(t, anm2::LAYER); + + if (frame.size == vec2() || !frame.isVisible) continue; + + auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot, + math::percent_to_unit(frame.scale), frame.rotation); + for (auto& corner : CORNERS) + { + vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f); + minX = std::min(minX, world.x); + minY = std::min(minY, world.y); + maxX = std::max(maxX, world.x); + maxY = std::max(maxY, world.y); + any = true; + } + } + } + + if (!any) return vec4(-1.0f); + return {minX, minY, maxX - minX, maxY - minY}; + } + +} \ No newline at end of file diff --git a/src/anm2/animation.h b/src/anm2/animation.h new file mode 100644 index 0000000..dc47fc7 --- /dev/null +++ b/src/anm2/animation.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "item.h" + +namespace anm2ed::anm2 +{ + constexpr auto FRAME_NUM_MIN = 1; + constexpr auto FRAME_NUM_MAX = FRAME_DELAY_MAX; + + class Animation + { + public: + std::string name{"New Animation"}; + int frameNum{FRAME_NUM_MIN}; + bool isLoop{true}; + Item rootAnimation; + std::unordered_map layerAnimations{}; + std::vector layerOrder{}; + std::map nullAnimations{}; + Item triggers; + + Animation() = default; + Animation(tinyxml2::XMLElement*); + Item* item_get(Type, int = -1); + void item_remove(Type, int = -1); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); + int length(); + glm::vec4 rect(bool); + std::string to_string(); + }; + +} \ No newline at end of file diff --git a/src/anm2/animations.cpp b/src/anm2/animations.cpp new file mode 100644 index 0000000..1ace1bb --- /dev/null +++ b/src/anm2/animations.cpp @@ -0,0 +1,155 @@ +#include "animations.h" + +#include + +#include "xml_.h" + +using namespace tinyxml2; +using namespace anm2ed::types; +using namespace anm2ed::util; + +namespace anm2ed::anm2 +{ + Animations::Animations(XMLElement* element) + { + xml::query_string_attribute(element, "DefaultAnimation", &defaultAnimation); + + for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation")) + items.push_back(Animation(child)); + } + + XMLElement* Animations::to_element(XMLDocument& document) + { + auto element = document.NewElement("Animations"); + element->SetAttribute("DefaultAnimation", defaultAnimation.c_str()); + for (auto& animation : items) + animation.serialize(document, element); + return element; + } + + void Animations::serialize(XMLDocument& document, XMLElement* parent) + { + parent->InsertEndChild(to_element(document)); + } + + int Animations::length() + { + int length{}; + + for (auto& animation : items) + if (int animationLength = animation.length(); animationLength > length) length = animationLength; + + return length; + } + + int Animations::merge(int target, std::set& sources, merge::Type type, bool isDeleteAfter) + { + Animation& animation = items.at(target); + + if (!animation.name.ends_with(MERGED_STRING)) animation.name = animation.name + " " + MERGED_STRING; + + auto merge_item = [&](Item& destination, Item& source) + { + switch (type) + { + case merge::APPEND: + destination.frames.insert(destination.frames.end(), source.frames.begin(), source.frames.end()); + break; + case merge::PREPEND: + destination.frames.insert(destination.frames.begin(), source.frames.begin(), source.frames.end()); + break; + case merge::REPLACE: + if (destination.frames.size() < source.frames.size()) destination.frames.resize(source.frames.size()); + for (int i = 0; i < (int)source.frames.size(); i++) + destination.frames[i] = source.frames[i]; + break; + case merge::IGNORE: + default: + break; + } + }; + + for (auto& i : sources) + { + if (i == target) continue; + if (i < 0 || i >= (int)items.size()) continue; + + auto& source = items.at(i); + + merge_item(animation.rootAnimation, source.rootAnimation); + + for (auto& [id, layerAnimation] : source.layerAnimations) + { + if (!animation.layerAnimations.contains(id)) + { + animation.layerAnimations[id] = layerAnimation; + animation.layerOrder.emplace_back(id); + } + merge_item(animation.layerAnimations[id], layerAnimation); + } + + for (auto& [id, nullAnimation] : source.nullAnimations) + { + if (!animation.nullAnimations.contains(id)) animation.nullAnimations[id] = nullAnimation; + merge_item(animation.nullAnimations[id], nullAnimation); + } + + merge_item(animation.triggers, source.triggers); + } + + if (isDeleteAfter) + { + for (auto& source : std::ranges::reverse_view(sources)) + { + if (source == target) continue; + items.erase(items.begin() + source); + } + } + + int finalIndex = target; + + if (isDeleteAfter) + { + int numDeletedBefore = 0; + for (auto& idx : sources) + { + if (idx == target) continue; + if (idx >= 0 && idx < target) ++numDeletedBefore; + } + finalIndex -= numDeletedBefore; + } + + return finalIndex; + } + + bool Animations::animations_deserialize(const std::string& string, int start, std::set& indices, + std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + if (!document.FirstChildElement("Animation")) + { + if (errorString) *errorString = "No valid animation(s)."; + return false; + } + + int count{}; + for (auto element = document.FirstChildElement("Animation"); element; + element = element->NextSiblingElement("Animation")) + { + auto index = start + count; + items.insert(items.begin() + start + count, Animation(element)); + indices.insert(index); + count++; + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } +} \ No newline at end of file diff --git a/src/anm2/animations.h b/src/anm2/animations.h new file mode 100644 index 0000000..ce377b2 --- /dev/null +++ b/src/anm2/animations.h @@ -0,0 +1,22 @@ +#pragma once + +#include "animation.h" + +namespace anm2ed::anm2 +{ + constexpr auto MERGED_STRING = "(Merged)"; + + struct Animations + { + std::string defaultAnimation{}; + std::vector items{}; + + Animations() = default; + Animations(tinyxml2::XMLElement*); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&); + int length(); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); + int merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); + bool animations_deserialize(const std::string&, int, std::set&, std::string* = nullptr); + }; +} \ No newline at end of file diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp new file mode 100644 index 0000000..0b53b9c --- /dev/null +++ b/src/anm2/anm2.cpp @@ -0,0 +1,376 @@ +#include "anm2.h" + +#include + +#include "filesystem_.h" +#include "map_.h" +#include "time_.h" +#include "unordered_map_.h" +#include "vector_.h" + +using namespace tinyxml2; +using namespace anm2ed::types; +using namespace anm2ed::util; +using namespace glm; + +namespace anm2ed::anm2 +{ + Anm2::Anm2() + { + info.createdOn = time::get("%d-%B-%Y %I:%M:%S"); + } + + bool Anm2::serialize(const std::string& path, std::string* errorString) + { + XMLDocument document; + + auto* element = document.NewElement("AnimatedActor"); + document.InsertFirstChild(element); + + info.serialize(document, element); + content.serialize(document, element); + animations.serialize(document, element); + + if (document.SaveFile(path.c_str()) != XML_SUCCESS) + { + if (errorString) *errorString = document.ErrorStr(); + return false; + } + return true; + } + + std::string Anm2::to_string() + { + XMLDocument document; + + auto* element = document.NewElement("AnimatedActor"); + document.InsertFirstChild(element); + + info.serialize(document, element); + content.serialize(document, element); + animations.serialize(document, element); + + XMLPrinter printer; + document.Print(&printer); + return std::string(printer.CStr()); + } + + Anm2::Anm2(const std::string& path, std::string* errorString) + { + XMLDocument document; + + if (document.LoadFile(path.c_str()) != XML_SUCCESS) + { + if (errorString) *errorString = document.ErrorStr(); + return; + } + + filesystem::WorkingDirectory workingDirectory(path, true); + + const XMLElement* element = document.RootElement(); + + if (auto infoElement = element->FirstChildElement("Info")) info = Info((XMLElement*)infoElement); + if (auto contentElement = element->FirstChildElement("Content")) content = Content((XMLElement*)contentElement); + if (auto animationsElement = element->FirstChildElement("Animations")) + animations = Animations((XMLElement*)animationsElement); + } + + uint64_t Anm2::hash() + { + return std::hash{}(to_string()); + } + + Animation* Anm2::animation_get(Reference reference) + { + return vector::find(animations.items, reference.animationIndex); + } + + std::vector Anm2::animation_names_get() + { + std::vector names{}; + for (auto& animation : animations.items) + names.push_back(animation.name); + return names; + } + + Item* Anm2::item_get(Reference reference) + { + if (Animation* animation = animation_get(reference)) + { + switch (reference.itemType) + { + case ROOT: + return &animation->rootAnimation; + case LAYER: + return unordered_map::find(animation->layerAnimations, reference.itemID); + case NULL_: + return map::find(animation->nullAnimations, reference.itemID); + case TRIGGER: + return &animation->triggers; + default: + return nullptr; + } + } + return nullptr; + } + + Frame* Anm2::frame_get(Reference reference) + { + Item* item = item_get(reference); + if (!item) return nullptr; + return vector::find(item->frames, reference.frameIndex); + return nullptr; + } + + bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id) + { + Spritesheet spritesheet(directory, path); + if (!spritesheet.is_valid()) return false; + id = map::next_id_get(content.spritesheets); + content.spritesheets[id] = std::move(spritesheet); + return true; + } + + void Anm2::spritesheet_remove(int id) + { + content.spritesheets.erase(id); + } + + Spritesheet* Anm2::spritesheet_get(int id) + { + return map::find(content.spritesheets, id); + } + + std::set Anm2::spritesheets_unused() + { + return content.spritesheets_unused(); + } + + std::vector Anm2::spritesheet_names_get() + { + std::vector names{}; + for (auto& [id, spritesheet] : content.spritesheets) + names.push_back(std::format(SPRITESHEET_FORMAT, id, spritesheet.path.c_str())); + return names; + } + + Reference Anm2::layer_add(Reference reference, std::string name, int spritesheetID, locale::Type locale) + { + auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID; + auto& layer = content.layers[id]; + + layer.name = !name.empty() ? name : layer.name; + layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0; + + auto add = [&](Animation* animation, int id) + { + animation->layerAnimations[id] = Item(); + animation->layerOrder.push_back(id); + }; + + if (locale == locale::GLOBAL) + { + for (auto& animation : animations.items) + if (!animation.layerAnimations.contains(id)) add(&animation, id); + } + else if (locale == locale::LOCAL) + { + if (auto animation = animation_get(reference)) + if (!animation->layerAnimations.contains(id)) add(animation, id); + } + + return {reference.animationIndex, LAYER, id}; + } + + Reference Anm2::null_add(Reference reference, std::string name, locale::Type locale) + { + auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID; + auto& null = content.nulls[id]; + + null.name = !name.empty() ? name : null.name; + + auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); }; + + if (locale == locale::GLOBAL) + { + for (auto& animation : animations.items) + if (!animation.nullAnimations.contains(id)) add(&animation, id); + } + else if (locale == locale::LOCAL) + { + if (auto animation = animation_get(reference)) + if (!animation->nullAnimations.contains(id)) add(animation, id); + } + + return {reference.animationIndex, LAYER, id}; + } + + void Anm2::event_add(int& id) + { + content.event_add(id); + } + + std::set Anm2::events_unused(Reference reference) + { + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& frame : animation->triggers.frames) + used.insert(frame.eventID); + else + for (auto& animation : animations.items) + for (auto& frame : animation.triggers.frames) + used.insert(frame.eventID); + + for (auto& id : content.events | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + std::set Anm2::layers_unused(Reference reference) + { + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& id : animation->layerAnimations | std::views::keys) + used.insert(id); + else + for (auto& animation : animations.items) + for (auto& id : animation.layerAnimations | std::views::keys) + used.insert(id); + + for (auto& id : content.layers | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + std::set Anm2::nulls_unused(Reference reference) + { + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& id : animation->nullAnimations | std::views::keys) + used.insert(id); + else + for (auto& animation : animations.items) + for (auto& id : animation.nullAnimations | std::views::keys) + used.insert(id); + + for (auto& id : content.nulls | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + std::vector Anm2::event_names_get() + { + std::vector names{}; + for (auto& event : content.events | std::views::values) + names.push_back(event.name); + return names; + } + + bool Anm2::sound_add(const std::string& directory, const std::string& path, int& id) + { + id = map::next_id_get(content.sounds); + content.sounds[id] = Sound(directory, path); + return true; + } + + std::set Anm2::sounds_unused() + { + std::set used; + for (auto& event : content.events | std::views::values) + used.insert(event.soundID); + + std::set unused; + for (auto& id : content.sounds | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + std::vector Anm2::sound_names_get() + { + std::vector names{}; + for (auto& [id, sound] : content.sounds) + names.push_back(std::format(SOUND_FORMAT, id, sound.path.c_str())); + return names; + } + + void Anm2::bake(Reference reference, int interval, bool isRoundScale, bool isRoundRotation) + { + Item* item = item_get(reference); + if (!item) return; + + Frame* frame = frame_get(reference); + if (!frame) return; + + if (frame->delay == FRAME_DELAY_MIN) return; + + Reference referenceNext = reference; + referenceNext.frameIndex = reference.frameIndex + 1; + + Frame* frameNext = frame_get(referenceNext); + if (!frameNext) frameNext = frame; + + Frame baseFrame = *frame; + Frame baseFrameNext = *frameNext; + + int delay{}; + int index = reference.frameIndex; + + while (delay < baseFrame.delay) + { + float interpolation = (float)delay / baseFrame.delay; + + Frame baked = baseFrame; + baked.delay = std::min(interval, baseFrame.delay - delay); + baked.isInterpolated = (index == reference.frameIndex) ? baseFrame.isInterpolated : false; + + baked.rotation = glm::mix(baseFrame.rotation, baseFrameNext.rotation, interpolation); + baked.position = glm::mix(baseFrame.position, baseFrameNext.position, interpolation); + baked.scale = glm::mix(baseFrame.scale, baseFrameNext.scale, interpolation); + baked.colorOffset = glm::mix(baseFrame.colorOffset, baseFrameNext.colorOffset, interpolation); + baked.tint = glm::mix(baseFrame.tint, baseFrameNext.tint, interpolation); + + if (isRoundScale) baked.scale = vec2(ivec2(baked.scale)); + if (isRoundRotation) baked.rotation = (int)baked.rotation; + + if (index == reference.frameIndex) + item->frames[index] = baked; + else + item->frames.insert(item->frames.begin() + index, baked); + index++; + + delay += baked.delay; + } + } + + void Anm2::generate_from_grid(Reference reference, ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, + int count, int delay) + { + auto item = item_get(reference); + if (!item) return; + + for (int i = 0; i < count; i++) + { + auto row = i / columns; + auto column = i % columns; + + Frame frame{}; + + frame.delay = delay; + frame.pivot = pivot; + frame.size = size; + frame.crop = startPosition + ivec2(size.x * column, size.y * row); + + item->frames.emplace_back(frame); + } + } +} diff --git a/src/anm2/anm2.h b/src/anm2/anm2.h new file mode 100644 index 0000000..9ce3207 --- /dev/null +++ b/src/anm2/anm2.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "types.h" + +#include "animations.h" +#include "content.h" +#include "info.h" + +namespace anm2ed::anm2 +{ + constexpr auto NO_PATH = "[No Path]"; + + struct Reference + { + int animationIndex{-1}; + Type itemType{NONE}; + int itemID{-1}; + int frameIndex{-1}; + int frameTime{-1}; + + auto operator<=>(const Reference&) const = default; + }; + + constexpr anm2::Reference REFERENCE_DEFAULT = {-1, anm2::NONE, -1, -1, -1}; + + class Anm2 + { + public: + Info info{}; + Content content{}; + Animations animations{}; + + Anm2(); + bool serialize(const std::string&, std::string* = nullptr); + std::string to_string(); + Anm2(const std::string&, std::string* = nullptr); + uint64_t hash(); + Animation* animation_get(Reference); + std::vector animation_names_get(); + + Item* item_get(Reference); + + Frame* frame_get(Reference); + + bool spritesheet_add(const std::string&, const std::string&, int&); + Spritesheet* spritesheet_get(int); + void spritesheet_remove(int); + std::set spritesheets_unused(); + std::vector spritesheet_names_get(); + + int layer_add(); + Reference layer_add(Reference = REFERENCE_DEFAULT, std::string = {}, int = 0, + types::locale::Type = types::locale::GLOBAL); + std::set layers_unused(Reference = REFERENCE_DEFAULT); + + Reference null_add(Reference = REFERENCE_DEFAULT, std::string = {}, types::locale::Type = types::locale::GLOBAL); + std::set nulls_unused(Reference = REFERENCE_DEFAULT); + + bool sound_add(const std::string& directory, const std::string& path, int& id); + std::vector sound_names_get(); + std::set sounds_unused(); + + void event_add(int&); + std::set events_unused(Reference = REFERENCE_DEFAULT); + std::vector event_names_get(); + void bake(Reference, int = 1, bool = true, bool = true); + void generate_from_grid(Reference, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); + }; +} diff --git a/src/anm2/content.cpp b/src/anm2/content.cpp new file mode 100644 index 0000000..d2ca4f1 --- /dev/null +++ b/src/anm2/content.cpp @@ -0,0 +1,256 @@ +#include "content.h" + +#include + +#include "filesystem_.h" +#include "map_.h" + +using namespace anm2ed::types; +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Content::Content(XMLElement* element) + { + int id{}; + + if (auto spritesheetsElement = element->FirstChildElement("Spritesheets")) + for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child; + child = child->NextSiblingElement("Spritesheet")) + spritesheets[id] = Spritesheet(child, id); + + if (auto layersElement = element->FirstChildElement("Layers")) + for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer")) + layers[id] = Layer(child, id); + + if (auto nullsElement = element->FirstChildElement("Nulls")) + for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null")) + nulls[id] = Null(child, id); + + if (auto eventsElement = element->FirstChildElement("Events")) + for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event")) + events[id] = Event(child, id); + } + + void Content::serialize(XMLDocument& document, XMLElement* parent) + { + auto element = document.NewElement("Content"); + + auto spritesheetsElement = document.NewElement("Spritesheets"); + for (auto& [id, spritesheet] : spritesheets) + spritesheet.serialize(document, spritesheetsElement, id); + element->InsertEndChild(spritesheetsElement); + + auto layersElement = document.NewElement("Layers"); + for (auto& [id, layer] : layers) + layer.serialize(document, layersElement, id); + element->InsertEndChild(layersElement); + + auto nullsElement = document.NewElement("Nulls"); + for (auto& [id, null] : nulls) + null.serialize(document, nullsElement, id); + element->InsertEndChild(nullsElement); + + auto eventsElement = document.NewElement("Events"); + for (auto& [id, event] : events) + event.serialize(document, eventsElement, id); + element->InsertEndChild(eventsElement); + + parent->InsertEndChild(element); + } + + std::set Content::spritesheets_unused() + { + std::set used; + for (auto& layer : layers | std::views::values) + if (layer.spritesheetID != -1) used.insert(layer.spritesheetID); + + std::set unused; + for (auto& id : spritesheets | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + void Content::layer_add(int& id) + { + id = map::next_id_get(layers); + layers[id] = Layer(); + } + + void Content::null_add(int& id) + { + id = map::next_id_get(nulls); + nulls[id] = Null(); + } + + void Content::event_add(int& id) + { + id = map::next_id_get(events); + events[id] = Event(); + } + + bool Content::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type, + std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + int id{}; + + if (!document.FirstChildElement("Spritesheet")) + { + if (errorString) *errorString = "No valid spritesheet(s)."; + return false; + } + + filesystem::WorkingDirectory workingDirectory(directory); + + for (auto element = document.FirstChildElement("Spritesheet"); element; + element = element->NextSiblingElement("Spritesheet")) + { + auto spritesheet = Spritesheet(element, id); + + if (type == merge::APPEND) id = map::next_id_get(spritesheets); + + spritesheets[id] = std::move(spritesheet); + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } + + bool Content::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + int id{}; + + if (!document.FirstChildElement("Layer")) + { + if (errorString) *errorString = "No valid layer(s)."; + return false; + } + + for (auto element = document.FirstChildElement("Layer"); element; element = element->NextSiblingElement("Layer")) + { + auto layer = Layer(element, id); + + if (type == merge::APPEND) id = map::next_id_get(layers); + + layers[id] = layer; + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } + + bool Content::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + int id{}; + + if (!document.FirstChildElement("Null")) + { + if (errorString) *errorString = "No valid null(s)."; + return false; + } + + for (auto element = document.FirstChildElement("Null"); element; element = element->NextSiblingElement("Null")) + { + auto layer = Null(element, id); + + if (type == merge::APPEND) id = map::next_id_get(nulls); + + nulls[id] = layer; + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } + + bool Content::events_deserialize(const std::string& string, merge::Type type, std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + int id{}; + + if (!document.FirstChildElement("Event")) + { + if (errorString) *errorString = "No valid event(s)."; + return false; + } + + for (auto element = document.FirstChildElement("Event"); element; element = element->NextSiblingElement("Event")) + { + auto layer = Event(element, id); + + if (type == merge::APPEND) id = map::next_id_get(events); + + events[id] = layer; + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } + + bool Content::sounds_deserialize(const std::string& string, const std::string& directory, merge::Type type, + std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + int id{}; + + if (!document.FirstChildElement("Sound")) + { + if (errorString) *errorString = "No valid sound(s)."; + return false; + } + + filesystem::WorkingDirectory workingDirectory(directory); + + for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound")) + { + auto sound = Sound(element, id); + + if (type == merge::APPEND) id = map::next_id_get(sounds); + + sounds[id] = std::move(sound); + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } + +} \ No newline at end of file diff --git a/src/anm2/content.h b/src/anm2/content.h new file mode 100644 index 0000000..e97650b --- /dev/null +++ b/src/anm2/content.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#include "event.h" +#include "layer.h" +#include "null.h" +#include "sound.h" +#include "spritesheet.h" + +#include "types.h" + +namespace anm2ed::anm2 +{ + struct Content + { + std::map spritesheets{}; + std::map layers{}; + std::map nulls{}; + std::map events{}; + std::map sounds{}; + + Content() = default; + + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); + Content(tinyxml2::XMLElement*); + + bool spritesheet_add(const std::string&, const std::string&, int&); + std::set spritesheets_unused(); + void spritesheet_remove(int&); + bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr); + + void layer_add(int&); + bool layers_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); + + void null_add(int&); + bool nulls_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); + + void event_add(int&); + bool events_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); + + void sound_add(int&); + bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr); + }; +} \ No newline at end of file diff --git a/src/anm2/event.cpp b/src/anm2/event.cpp new file mode 100644 index 0000000..7427897 --- /dev/null +++ b/src/anm2/event.cpp @@ -0,0 +1,36 @@ +#include "event.h" + +#include "xml_.h" + +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Event::Event(XMLElement* element, int& id) + { + if (!element) return; + element->QueryIntAttribute("Id", &id); + xml::query_string_attribute(element, "Name", &name); + } + + XMLElement* Event::to_element(XMLDocument& document, int id) + { + auto element = document.NewElement("Event"); + element->SetAttribute("Id", id); + element->SetAttribute("Name", name.c_str()); + return element; + } + + void Event::serialize(XMLDocument& document, XMLElement* parent, int id) + { + parent->InsertEndChild(to_element(document, id)); + } + + std::string Event::to_string(int id) + { + XMLDocument document{}; + document.InsertEndChild(to_element(document, id)); + return xml::document_to_string(document); + } +} \ No newline at end of file diff --git a/src/anm2/event.h b/src/anm2/event.h new file mode 100644 index 0000000..3f23d86 --- /dev/null +++ b/src/anm2/event.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace anm2ed::anm2 +{ + class Event + { + public: + std::string name{"New Event"}; + int soundID{}; + + Event() = default; + Event(tinyxml2::XMLElement*, int&); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); + std::string to_string(int); + }; +} \ No newline at end of file diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp new file mode 100644 index 0000000..0ff462b --- /dev/null +++ b/src/anm2/frame.cpp @@ -0,0 +1,126 @@ +#include "frame.h" + +#include "math_.h" +#include "xml_.h" + +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Frame::Frame(XMLElement* element, Type type) + { + if (type != TRIGGER) + { + element->QueryFloatAttribute("XPosition", &position.x); + element->QueryFloatAttribute("YPosition", &position.y); + if (type == LAYER) + { + element->QueryFloatAttribute("XPivot", &pivot.x); + element->QueryFloatAttribute("YPivot", &pivot.y); + element->QueryFloatAttribute("XCrop", &crop.x); + element->QueryFloatAttribute("YCrop", &crop.y); + element->QueryFloatAttribute("Width", &size.x); + element->QueryFloatAttribute("Height", &size.y); + } + element->QueryFloatAttribute("XScale", &scale.x); + element->QueryFloatAttribute("YScale", &scale.y); + element->QueryIntAttribute("Delay", &delay); + element->QueryBoolAttribute("Visible", &isVisible); + xml::query_color_attribute(element, "RedTint", tint.r); + xml::query_color_attribute(element, "GreenTint", tint.g); + xml::query_color_attribute(element, "BlueTint", tint.b); + xml::query_color_attribute(element, "AlphaTint", tint.a); + xml::query_color_attribute(element, "RedOffset", colorOffset.r); + xml::query_color_attribute(element, "GreenOffset", colorOffset.g); + xml::query_color_attribute(element, "BlueOffset", colorOffset.b); + element->QueryFloatAttribute("Rotation", &rotation); + element->QueryBoolAttribute("Interpolated", &isInterpolated); + } + else + { + element->QueryIntAttribute("EventId", &eventID); + element->QueryIntAttribute("AtFrame", &atFrame); + } + } + + XMLElement* Frame::to_element(XMLDocument& document, Type type) + { + auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame"); + + switch (type) + { + case ROOT: + case NULL_: + element->SetAttribute("XPosition", position.x); + element->SetAttribute("YPosition", position.y); + element->SetAttribute("Delay", delay); + element->SetAttribute("Visible", isVisible); + element->SetAttribute("XScale", scale.x); + element->SetAttribute("YScale", scale.y); + element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); + element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); + element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); + element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); + element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); + element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); + element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); + element->SetAttribute("Rotation", rotation); + element->SetAttribute("Interpolated", isInterpolated); + break; + case LAYER: + element->SetAttribute("XPosition", position.x); + element->SetAttribute("YPosition", position.y); + element->SetAttribute("XPivot", pivot.x); + element->SetAttribute("YPivot", pivot.y); + element->SetAttribute("XCrop", crop.x); + element->SetAttribute("YCrop", crop.y); + element->SetAttribute("Width", size.x); + element->SetAttribute("Height", size.y); + element->SetAttribute("XScale", scale.x); + element->SetAttribute("YScale", scale.y); + element->SetAttribute("Delay", delay); + element->SetAttribute("Visible", isVisible); + element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); + element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); + element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); + element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); + element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); + element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); + element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); + element->SetAttribute("Rotation", rotation); + element->SetAttribute("Interpolated", isInterpolated); + break; + case TRIGGER: + element->SetAttribute("EventId", eventID); + element->SetAttribute("AtFrame", atFrame); + break; + default: + break; + } + + return element; + } + + void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type) + { + parent->InsertEndChild(to_element(document, type)); + } + + std::string Frame::to_string(Type type) + { + XMLDocument document{}; + document.InsertEndChild(to_element(document, type)); + return xml::document_to_string(document); + } + + void Frame::shorten() + { + delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); + } + + void Frame::extend() + { + delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); + } +} \ No newline at end of file diff --git a/src/anm2/frame.h b/src/anm2/frame.h new file mode 100644 index 0000000..b5a3f76 --- /dev/null +++ b/src/anm2/frame.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "types.h" + +namespace anm2ed::anm2 +{ + constexpr auto FRAME_DELAY_MIN = 1; + constexpr auto FRAME_DELAY_MAX = 100000; + +#define TYPE_LIST \ + X(NONE, "None", "None") \ + X(ROOT, "Root", "RootAnimation") \ + X(LAYER, "Layer", "LayerAnimation") \ + X(NULL_, "Null", "NullAnimation") \ + X(TRIGGER, "Trigger", "Triggers") + + enum Type + { +#define X(symbol, string, animationString) symbol, + TYPE_LIST +#undef X + }; + + constexpr const char* TYPE_STRINGS[] = { +#define X(symbol, string, animationString) string, + TYPE_LIST +#undef X + }; + + constexpr const char* TYPE_ANIMATION_STRINGS[] = { +#define X(symbol, string, animationString) animationString, + TYPE_LIST +#undef X + }; + + enum ChangeType + { + ADD, + SUBTRACT, + ADJUST + }; + +#define MEMBERS \ + X(isVisible, bool, true) \ + X(isInterpolated, bool, false) \ + X(rotation, float, 0.0f) \ + X(delay, int, FRAME_DELAY_MIN) \ + X(atFrame, int, -1) \ + X(eventID, int, -1) \ + X(pivot, glm::vec2, {}) \ + X(crop, glm::vec2, {}) \ + X(position, glm::vec2, {}) \ + X(size, glm::vec2, {}) \ + X(scale, glm::vec2, glm::vec2(100.0f)) \ + X(colorOffset, glm::vec3, types::color::TRANSPARENT) \ + X(tint, glm::vec4, types::color::WHITE) + + class Frame + { + public: +#define X(name, type, ...) type name = __VA_ARGS__; + MEMBERS +#undef X + + Frame() = default; + Frame(tinyxml2::XMLElement*, Type); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type); + std::string to_string(Type type); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type); + void shorten(); + void extend(); + }; + + struct FrameChange + { +#define X(name, type, ...) std::optional name{}; + MEMBERS +#undef X + }; + +#undef MEMBERS + +} \ No newline at end of file diff --git a/src/anm2/info.cpp b/src/anm2/info.cpp new file mode 100644 index 0000000..2ea0433 --- /dev/null +++ b/src/anm2/info.cpp @@ -0,0 +1,40 @@ +#include "info.h" + +#include "xml_.h" + +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Info::Info(XMLElement* element) + { + if (!element) return; + xml::query_string_attribute(element, "CreatedBy", &createdBy); + xml::query_string_attribute(element, "CreatedOn", &createdOn); + element->QueryIntAttribute("Fps", &fps); + element->QueryIntAttribute("Version", &version); + } + + XMLElement* Info::to_element(XMLDocument& document) + { + auto element = document.NewElement("Info"); + element->SetAttribute("CreatedBy", createdBy.c_str()); + element->SetAttribute("CreatedOn", createdOn.c_str()); + element->SetAttribute("Fps", fps); + element->SetAttribute("Version", version); + return element; + } + + void Info::serialize(XMLDocument& document, XMLElement* parent) + { + parent->InsertEndChild(to_element(document)); + } + + std::string Info::to_string() + { + XMLDocument document{}; + document.InsertEndChild(to_element(document)); + return xml::document_to_string(document); + } +} \ No newline at end of file diff --git a/src/anm2/info.h b/src/anm2/info.h new file mode 100644 index 0000000..c2fd400 --- /dev/null +++ b/src/anm2/info.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace anm2ed::anm2 +{ + constexpr auto FPS_MIN = 1; + constexpr auto FPS_MAX = 120; + + class Info + { + public: + std::string createdBy{"robot"}; + std::string createdOn{}; + int fps = 30; + int version{}; + + Info() = default; + Info(tinyxml2::XMLElement*); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument& document); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); + std::string to_string(); + }; +} \ No newline at end of file diff --git a/src/anm2/item.cpp b/src/anm2/item.cpp new file mode 100644 index 0000000..0b20ca5 --- /dev/null +++ b/src/anm2/item.cpp @@ -0,0 +1,200 @@ +#include "item.h" +#include + +#include "xml_.h" + +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Item::Item(XMLElement* element, Type type, int* id) + { + if (type == LAYER && id) element->QueryIntAttribute("LayerId", id); + if (type == NULL_ && id) element->QueryIntAttribute("NullId", id); + + element->QueryBoolAttribute("Visible", &isVisible); + + for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame"); + child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame")) + frames.push_back(Frame(child, type)); + } + + XMLElement* Item::to_element(XMLDocument& document, Type type, int id) + { + auto element = document.NewElement(TYPE_ANIMATION_STRINGS[type]); + + if (type == LAYER) element->SetAttribute("LayerId", id); + if (type == NULL_) element->SetAttribute("NullId", id); + if (type == LAYER || type == NULL_) element->SetAttribute("Visible", isVisible); + + for (auto& frame : frames) + frame.serialize(document, element, type); + + return element; + } + + void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id) + { + parent->InsertEndChild(to_element(document, type, id)); + } + + std::string Item::to_string(Type type, int id) + { + XMLDocument document{}; + document.InsertEndChild(to_element(document, type, id)); + return xml::document_to_string(document); + } + + int Item::length(Type type) + { + int length{}; + + if (type == TRIGGER) + for (auto& frame : frames) + length = frame.atFrame > length ? frame.atFrame : length; + else + for (auto& frame : frames) + length += frame.delay; + + return length; + } + + Frame Item::frame_generate(float time, Type type) + { + Frame frame{}; + frame.isVisible = false; + + if (frames.empty()) return frame; + + Frame* frameNext = nullptr; + int delayCurrent = 0; + int delayNext = 0; + + for (auto [i, iFrame] : std::views::enumerate(frames)) + { + if (type == TRIGGER) + { + if ((int)time == iFrame.atFrame) + { + frame = iFrame; + break; + } + } + else + { + frame = iFrame; + + delayNext += frame.delay; + + if (time >= delayCurrent && time < delayNext) + { + if (i + 1 < (int)frames.size()) + frameNext = &frames[i + 1]; + else + frameNext = nullptr; + break; + } + + delayCurrent += frame.delay; + } + } + + if (type != TRIGGER && frame.isInterpolated && frameNext && frame.delay > 1) + { + auto interpolation = (time - delayCurrent) / (delayNext - delayCurrent); + + frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation); + frame.position = glm::mix(frame.position, frameNext->position, interpolation); + frame.scale = glm::mix(frame.scale, frameNext->scale, interpolation); + frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, interpolation); + frame.tint = glm::mix(frame.tint, frameNext->tint, interpolation); + } + + return frame; + } + + void Item::frames_change(anm2::FrameChange& change, ChangeType type, int start, int numberFrames) + { + auto useStart = numberFrames > -1 ? start : 0; + auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size(); + end = glm::clamp(end, start, (int)frames.size()); + + for (int i = useStart; i < end; i++) + { + Frame& frame = frames[i]; + + if (change.isVisible) frame.isVisible = *change.isVisible; + if (change.isInterpolated) frame.isInterpolated = *change.isInterpolated; + + switch (type) + { + case ADJUST: + if (change.rotation) frame.rotation = *change.rotation; + if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, *change.delay); + if (change.crop) frame.crop = *change.crop; + if (change.pivot) frame.pivot = *change.pivot; + if (change.position) frame.position = *change.position; + if (change.size) frame.size = *change.size; + if (change.scale) frame.scale = *change.scale; + if (change.colorOffset) frame.colorOffset = glm::clamp(*change.colorOffset, 0.0f, 1.0f); + if (change.tint) frame.tint = glm::clamp(*change.tint, 0.0f, 1.0f); + break; + + case ADD: + if (change.rotation) frame.rotation += *change.rotation; + if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay + *change.delay); + if (change.crop) frame.crop += *change.crop; + if (change.pivot) frame.pivot += *change.pivot; + if (change.position) frame.position += *change.position; + if (change.size) frame.size += *change.size; + if (change.scale) frame.scale += *change.scale; + if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset + *change.colorOffset, 0.0f, 1.0f); + if (change.tint) frame.tint = glm::clamp(frame.tint + *change.tint, 0.0f, 1.0f); + break; + + case SUBTRACT: + if (change.rotation) frame.rotation -= *change.rotation; + if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay - *change.delay); + if (change.crop) frame.crop -= *change.crop; + if (change.pivot) frame.pivot -= *change.pivot; + if (change.position) frame.position -= *change.position; + if (change.size) frame.size -= *change.size; + if (change.scale) frame.scale -= *change.scale; + if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset - *change.colorOffset, 0.0f, 1.0f); + if (change.tint) frame.tint = glm::clamp(frame.tint - *change.tint, 0.0f, 1.0f); + break; + } + } + } + + bool Item::frames_deserialize(const std::string& string, Type type, int start, std::set& indices, + std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + if (!document.FirstChildElement("Frame")) + { + if (errorString) *errorString = "No valid frame(s)."; + return false; + } + + int count{}; + for (auto element = document.FirstChildElement("Frame"); element; element = element->NextSiblingElement("Frame")) + { + auto index = start + count; + frames.insert(frames.begin() + start + count, Frame(element, type)); + indices.insert(index); + count++; + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } +} \ No newline at end of file diff --git a/src/anm2/item.h b/src/anm2/item.h new file mode 100644 index 0000000..cb3b20c --- /dev/null +++ b/src/anm2/item.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include "frame.h" + +namespace anm2ed::anm2 +{ + class Item + { + public: + std::vector frames{}; + bool isVisible{true}; + + Item() = default; + Item(tinyxml2::XMLElement*, Type, int* = nullptr); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, int); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1); + std::string to_string(Type, int = -1); + int length(Type); + Frame frame_generate(float, Type); + void frames_change(anm2::FrameChange&, ChangeType, int, int = 0); + bool frames_deserialize(const std::string&, Type, int, std::set&, std::string*); + }; +} \ No newline at end of file diff --git a/src/anm2/layer.cpp b/src/anm2/layer.cpp new file mode 100644 index 0000000..4ca3106 --- /dev/null +++ b/src/anm2/layer.cpp @@ -0,0 +1,39 @@ +#include "layer.h" + +#include "xml_.h" + +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Layer::Layer(XMLElement* element, int& id) + { + if (!element) return; + element->QueryIntAttribute("Id", &id); + xml::query_string_attribute(element, "Name", &name); + element->QueryIntAttribute("SpritesheetId", &spritesheetID); + } + + XMLElement* Layer::to_element(XMLDocument& document, int id) + { + auto element = document.NewElement("Layer"); + element->SetAttribute("Id", id); + element->SetAttribute("Name", name.c_str()); + element->SetAttribute("SpritesheetId", spritesheetID); + return element; + } + + void Layer::serialize(XMLDocument& document, XMLElement* parent, int id) + { + parent->InsertEndChild(to_element(document, id)); + } + + std::string Layer::to_string(int id) + { + XMLDocument document{}; + document.InsertEndChild(to_element(document, id)); + return xml::document_to_string(document); + } + +} \ No newline at end of file diff --git a/src/anm2/layer.h b/src/anm2/layer.h new file mode 100644 index 0000000..715fe1b --- /dev/null +++ b/src/anm2/layer.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace anm2ed::anm2 +{ + constexpr auto LAYER_FORMAT = "#{} {} (Spritesheet: #{})"; + + class Layer + { + public: + std::string name{"New Layer"}; + int spritesheetID{}; + + Layer() = default; + Layer(tinyxml2::XMLElement*, int&); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); + std::string to_string(int); + }; +} \ No newline at end of file diff --git a/src/anm2/null.cpp b/src/anm2/null.cpp new file mode 100644 index 0000000..272228a --- /dev/null +++ b/src/anm2/null.cpp @@ -0,0 +1,38 @@ +#include "null.h" + +#include "xml_.h" + +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Null::Null(XMLElement* element, int& id) + { + if (!element) return; + element->QueryIntAttribute("Id", &id); + xml::query_string_attribute(element, "Name", &name); + element->QueryBoolAttribute("ShowRect", &isShowRect); + } + + XMLElement* Null::to_element(XMLDocument& document, int id) + { + auto element = document.NewElement("Null"); + element->SetAttribute("Id", id); + element->SetAttribute("Name", name.c_str()); + if (isShowRect) element->SetAttribute("ShowRect", isShowRect); + return element; + } + + void Null::serialize(XMLDocument& document, XMLElement* parent, int id) + { + parent->InsertEndChild(to_element(document, id)); + } + + std::string Null::to_string(int id) + { + XMLDocument document{}; + document.InsertEndChild(to_element(document, id)); + return xml::document_to_string(document); + } +} \ No newline at end of file diff --git a/src/anm2/null.h b/src/anm2/null.h new file mode 100644 index 0000000..1691be9 --- /dev/null +++ b/src/anm2/null.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace anm2ed::anm2 +{ + constexpr auto NULL_FORMAT = "#{} {}"; + + class Null + { + public: + std::string name{"New Null"}; + bool isShowRect{}; + + Null() = default; + Null(tinyxml2::XMLElement*, int&); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument& document, int id); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); + std::string to_string(int); + }; +} \ No newline at end of file diff --git a/src/anm2/sound.cpp b/src/anm2/sound.cpp new file mode 100644 index 0000000..eca9655 --- /dev/null +++ b/src/anm2/sound.cpp @@ -0,0 +1,59 @@ +#include "sound.h" + +#include "filesystem_.h" +#include "string_.h" +#include "xml_.h" + +using namespace anm2ed::resource; +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Sound::Sound(const Sound& other) : path(other.path) + { + audio = path.empty() ? Audio() : Audio(path.c_str()); + } + + Sound& Sound::operator=(const Sound& other) + { + if (this != &other) + { + path = other.path; + audio = path.empty() ? Audio() : Audio(path.c_str()); + } + return *this; + } + + Sound::Sound(const std::string& directory, const std::string& path) + { + filesystem::WorkingDirectory workingDirectory(directory); + this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string(); + this->path = string::backslash_replace_to_lower(this->path); + audio = Audio(this->path.c_str()); + } + + Sound::Sound(XMLElement* element, int& id) + { + if (!element) return; + element->QueryIntAttribute("Id", &id); + xml::query_path_attribute(element, "Path", &path); + string::backslash_replace_to_lower(this->path); + audio = Audio(this->path.c_str()); + } + + XMLElement* Sound::to_element(XMLDocument& document, int id) + { + auto element = document.NewElement("Sound"); + element->SetAttribute("Id", id); + element->SetAttribute("Path", path.c_str()); + return element; + } + + std::string Sound::to_string(int id) + { + XMLDocument document{}; + document.InsertEndChild(to_element(document, id)); + return xml::document_to_string(document); + } +} \ No newline at end of file diff --git a/src/anm2/sound.h b/src/anm2/sound.h new file mode 100644 index 0000000..fc1e43f --- /dev/null +++ b/src/anm2/sound.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "audio.h" + +namespace anm2ed::anm2 +{ + constexpr auto SOUND_FORMAT = "#{} {}"; + constexpr auto SOUND_FORMAT_C = "#%d %s"; + + class Sound + { + public: + std::filesystem::path path{}; + resource::Audio audio{}; + + Sound() = default; + Sound(Sound&&) noexcept = default; + Sound& operator=(Sound&&) noexcept = default; + + Sound(const Sound&); + Sound& operator=(const Sound&); + Sound(tinyxml2::XMLElement*, int&); + Sound(const std::string&, const std::string&); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); + std::string to_string(int); + }; +} \ No newline at end of file diff --git a/src/anm2/spritesheet.cpp b/src/anm2/spritesheet.cpp new file mode 100644 index 0000000..309c40e --- /dev/null +++ b/src/anm2/spritesheet.cpp @@ -0,0 +1,70 @@ +#include "spritesheet.h" + +#include "filesystem_.h" +#include "string_.h" +#include "xml_.h" + +using namespace anm2ed::resource; +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Spritesheet::Spritesheet(XMLElement* element, int& id) + { + if (!element) return; + element->QueryIntAttribute("Id", &id); + xml::query_path_attribute(element, "Path", &path); + // Spritesheet paths from Isaac Rebirth are made with the assumption that paths are case-insensitive + // However when using the resource dumper, the spritesheet paths are all lowercase (on Linux anyway) + // This will handle this case and make the paths OS-agnostic + this->path = string::backslash_replace_to_lower(this->path); + texture = Texture(path); + } + + Spritesheet::Spritesheet(const std::string& directory, const std::string& path) + { + filesystem::WorkingDirectory workingDirectory(directory); + this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string(); + this->path = string::backslash_replace_to_lower(this->path); + texture = Texture(this->path); + } + + XMLElement* Spritesheet::to_element(XMLDocument& document, int id) + { + auto element = document.NewElement("Spritesheet"); + element->SetAttribute("Id", id); + element->SetAttribute("Path", path.c_str()); + return element; + } + + void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id) + { + parent->InsertEndChild(to_element(document, id)); + } + + std::string Spritesheet::to_string(int id) + { + XMLDocument document{}; + document.InsertEndChild(to_element(document, id)); + return xml::document_to_string(document); + } + + bool Spritesheet::save(const std::string& directory, const std::string& path) + { + filesystem::WorkingDirectory workingDirectory(directory); + this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string(); + return texture.write_png(this->path); + } + + void Spritesheet::reload(const std::string& directory) + { + *this = Spritesheet(directory, this->path); + } + + bool Spritesheet::is_valid() + { + return texture.is_valid(); + } + +} \ No newline at end of file diff --git a/src/anm2/spritesheet.h b/src/anm2/spritesheet.h new file mode 100644 index 0000000..6db08f9 --- /dev/null +++ b/src/anm2/spritesheet.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include "texture.h" + +namespace anm2ed::anm2 +{ + constexpr auto SPRITESHEET_FORMAT_C = "#%d %s"; + constexpr auto SPRITESHEET_FORMAT = "#{} {}"; + + class Spritesheet + { + public: + std::filesystem::path path{}; + resource::Texture texture; + + Spritesheet() = default; + Spritesheet(tinyxml2::XMLElement*, int&); + Spritesheet(const std::string&, const std::string& = {}); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); + std::string to_string(int id); + bool save(const std::string&, const std::string& = {}); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); + void reload(const std::string&); + bool is_valid(); + }; +} \ No newline at end of file diff --git a/src/canvas.cpp b/src/canvas.cpp index 6359da4..0ea9e2f 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -1,19 +1,20 @@ #include "canvas.h" #include -#include #include #include #include #include -#include "math.h" +#include "math_.h" #include "texture.h" using namespace glm; -using namespace anm2ed::shader; +using namespace anm2ed::resource; +using namespace anm2ed::util; +using namespace anm2ed::canvas; -namespace anm2ed::canvas +namespace anm2ed { constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f}; constexpr float GRID_VERTICES[] = {-1.f, -1.f, 0.f, 0.f, 3.f, -1.f, 2.f, 0.f, -1.f, 3.f, 0.f, 2.f}; @@ -305,7 +306,7 @@ namespace anm2ed::canvas void Canvas::zoom_set(float& zoom, vec2& pan, vec2 focus, float step) { auto zoomFactor = math::percent_to_unit(zoom); - float newZoom = glm::clamp(math::round_nearest_multiple(zoom + step, step), canvas::ZOOM_MIN, canvas::ZOOM_MAX); + float newZoom = glm::clamp(math::round_nearest_multiple(zoom + step, step), ZOOM_MIN, ZOOM_MAX); if (newZoom != zoom) { float newZoomFactor = math::percent_to_unit(newZoom); @@ -347,4 +348,4 @@ namespace anm2ed::canvas pan = -rectCenter * fitScale; } } -} +} \ No newline at end of file diff --git a/src/canvas.h b/src/canvas.h index cb99da7..b1955b8 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -20,7 +20,10 @@ namespace anm2ed::canvas constexpr auto STEP = 1.0f; constexpr auto STEP_FAST = 5.0f; +} +namespace anm2ed +{ class Canvas { public: @@ -48,13 +51,14 @@ namespace anm2ed::canvas void size_set(glm::vec2); glm::vec4 pixel_read(glm::vec2, glm::vec2); glm::mat4 transform_get(float = 100.0f, glm::vec2 = {}); - void axes_render(shader::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f)); - void grid_render(shader::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {}, + void axes_render(resource::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f)); + void grid_render(resource::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {}, glm::vec4 = glm::vec4(1.0f)); - void texture_render(shader::Shader&, GLuint&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}, - float* = (float*)TEXTURE_VERTICES); - void rect_render(shader::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f), - float dashLength = DASH_LENGTH, float dashGap = DASH_GAP, float dashOffset = DASH_OFFSET); + void texture_render(resource::Shader&, GLuint&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}, + float* = (float*)canvas::TEXTURE_VERTICES); + void rect_render(resource::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f), + float dashLength = canvas::DASH_LENGTH, float dashGap = canvas::DASH_GAP, + float dashOffset = canvas::DASH_OFFSET); void viewport_set(); void clear(glm::vec4&); void bind(); diff --git a/src/clipboard.cpp b/src/clipboard.cpp index 8ec5e53..e66150d 100644 --- a/src/clipboard.cpp +++ b/src/clipboard.cpp @@ -2,7 +2,7 @@ #include -namespace anm2ed::clipboard +namespace anm2ed { std::string Clipboard::get() { diff --git a/src/clipboard.h b/src/clipboard.h index 0b13a07..9e8ec8b 100644 --- a/src/clipboard.h +++ b/src/clipboard.h @@ -2,7 +2,7 @@ #include -namespace anm2ed::clipboard +namespace anm2ed { class Clipboard { diff --git a/src/dialog.cpp b/src/dialog.cpp index 67da4f6..ddffb95 100644 --- a/src/dialog.cpp +++ b/src/dialog.cpp @@ -8,10 +8,6 @@ namespace anm2ed::dialog { - - constexpr SDL_DialogFileFilter FILE_FILTER_ANM2[] = {{"Anm2 file", "anm2;xml"}}; - constexpr SDL_DialogFileFilter FILE_FILTER_SPRITESHEET[] = {{"PNG image", "png"}}; - void callback(void* userData, const char* const* filelist, int filter) { auto self = (Dialog*)(userData); @@ -24,8 +20,12 @@ namespace anm2ed::dialog else self->selectedFilter = -1; } +} - Dialog::Dialog() = default; +using namespace anm2ed::dialog; + +namespace anm2ed +{ Dialog::Dialog(SDL_Window* window) { @@ -33,36 +33,24 @@ namespace anm2ed::dialog this->window = window; } - void Dialog::anm2_new() + void Dialog::file_open(dialog::Type type) { - SDL_ShowSaveFileDialog(callback, this, window, FILE_FILTER_ANM2, std::size(FILE_FILTER_ANM2), nullptr); - type = ANM2_NEW; + SDL_ShowOpenFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]), + nullptr, false); + this->type = type; } - void Dialog::anm2_open() + void Dialog::file_save(dialog::Type type) { - SDL_ShowOpenFileDialog(callback, this, window, FILE_FILTER_ANM2, std::size(FILE_FILTER_ANM2), nullptr, false); - type = ANM2_OPEN; + SDL_ShowSaveFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]), + nullptr); + this->type = type; } - void Dialog::anm2_save() + void Dialog::folder_open(dialog::Type type) { - SDL_ShowSaveFileDialog(callback, this, window, FILE_FILTER_ANM2, std::size(FILE_FILTER_ANM2), nullptr); - type = ANM2_SAVE; - } - - void Dialog::spritesheet_open() - { - SDL_ShowOpenFileDialog(callback, this, window, FILE_FILTER_SPRITESHEET, std::size(FILE_FILTER_SPRITESHEET), nullptr, - false); - type = SPRITESHEET_OPEN; - } - - void Dialog::spritesheet_replace() - { - SDL_ShowOpenFileDialog(callback, this, window, FILE_FILTER_SPRITESHEET, std::size(FILE_FILTER_SPRITESHEET), nullptr, - false); - type = SPRITESHEET_REPLACE; + SDL_ShowOpenFolderDialog(callback, this, window, nullptr, false); + this->type = type; } void Dialog::file_explorer_open(const std::string& path) @@ -79,8 +67,16 @@ namespace anm2ed::dialog *this = Dialog(this->window); } - bool Dialog::is_selected_file(Type type) + bool Dialog::is_selected(dialog::Type type) { return this->type == type && !path.empty(); } + + void Dialog::set_string_to_selected_path(std::string& string, dialog::Type type) + { + if (type == NONE) return; + if (!is_selected(type)) return; + string = path; + reset(); + } }; diff --git a/src/dialog.h b/src/dialog.h index 487c37f..46f716a 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -6,34 +6,88 @@ namespace anm2ed::dialog { +#if defined(_WIN32) + #define EXECUTABLE_FILTER {"Executable", "exe"} +#else + #define EXECUTABLE_FILTER \ + { \ + } +#endif + +#define FILTER_LIST \ + X(NO_FILTER, {}) \ + X(ANM2, {"Anm2 file", "anm2;xml"}) \ + X(PNG, {"PNG image", "png"}) \ + X(SOUND, {"WAV file;OGG file", "wav;ogg"}) \ + X(GIF, {"GIF image", "gif"}) \ + X(WEBM, {"WebM video", "webm"}) \ + X(MP4, {"MP4 video", "MP4"}) \ + X(EXECUTABLE, EXECUTABLE_FILTER) + + enum Filter + { +#define X(symbol, ...) symbol, + FILTER_LIST +#undef X + }; + + constexpr SDL_DialogFileFilter FILTERS[][1] = { +#define X(symbol, ...) {__VA_ARGS__}, + FILTER_LIST +#undef X + }; + +#undef FILTER_LIST + +#define DIALOG_LIST \ + X(NONE, NO_FILTER) \ + X(ANM2_NEW, ANM2) \ + X(ANM2_OPEN, ANM2) \ + X(ANM2_SAVE, ANM2) \ + X(SOUND_OPEN, SOUND) \ + X(SPRITESHEET_OPEN, PNG) \ + X(SPRITESHEET_REPLACE, PNG) \ + X(FFMPEG_PATH_SET, EXECUTABLE) \ + X(PNG_DIRECTORY_SET, NO_FILTER) \ + X(GIF_PATH_SET, GIF) \ + X(WEBM_PATH_SET, WEBM) \ + X(MP4_PATH_SET, MP4) + enum Type { - NONE, - ANM2_NEW, - ANM2_OPEN, - ANM2_SAVE, - SPRITESHEET_OPEN, - SPRITESHEET_REPLACE +#define X(symbol, filter) symbol, + DIALOG_LIST +#undef X }; + constexpr Filter TYPE_FILTERS[] = { +#define X(symbol, filter) filter, + DIALOG_LIST +#undef X + }; + +#undef DIALOG_LIST +} + +namespace anm2ed +{ + class Dialog { public: SDL_Window* window{}; std::string path{}; - Type type{NONE}; + dialog::Type type{dialog::NONE}; int selectedFilter{-1}; - int replaceID{-1}; - Dialog(); + Dialog() = default; Dialog(SDL_Window*); - void anm2_new(); - void anm2_open(); - void anm2_save(); - void spritesheet_open(); - void spritesheet_replace(); - void file_explorer_open(const std::string&); + void file_open(dialog::Type type); + void file_save(dialog::Type type); + void folder_open(dialog::Type type); + bool is_selected(dialog::Type type); void reset(); - bool is_selected_file(Type); + void file_explorer_open(const std::string&); + void set_string_to_selected_path(std::string& set, dialog::Type type); }; } diff --git a/src/dockspace.h b/src/dockspace.h deleted file mode 100644 index 532fed3..0000000 --- a/src/dockspace.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include "animation_preview.h" -#include "animations.h" -#include "documents.h" -#include "events.h" -#include "frame_properties.h" -#include "layers.h" -#include "nulls.h" -#include "onionskin.h" -#include "spritesheet_editor.h" -#include "spritesheets.h" -#include "taskbar.h" -#include "timeline.h" -#include "tools.h" -#include "welcome.h" - -namespace anm2ed::dockspace -{ - class Dockspace - { - animation_preview::AnimationPreview animationPreview; - animations::Animations animations; - events::Events events; - frame_properties::FrameProperties frameProperties; - layers::Layers layers; - nulls::Nulls nulls; - onionskin::Onionskin onionskin; - spritesheet_editor::SpritesheetEditor spritesheetEditor; - spritesheets::Spritesheets spritesheets; - timeline::Timeline timeline; - tools::Tools tools; - welcome::Welcome welcome; - - public: - void update(taskbar::Taskbar&, documents::Documents&, manager::Manager&, settings::Settings&, resources::Resources&, - dialog::Dialog&, clipboard::Clipboard&); - }; -} diff --git a/src/document.cpp b/src/document.cpp index a24ccb4..a17f208 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -3,26 +3,24 @@ #include #include -#include "anm2.h" -#include "filesystem.h" +#include "filesystem_.h" #include "log.h" +#include "map_.h" #include "toast.h" -#include "util.h" +#include "vector_.h" using namespace anm2ed::anm2; -using namespace anm2ed::filesystem; -using namespace anm2ed::toast; +using namespace anm2ed::imgui; using namespace anm2ed::types; using namespace anm2ed::util; -using namespace anm2ed::log; using namespace glm; -namespace anm2ed::document +namespace anm2ed { Document::Document(const std::string& path, bool isNew, std::string* errorString) { - if (!path_is_exist(path)) return; + if (!filesystem::path_is_exist(path)) return; if (isNew) anm2 = anm2::Anm2(); @@ -34,7 +32,7 @@ namespace anm2ed::document this->path = path; clean(); - change(change::ALL); + change(Document::ALL); } bool Document::save(const std::string& path, std::string* errorString) @@ -43,12 +41,12 @@ namespace anm2ed::document if (anm2.serialize(this->path, errorString)) { - toasts.info(std::format("Saved document to: {}", path)); + toasts.info(std::format("Saved document to: {}", this->path.string())); clean(); return true; } else if (errorString) - toasts.warning(std::format("Could not save document to: {} ({})", path, *errorString)); + toasts.warning(std::format("Could not save document to: {} ({})", this->path.string(), *errorString)); return false; } @@ -82,43 +80,76 @@ namespace anm2ed::document isForceDirty = false; } - void Document::change(change::Type type) + void Document::change(ChangeType type) { hash_set(); - auto layer_set = [&]() { unusedLayerIDs = anm2.layers_unused(); }; - auto null_set = [&]() { unusedNullIDs = anm2.nulls_unused(); }; - auto event_set = [&]() { unusedEventIDs = anm2.events_unused(); }; - auto spritesheet_set = [&]() + auto layers_set = [&]() { unusedLayerIDs = anm2.layers_unused(); }; + auto nulls_set = [&]() { unusedNullIDs = anm2.nulls_unused(); }; + auto events_set = [&]() + { + unusedEventIDs = anm2.events_unused(); + eventNames = anm2.event_names_get(); + for (auto& name : eventNames) + eventNamesCStr.push_back(name.c_str()); + }; + + auto animations_set = [&]() + { + animationNames = anm2.animation_names_get(); + animationNamesCStr.clear(); + animationNames.insert(animationNames.begin(), "None"); + for (auto& name : animationNames) + animationNamesCStr.push_back(name.c_str()); + }; + + auto spritesheets_set = [&]() { unusedSpritesheetIDs = anm2.spritesheets_unused(); spritesheetNames = anm2.spritesheet_names_get(); - spritesheetNamesCstr.clear(); + spritesheetNamesCStr.clear(); for (auto& name : spritesheetNames) - spritesheetNamesCstr.push_back(name.c_str()); + spritesheetNamesCStr.push_back(name.c_str()); + }; + + auto sounds_set = [&]() + { + unusedSoundIDs = anm2.sounds_unused(); + soundNames = anm2.sound_names_get(); + soundNamesCStr.clear(); + for (auto& name : soundNames) + soundNamesCStr.push_back(name.c_str()); }; switch (type) { - case change::LAYERS: - layer_set(); + case LAYERS: + layers_set(); break; - case change::NULLS: - null_set(); + case NULLS: + nulls_set(); break; - case change::EVENTS: - event_set(); + case EVENTS: + events_set(); break; - case change::SPRITESHEETS: - spritesheet_set(); + case ANIMATIONS: + animations_set(); break; - case change::ITEMS: + case SPRITESHEETS: + spritesheets_set(); break; - case change::ALL: - layer_set(); - null_set(); - event_set(); - spritesheet_set(); + case SOUNDS: + sounds_set(); + break; + case ITEMS: + break; + case ALL: + layers_set(); + nulls_set(); + events_set(); + animations_set(); + spritesheets_set(); + sounds_set(); break; default: break; @@ -159,7 +190,7 @@ namespace anm2ed::document { snapshot("Bake Frames"); anm2.bake(reference, interval, isRoundScale, isRoundRotation); - change(change::FRAMES); + change(Document::FRAMES); } void Document::frames_add(anm2::Item* item) @@ -188,7 +219,7 @@ namespace anm2ed::document snapshot("Delete Frames"); item->frames.erase(item->frames.begin() + reference.frameIndex); reference.frameIndex = glm::max(-1, --reference.frameIndex); - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_crop_set(anm2::Frame* frame, vec2 crop) @@ -196,7 +227,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Crop"); frame->crop = crop; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_size_set(anm2::Frame* frame, vec2 size) @@ -204,7 +235,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Size"); frame->size = size; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_position_set(anm2::Frame* frame, vec2 position) @@ -212,7 +243,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Position"); frame->position = position; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_pivot_set(anm2::Frame* frame, vec2 pivot) @@ -220,7 +251,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Pivot"); frame->pivot = pivot; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_scale_set(anm2::Frame* frame, vec2 scale) @@ -228,7 +259,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Scale"); frame->scale = scale; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_rotation_set(anm2::Frame* frame, float rotation) @@ -236,7 +267,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Rotation"); frame->rotation = rotation; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_delay_set(anm2::Frame* frame, int delay) @@ -244,7 +275,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Delay"); frame->delay = delay; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_tint_set(anm2::Frame* frame, vec4 tint) @@ -252,7 +283,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Tint"); frame->tint = tint; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_color_offset_set(anm2::Frame* frame, vec3 colorOffset) @@ -260,7 +291,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Color Offset"); frame->colorOffset = colorOffset; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_is_visible_set(anm2::Frame* frame, bool isVisible) @@ -268,7 +299,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Visibility"); frame->isVisible = isVisible; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_is_interpolated_set(anm2::Frame* frame, bool isInterpolated) @@ -276,7 +307,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Interpolation"); frame->isInterpolated = isInterpolated; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_flip_x(anm2::Frame* frame) @@ -284,7 +315,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Flip X"); frame->scale.x = -frame->scale.x; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_flip_y(anm2::Frame* frame) @@ -292,7 +323,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Frame Flip Y"); frame->scale.y = -frame->scale.y; - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_shorten() @@ -301,7 +332,7 @@ namespace anm2ed::document if (!frame) return; snapshot("Shorten Frame"); frame->shorten(); - change(change::FRAMES); + change(Document::FRAMES); } void Document::frame_extend() @@ -310,10 +341,10 @@ namespace anm2ed::document if (!frame) return; snapshot("Extend Frame"); frame->extend(); - change(change::FRAMES); + change(Document::FRAMES); } - void Document::frames_change(anm2::FrameChange& frameChange, frame_change::Type type, bool isFromSelectedFrame, + void Document::frames_change(anm2::FrameChange& frameChange, anm2::ChangeType type, bool isFromSelectedFrame, int numberFrames) { auto item = item_get(); @@ -321,7 +352,7 @@ namespace anm2ed::document snapshot("Change All Frame Properties"); item->frames_change(frameChange, type, isFromSelectedFrame && frame_get() ? reference.frameIndex : 0, isFromSelectedFrame ? numberFrames : -1); - change(change::FRAMES); + change(Document::FRAMES); } void Document::frames_deserialize(const std::string& string) @@ -333,7 +364,7 @@ namespace anm2ed::document std::string errorString{}; auto start = reference.frameIndex + 1; if (item->frames_deserialize(string, reference.itemType, start, indices, &errorString)) - change(change::FRAMES); + change(Document::FRAMES); else toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); } @@ -359,7 +390,7 @@ namespace anm2ed::document { spritesheetMultiSelect = {id}; toasts.info(std::format("Initialized spritesheet #{}: {}", id, path)); - change(change::SPRITESHEETS); + change(Document::SPRITESHEETS); } else toasts.error(std::format("Failed to initialize spritesheet: {}", path)); @@ -370,7 +401,7 @@ namespace anm2ed::document snapshot("Paste Spritesheet(s)"); std::string errorString{}; if (anm2.content.spritesheets_deserialize(string, directory_get(), type, &errorString)) - change(change::SPRITESHEETS); + change(Document::SPRITESHEETS); else toasts.error(std::format("Failed to deserialize spritesheet(s): {}", errorString)); } @@ -380,7 +411,7 @@ namespace anm2ed::document snapshot("Paste Layer(s)"); std::string errorString{}; if (anm2.content.layers_deserialize(string, type, &errorString)) - change(change::NULLS); + change(Document::NULLS); else toasts.error(std::format("Failed to deserialize layer(s): {}", errorString)); } @@ -400,7 +431,7 @@ namespace anm2ed::document anm2.content.layers[id] = layer; layersMultiSelect = {id}; } - change(change::LAYERS); + change(Document::LAYERS); } void Document::layers_remove_unused() @@ -408,7 +439,7 @@ namespace anm2ed::document snapshot("Remove Unused Layers"); for (auto& id : unusedLayerIDs) anm2.content.layers.erase(id); - change(change::LAYERS); + change(Document::LAYERS); unusedLayerIDs.clear(); } @@ -427,14 +458,14 @@ namespace anm2ed::document anm2.content.nulls[id] = null; nullMultiSelect = {id}; } - change(change::NULLS); + change(Document::NULLS); } void Document::null_rect_toggle(anm2::Null& null) { snapshot("Null Rect"); null.isShowRect = !null.isShowRect; - change(change::NULLS); + change(Document::NULLS); } void Document::nulls_remove_unused() @@ -442,7 +473,7 @@ namespace anm2ed::document snapshot("Remove Unused Nulls"); for (auto& id : unusedNullIDs) anm2.content.nulls.erase(id); - change(change::NULLS); + change(Document::NULLS); unusedNullIDs.clear(); } @@ -451,18 +482,27 @@ namespace anm2ed::document snapshot("Paste Null(s)"); std::string errorString{}; if (anm2.content.nulls_deserialize(string, type, &errorString)) - change(change::NULLS); + change(Document::NULLS); else toasts.error(std::format("Failed to deserialize null(s): {}", errorString)); } - void Document::event_add() + void Document::event_set(anm2::Event& event) { - snapshot("Add Event"); - int id{}; - anm2.event_add(id); - eventMultiSelect = {id}; - change(change::EVENTS); + if (referenceEvent > -1) + { + snapshot("Set Event"); + anm2.content.events[referenceEvent] = event; + eventMultiSelect = {referenceEvent}; + } + else + { + snapshot("Add Event"); + auto id = map::next_id_get(anm2.content.events); + anm2.content.events[id] = event; + eventMultiSelect = {id}; + } + change(Document::EVENTS); } void Document::events_remove_unused() @@ -470,7 +510,7 @@ namespace anm2ed::document snapshot("Remove Unused Events"); for (auto& id : unusedEventIDs) anm2.content.events.erase(id); - change(change::EVENTS); + change(Document::EVENTS); unusedEventIDs.clear(); } @@ -479,7 +519,40 @@ namespace anm2ed::document snapshot("Paste Event(s)"); std::string errorString{}; if (anm2.content.events_deserialize(string, type, &errorString)) - change(change::EVENTS); + change(Document::EVENTS); + else + toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); + } + + void Document::sound_add(const std::string& path) + { + int id{}; + snapshot("Add Sound"); + if (anm2.sound_add(directory_get(), path, id)) + { + soundMultiSelect = {id}; + toasts.info(std::format("Initialized sound #{}: {}", id, path)); + change(Document::SOUNDS); + } + else + toasts.error(std::format("Failed to initialize sound: {}", path)); + } + + void Document::sounds_remove_unused() + { + snapshot("Remove Unused Sounds"); + for (auto& id : unusedSoundIDs) + anm2.content.sounds.erase(id); + change(Document::LAYERS); + unusedSoundIDs.clear(); + } + + void Document::sounds_deserialize(const std::string& string, merge::Type type) + { + snapshot("Paste Sound(s)"); + std::string errorString{}; + if (anm2.content.sounds_deserialize(string, directory_get(), type, &errorString)) + change(Document::EVENTS); else toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); } @@ -498,7 +571,7 @@ namespace anm2ed::document reference = addReference; - change(change::ITEMS); + change(Document::ITEMS); } void Document::item_remove(anm2::Animation* animation) @@ -507,7 +580,7 @@ namespace anm2ed::document snapshot("Remove Item"); animation->item_remove(reference.itemType, reference.itemID); reference = {reference.animationIndex}; - change(change::ITEMS); + change(Document::ITEMS); } void Document::item_visible_toggle(anm2::Item* item) @@ -515,7 +588,7 @@ namespace anm2ed::document if (!item) return; snapshot("Item Visibility"); item->isVisible = !item->isVisible; - change(change::ITEMS); + change(Document::ITEMS); } anm2::Animation* Document::animation_get() @@ -527,7 +600,7 @@ namespace anm2ed::document { snapshot("Select Animation"); reference = {index}; - change(change::ITEMS); + change(Document::ITEMS); } void Document::animation_add() @@ -549,7 +622,7 @@ namespace anm2ed::document anm2.animations.items.insert(anm2.animations.items.begin() + index, animation); animationMultiSelect = {index}; reference = {index}; - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } void Document::animation_duplicate() @@ -563,14 +636,14 @@ namespace anm2ed::document animationMultiSelect.insert(++duplicatedEnd); animationMultiSelect.erase(id); } - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } void Document::animations_move(std::vector& indices, int index) { snapshot("Move Animation(s)"); animationMultiSelect = vector::move_indices(anm2.animations.items, indices, index); - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } void Document::animations_remove() @@ -589,14 +662,14 @@ namespace anm2ed::document hoveredAnimation = -1; } - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } void Document::animation_default() { snapshot("Default Animation"); anm2.animations.defaultAnimation = anm2.animations.items[*animationMultiSelect.begin()].name; - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } void Document::animations_deserialize(const std::string& string) @@ -609,7 +682,7 @@ namespace anm2ed::document if (anm2.animations.animations_deserialize(string, start, indices, &errorString)) { multiSelect = indices; - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } else toasts.error(std::format("Failed to deserialize animation(s): {}", errorString)); @@ -624,7 +697,7 @@ namespace anm2ed::document if (auto animation = animation_get()) animation->frameNum = animation->length(); - change(change::ALL); + change(Document::ALL); } void Document::animations_merge_quick() @@ -648,7 +721,7 @@ namespace anm2ed::document animationMultiSelect = {merged}; reference = {merged}; - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } void Document::animations_merge(merge::Type type, bool isDeleteAnimationsAfter) @@ -657,7 +730,7 @@ namespace anm2ed::document auto merged = anm2.animations.merge(mergeTarget, animationMergeMultiSelect, type, isDeleteAnimationsAfter); animationMultiSelect = {merged}; reference = {merged}; - change(change::ANIMATIONS); + change(Document::ANIMATIONS); } void Document::snapshot(const std::string& message) @@ -669,22 +742,22 @@ namespace anm2ed::document { snapshots.undo(anm2, reference, message); toasts.info(std::format("Undo: {}", message)); - change(change::ALL); + change(Document::ALL); } void Document::redo() { toasts.info(std::format("Redo: {}", message)); snapshots.redo(anm2, reference, message); - change(change::ALL); + change(Document::ALL); } - bool Document::is_undo() + bool Document::is_able_to_undo() { return !snapshots.undoStack.is_empty(); } - bool Document::is_redo() + bool Document::is_able_to_redo() { return !snapshots.redoStack.is_empty(); } diff --git a/src/document.h b/src/document.h index 46ec309..35ae5ef 100644 --- a/src/document.h +++ b/src/document.h @@ -3,24 +3,38 @@ #include #include -#include "anm2.h" -#include "imgui.h" +#include "anm2/anm2.h" +#include "imgui_.h" #include "playback.h" #include "snapshots.h" #include "types.h" #include -namespace anm2ed::document +namespace anm2ed { class Document { public: + enum ChangeType + { + LAYERS, + NULLS, + SPRITESHEETS, + EVENTS, + ANIMATIONS, + ITEMS, + FRAMES, + SOUNDS, + ALL, + COUNT + }; + std::filesystem::path path{}; anm2::Anm2 anm2{}; std::string message{}; - playback::Playback playback{}; - snapshots::Snapshots snapshots{}; + Playback playback{}; + Snapshots snapshots{}; float previewZoom{200}; glm::vec2 previewPan{}; @@ -34,6 +48,8 @@ namespace anm2ed::document int mergeTarget{-1}; imgui::MultiSelectStorage animationMultiSelect; imgui::MultiSelectStorage animationMergeMultiSelect; + std::vector animationNamesCStr{}; + std::vector animationNames{}; anm2::Reference hoveredFrame{anm2::REFERENCE_DEFAULT}; @@ -41,7 +57,7 @@ namespace anm2ed::document int hoveredSpritesheet{-1}; std::set unusedSpritesheetIDs{}; std::vector spritesheetNames{}; - std::vector spritesheetNamesCstr{}; + std::vector spritesheetNamesCStr{}; imgui::MultiSelectStorage spritesheetMultiSelect; int referenceLayer{-1}; @@ -58,6 +74,15 @@ namespace anm2ed::document int hoveredEvent{-1}; std::set unusedEventIDs{}; imgui::MultiSelectStorage eventMultiSelect; + std::vector eventNamesCStr{}; + std::vector eventNames{}; + + int referenceSound{-1}; + int hoveredSound{-1}; + std::set unusedSoundIDs{}; + imgui::MultiSelectStorage soundMultiSelect; + std::vector soundNamesCStr{}; + std::vector soundNames{}; uint64_t hash{}; uint64_t saveHash{}; @@ -72,7 +97,7 @@ namespace anm2ed::document void hash_set(); void clean(); void on_change(); - void change(types::change::Type); + void change(ChangeType); bool is_dirty(); bool is_autosave_dirty(); std::filesystem::path directory_get(); @@ -81,7 +106,6 @@ namespace anm2ed::document anm2::Frame* frame_get(); void frames_add(anm2::Item* item); - void frames_change(); void frames_delete(anm2::Item* item); void frames_bake(int, bool, bool); void frame_crop_set(anm2::Frame*, glm::vec2); @@ -99,31 +123,35 @@ namespace anm2ed::document void frame_flip_y(anm2::Frame* frame); void frame_shorten(); void frame_extend(); - void frames_change(anm2::FrameChange&, types::frame_change::Type, bool, int = -1); + void frames_change(anm2::FrameChange&, anm2::ChangeType, bool, int = -1); void frames_deserialize(const std::string&); anm2::Item* item_get(); void item_add(anm2::Type, int, std::string&, types::locale::Type, int); - void item_remove(anm2::Animation* animation); + void item_remove(anm2::Animation*); void item_visible_toggle(anm2::Item*); anm2::Spritesheet* spritesheet_get(); void spritesheet_add(const std::string&); void spritesheets_deserialize(const std::string&, types::merge::Type); - void layer_set(anm2::Layer& layer); + void layer_set(anm2::Layer&); void layers_remove_unused(); void layers_deserialize(const std::string&, types::merge::Type); - void null_set(anm2::Null& null); - void null_rect_toggle(anm2::Null& null); + void null_set(anm2::Null&); + void null_rect_toggle(anm2::Null&); void nulls_remove_unused(); void nulls_deserialize(const std::string&, types::merge::Type); - void event_add(); + void event_set(anm2::Event&); void events_remove_unused(); void events_deserialize(const std::string&, types::merge::Type); + void sound_add(const std::string& path); + void sounds_remove_unused(); + void sounds_deserialize(const std::string& string, types::merge::Type); + void animation_add(); void animation_set(int); void animation_duplicate(); @@ -141,7 +169,19 @@ namespace anm2ed::document void undo(); void redo(); - bool is_undo(); - bool is_redo(); + bool is_able_to_undo(); + bool is_able_to_redo(); }; + +#define DOCUMENT_SNAPSHOT(document, message) document.snapshot(message); +#define DOCUMENT_CHANGE(document, changeType) document.change(changeType); + +#define DOCUMENT_EDIT(document, message, changeType, body) \ + { \ + \ + DOCUMENT_SNAPSHOT(document, message) \ + body; \ + DOCUMENT_CHANGE(document, changeType) \ + } + } diff --git a/src/events.h b/src/events.h deleted file mode 100644 index ef27454..0000000 --- a/src/events.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "clipboard.h" -#include "manager.h" -#include "resources.h" -#include "settings.h" - -namespace anm2ed::events -{ - class Events - { - - public: - void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); - }; -} diff --git a/src/frame_properties.cpp b/src/frame_properties.cpp deleted file mode 100644 index 7a086dc..0000000 --- a/src/frame_properties.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "frame_properties.h" - -#include - -#include - -#include "imgui.h" -#include "math.h" -#include "types.h" - -using namespace anm2ed::settings; -using namespace anm2ed::manager; -using namespace anm2ed::math; -using namespace anm2ed::types; -using namespace glm; - -namespace anm2ed::frame_properties -{ - - void FrameProperties::update(Manager& manager, Settings& settings) - { - if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties)) - { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& reference = document.reference; - auto& type = reference.itemType; - auto frame = document.frame_get(); - auto useFrame = frame ? *frame : anm2::Frame(); - - ImGui::BeginDisabled(!frame); - { - if (type == anm2::TRIGGER) - { - std::vector eventNames{}; - for (auto& event : anm2.content.events | std::views::values) - eventNames.emplace_back(event.name); - - imgui::combo_strings("Event", frame ? &frame->eventID : &dummy_value(), eventNames); - ImGui::SetItemTooltip("%s", "Change the event this trigger uses."); - ImGui::InputInt("At Frame", frame ? &frame->atFrame : &dummy_value(), step::NORMAL, step::FAST, - !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); - ImGui::SetItemTooltip("%s", "Change the frame the trigger will be activated at."); - } - else - { - ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); - { - if (ImGui::InputFloat2("Crop", frame ? value_ptr(useFrame.crop) : &dummy_value(), - frame ? vec2_format_get(useFrame.crop) : "")) - document.frame_crop_set(frame, useFrame.crop); - ImGui::SetItemTooltip("%s", "Change the crop position the frame uses."); - - if (ImGui::InputFloat2("Size", frame ? value_ptr(useFrame.size) : &dummy_value(), - frame ? vec2_format_get(useFrame.size) : "")) - document.frame_size_set(frame, useFrame.size); - ImGui::SetItemTooltip("%s", "Change the size of the crop the frame uses."); - } - ImGui::EndDisabled(); - - if (ImGui::InputFloat2("Position", frame ? value_ptr(useFrame.position) : &dummy_value(), - frame ? vec2_format_get(useFrame.position) : "")) - document.frame_position_set(frame, useFrame.position); - ImGui::SetItemTooltip("%s", "Change the position of the frame."); - - ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); - { - if (ImGui::InputFloat2("Pivot", frame ? value_ptr(useFrame.pivot) : &dummy_value(), - frame ? vec2_format_get(useFrame.pivot) : "")) - document.frame_pivot_set(frame, useFrame.pivot); - ImGui::SetItemTooltip("%s", "Change the pivot of the frame; i.e., where it is centered."); - } - ImGui::EndDisabled(); - - if (ImGui::InputFloat2("Scale", frame ? value_ptr(useFrame.scale) : &dummy_value(), - frame ? vec2_format_get(useFrame.scale) : "")) - document.frame_scale_set(frame, useFrame.scale); - ImGui::SetItemTooltip("%s", "Change the scale of the frame, in percent."); - - if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value(), step::NORMAL, - step::FAST, frame ? float_format_get(useFrame.rotation) : "")) - document.frame_rotation_set(frame, useFrame.rotation); - ImGui::SetItemTooltip("%s", "Change the rotation of the frame."); - - if (ImGui::InputInt("Duration", frame ? &useFrame.delay : &dummy_value(), step::NORMAL, step::FAST, - !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) - document.frame_delay_set(frame, useFrame.delay); - ImGui::SetItemTooltip("%s", "Change how long the frame lasts."); - - if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value())) - document.frame_tint_set(frame, useFrame.tint); - ImGui::SetItemTooltip("%s", "Change the tint of the frame."); - - if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value())) - document.frame_color_offset_set(frame, useFrame.colorOffset); - ImGui::SetItemTooltip("%s", "Change the color added onto the frame."); - - if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value())) - document.frame_is_visible_set(frame, useFrame.isVisible); - ImGui::SetItemTooltip("%s", "Toggle the frame's visibility."); - - ImGui::SameLine(); - - if (ImGui::Checkbox("Interpolated", frame ? &useFrame.isInterpolated : &dummy_value())) - document.frame_is_interpolated_set(frame, useFrame.isInterpolated); - ImGui::SetItemTooltip( - "%s", "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time."); - - auto widgetSize = imgui::widget_size_with_row_get(2); - - if (ImGui::Button("Flip X", widgetSize)) document.frame_flip_x(frame); - ImGui::SetItemTooltip("%s", "Flip the horizontal scale of the frame, to cheat mirroring the frame " - "horizontally.\n(Note: the format does not support mirroring.)"); - ImGui::SameLine(); - if (ImGui::Button("Flip Y", widgetSize)) document.frame_flip_y(frame); - ImGui::SetItemTooltip("%s", "Flip the vertical scale of the frame, to cheat mirroring the frame " - "vertically.\n(Note: the format does not support mirroring.)"); - } - } - ImGui::EndDisabled(); - } - ImGui::End(); - } -} \ No newline at end of file diff --git a/src/dockspace.cpp b/src/imgui/dockspace.cpp similarity index 83% rename from src/dockspace.cpp rename to src/imgui/dockspace.cpp index ec28d91..4a5a99d 100644 --- a/src/dockspace.cpp +++ b/src/imgui/dockspace.cpp @@ -1,22 +1,13 @@ #include "dockspace.h" -#include "animations.h" -#include "onionskin.h" -#include "tools.h" - -using namespace anm2ed::animations; -using namespace anm2ed::dialog; -using namespace anm2ed::clipboard; -using namespace anm2ed::manager; -using namespace anm2ed::documents; -using namespace anm2ed::playback; -using namespace anm2ed::resources; -using namespace anm2ed::settings; -using namespace anm2ed::taskbar; -using namespace anm2ed::welcome; - -namespace anm2ed::dockspace +namespace anm2ed::imgui { + void Dockspace::tick(Manager& manager, Settings& settings) + { + if (auto document = manager.get(); document) + if (settings.windowIsAnimationPreview) animationPreview.tick(manager, *document, settings); + } + void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, Clipboard& clipboard) { @@ -44,6 +35,7 @@ namespace anm2ed::dockspace if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard); if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard); if (settings.windowIsOnionskin) onionskin.update(settings); + if (settings.windowIsSounds) sounds.update(manager, settings, resources, dialog, clipboard); if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources); if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard); if (settings.windowIsTimeline) timeline.update(manager, settings, resources, clipboard); diff --git a/src/imgui/dockspace.h b/src/imgui/dockspace.h new file mode 100644 index 0000000..52cd852 --- /dev/null +++ b/src/imgui/dockspace.h @@ -0,0 +1,41 @@ +#pragma once + +#include "documents.h" +#include "taskbar.h" +#include "window/animation_preview.h" +#include "window/animations.h" +#include "window/events.h" +#include "window/frame_properties.h" +#include "window/layers.h" +#include "window/nulls.h" +#include "window/onionskin.h" +#include "window/sounds.h" +#include "window/spritesheet_editor.h" +#include "window/spritesheets.h" +#include "window/timeline.h" +#include "window/tools.h" +#include "window/welcome.h" + +namespace anm2ed::imgui +{ + class Dockspace + { + AnimationPreview animationPreview; + Animations animations; + Events events; + FrameProperties frameProperties; + Layers layers; + Nulls nulls; + Onionskin onionskin; + SpritesheetEditor spritesheetEditor; + Spritesheets spritesheets; + Sounds sounds; + Timeline timeline; + Tools tools; + Welcome welcome; + + public: + void tick(Manager&, Settings&); + void update(Taskbar&, Documents&, Manager&, Settings&, Resources&, Dialog&, Clipboard&); + }; +} diff --git a/src/documents.cpp b/src/imgui/documents.cpp similarity index 94% rename from src/documents.cpp rename to src/imgui/documents.cpp index 5fae943..9988778 100644 --- a/src/documents.cpp +++ b/src/imgui/documents.cpp @@ -2,16 +2,13 @@ #include -#include "util.h" +#include "time_.h" -using namespace anm2ed::taskbar; -using namespace anm2ed::manager; -using namespace anm2ed::settings; -using namespace anm2ed::resources; +using namespace anm2ed::resource; using namespace anm2ed::types; using namespace anm2ed::util; -namespace anm2ed::documents +namespace anm2ed::imgui { void Documents::update(Taskbar& taskbar, Manager& manager, Settings& settings, Resources& resources, bool& isQuitting) { @@ -27,7 +24,7 @@ namespace anm2ed::documents auto isDirty = document.is_dirty() && document.is_autosave_dirty(); document.lastAutosaveTime += ImGui::GetIO().DeltaTime; - if (isDirty && document.lastAutosaveTime > time::SECOND_S) manager.autosave(document); + if (isDirty && document.lastAutosaveTime > settings.fileAutosaveTime * time::SECOND_M) manager.autosave(document); } if (ImGui::Begin("##Documents", nullptr, diff --git a/src/documents.h b/src/imgui/documents.h similarity index 70% rename from src/documents.h rename to src/imgui/documents.h index 80050cc..0011f8b 100644 --- a/src/documents.h +++ b/src/imgui/documents.h @@ -6,7 +6,7 @@ #include "settings.h" #include "taskbar.h" -namespace anm2ed::documents +namespace anm2ed::imgui { class Documents { @@ -16,6 +16,6 @@ namespace anm2ed::documents public: float height{}; - void update(taskbar::Taskbar&, manager::Manager&, settings::Settings&, resources::Resources&, bool&); + void update(Taskbar&, Manager&, Settings&, Resources&, bool&); }; } diff --git a/src/imgui.cpp b/src/imgui/imgui_.cpp similarity index 87% rename from src/imgui.cpp rename to src/imgui/imgui_.cpp index d5a1713..ebcee70 100644 --- a/src/imgui.cpp +++ b/src/imgui/imgui_.cpp @@ -1,4 +1,4 @@ -#include "imgui.h" +#include "imgui_.h" #include @@ -11,6 +11,97 @@ using namespace glm; namespace anm2ed::imgui { + int input_text_callback(ImGuiInputTextCallbackData* data) + { + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + auto* string = (std::string*)(data->UserData); + string->resize(data->BufTextLen); + data->Buf = string->data(); + } + return 0; + } + + bool input_text_string(const char* label, std::string* string, ImGuiInputTextFlags flags) + { + flags |= ImGuiInputTextFlags_CallbackResize; + return ImGui::InputText(label, string->data(), string->capacity() + 1, flags, input_text_callback, string); + } + + bool combo_strings(const std::string& label, int* index, std::vector& strings) + { + std::vector items{}; + for (auto& string : strings) + items.push_back(string.c_str()); + return ImGui::Combo(label.c_str(), index, items.data(), (int)items.size()); + } + + bool combo_strings(const std::string& label, int* index, std::vector& strings) + { + return ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size()); + } + + bool input_int_range(const char* label, int& value, int min, int max, int step, int stepFast, + ImGuiInputTextFlags flags) + { + auto isActivated = ImGui::InputInt(label, &value, step, stepFast, flags); + value = glm::clamp(value, min, max); + return isActivated; + } + + bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected, + ImGuiSelectableFlags flags, bool* isRenamed) + { + static std::string editID{}; + static bool isJustEdit{}; + const bool isEditing = editID == id; + bool isActivated{}; + + if (isEditing) + { + if (isJustEdit) + { + ImGui::SetKeyboardFocusHere(); + isJustEdit = false; + } + + ImGui::SetNextItemWidth(-FLT_MIN); + if (input_text_string("##Edit", &text, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) + { + editID.clear(); + isActivated = true; + if (isRenamed) *isRenamed = true; + } + if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) editID.clear(); + } + else + { + if (ImGui::Selectable(label.c_str(), isSelected, flags)) isActivated = true; + + if ((ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_F2) && isSelected) || + (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))) + { + editID = id; + isJustEdit = true; + } + } + + return isActivated; + } + + void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut) + { + ImGui::SetItemTooltip("%s\n(Shortcut: %s)", tooltip, shortcut.c_str()); + } + + void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected) + { + auto* set = (std::set*)self->UserData; + if (isSelected) + set->insert(id); + else + set->erase(id); + }; std::string chord_to_string(ImGuiKeyChord chord) { @@ -91,90 +182,6 @@ namespace anm2ed::imgui (ImGui::GetFrameHeightWithSpacing() * rowCount) + (ImGui::GetStyle().WindowPadding.y * 2.0f)); } - int input_text_callback(ImGuiInputTextCallbackData* data) - { - if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) - { - auto* string = (std::string*)(data->UserData); - string->resize(data->BufTextLen); - data->Buf = string->data(); - } - return 0; - } - - bool input_text_string(const char* label, std::string* string, ImGuiInputTextFlags flags) - { - flags |= ImGuiInputTextFlags_CallbackResize; - return ImGui::InputText(label, string->data(), string->capacity() + 1, flags, input_text_callback, string); - } - - void combo_strings(const std::string& label, int* index, std::vector& strings) - { - std::vector items{}; - for (auto& string : strings) - items.push_back(string.c_str()); - ImGui::Combo(label.c_str(), index, items.data(), (int)items.size()); - } - - void combo_strings(const std::string& label, int* index, std::vector& strings) - { - ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size()); - } - - bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected, - ImGuiSelectableFlags flags, bool* isRenamed) - { - static std::string editID{}; - static bool isJustEdit{}; - const bool isEditing = editID == id; - bool isActivated{}; - - if (isEditing) - { - if (isJustEdit) - { - ImGui::SetKeyboardFocusHere(); - isJustEdit = false; - } - - ImGui::SetNextItemWidth(-FLT_MIN); - if (input_text_string("##Edit", &text, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) - { - editID.clear(); - isActivated = true; - if (isRenamed) *isRenamed = true; - } - if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) editID.clear(); - } - else - { - if (ImGui::Selectable(label.c_str(), isSelected, flags)) isActivated = true; - - if ((ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_F2) && isSelected) || - (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))) - { - editID = id; - isJustEdit = true; - } - } - - return isActivated; - } - - void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut) - { - ImGui::SetItemTooltip("%s\n(Shortcut: %s)", tooltip, shortcut.c_str()); - } - - void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected) - { - auto* set = (std::set*)self->UserData; - if (isSelected) - set->insert(id); - else - set->erase(id); - }; - ImVec2 icon_size_get() { return ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing()); @@ -266,11 +273,11 @@ namespace anm2ed::imgui internal.ApplyRequests(io); } - PopupHelper::PopupHelper(const char* label, float percent, bool isNoHeight) + PopupHelper::PopupHelper(const char* label, PopupType type, PopupPosition position) { this->label = label; - this->percent = percent; - this->isNoHeight = isNoHeight; + this->type = type; + this->position = position; } void PopupHelper::open() @@ -291,11 +298,16 @@ namespace anm2ed::imgui isTriggered = false; auto viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f))); - if (isNoHeight) - ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * percent, 0)); + + if (position == POPUP_CENTER) + ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f))); else - ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * percent)); + ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None); + + if (POPUP_IS_HEIGHT_SET[type]) + ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type])); + else + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0)); } void PopupHelper::end() diff --git a/src/imgui.h b/src/imgui/imgui_.h similarity index 87% rename from src/imgui.h rename to src/imgui/imgui_.h index 7e032ec..0555b4f 100644 --- a/src/imgui.h +++ b/src/imgui/imgui_.h @@ -10,9 +10,41 @@ namespace anm2ed::imgui { - constexpr auto POPUP_TO_CONTENT = 0.0f; - constexpr auto POPUP_SMALL = 0.25f; - constexpr auto POPUP_NORMAL = 0.5f; + constexpr auto DRAG_SPEED = 1.0f; + constexpr auto STEP = 1.0f; + constexpr auto STEP_FAST = 5.0f; + +#define POPUP_LIST \ + X(POPUP_SMALL, 0.25f, true) \ + X(POPUP_NORMAL, 0.5f, true) \ + X(POPUP_TO_CONTENT, 0.0f, true) \ + X(POPUP_SMALL_NO_HEIGHT, 0.25f, false) \ + X(POPUP_NORMAL_NO_HEIGHT, 0.5f, false) + + enum PopupType + { +#define X(name, multiplier, isHeightSet) name, + POPUP_LIST +#undef X + }; + + enum PopupPosition + { + POPUP_CENTER, + POPUP_BY_ITEM + }; + + constexpr float POPUP_MULTIPLIERS[] = { +#define X(name, multiplier, isHeightSet) multiplier, + POPUP_LIST +#undef X + }; + + constexpr bool POPUP_IS_HEIGHT_SET[] = { +#define X(name, multiplier, isHeightSet) isHeightSet, + POPUP_LIST +#undef X + }; const std::unordered_map KEY_MAP = {{"A", ImGuiKey_A}, {"B", ImGuiKey_B}, @@ -134,8 +166,9 @@ namespace anm2ed::imgui ImVec2 child_size_get(int = 1); int input_text_callback(ImGuiInputTextCallbackData*); bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0); - void combo_strings(const std::string&, int*, std::vector&); - void combo_strings(const std::string&, int*, std::vector&); + bool input_int_range(const char*, int&, int, int, int = STEP, int = STEP_FAST, ImGuiInputTextFlags = 0); + bool combo_strings(const std::string&, int*, std::vector&); + bool combo_strings(const std::string&, int*, std::vector&); bool selectable_input_text(const std::string&, const std::string&, std::string&, bool = false, ImGuiSelectableFlags = 0, bool* = nullptr); void set_item_tooltip_shortcut(const char*, const std::string& = {}); @@ -167,13 +200,13 @@ namespace anm2ed::imgui { public: const char* label{}; + PopupType type{}; + PopupPosition position{}; bool isOpen{}; bool isTriggered{}; bool isJustOpened{}; - bool isNoHeight{}; - float percent{}; - PopupHelper(const char*, float = POPUP_NORMAL, bool = false); + PopupHelper(const char*, PopupType = POPUP_NORMAL, PopupPosition = POPUP_CENTER); bool is_open(); void open(); void trigger(); diff --git a/src/taskbar.cpp b/src/imgui/taskbar.cpp similarity index 80% rename from src/taskbar.cpp rename to src/imgui/taskbar.cpp index 04da3b4..e04b0fe 100644 --- a/src/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -3,18 +3,18 @@ #include #include -#include "imgui.h" -#include "math.h" +#include "math_.h" +#include "render.h" +#include "shader.h" +#include "types.h" -using namespace anm2ed::canvas; -using namespace anm2ed::dialog; -using namespace anm2ed::manager; -using namespace anm2ed::resources; -using namespace anm2ed::settings; +using namespace anm2ed::resource; using namespace anm2ed::types; +using namespace anm2ed::canvas; +using namespace anm2ed::util; using namespace glm; -namespace anm2ed::taskbar +namespace anm2ed::imgui { Taskbar::Taskbar() : generate(vec2()) { @@ -30,9 +30,9 @@ namespace anm2ed::taskbar if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New", settings.shortcutNew.c_str())) dialog.anm2_new(); + if (ImGui::MenuItem("New", settings.shortcutNew.c_str())) dialog.file_open(dialog::ANM2_NEW); - if (ImGui::MenuItem("Open", settings.shortcutOpen.c_str())) dialog.anm2_open(); + if (ImGui::MenuItem("Open", settings.shortcutOpen.c_str())) dialog.file_open(dialog::ANM2_NEW); if (manager.recentFiles.empty()) { @@ -63,7 +63,7 @@ namespace anm2ed::taskbar ImGui::BeginDisabled(!document); { if (ImGui::MenuItem("Save", settings.shortcutSave.c_str())) manager.save(); - if (ImGui::MenuItem("Save As", settings.shortcutSaveAs.c_str())) dialog.anm2_save(); + if (ImGui::MenuItem("Save As", settings.shortcutSaveAs.c_str())) dialog.file_save(dialog::ANM2_SAVE); if (ImGui::MenuItem("Explore XML Location")) dialog.file_explorer_open(document->directory_get()); } ImGui::EndDisabled(); @@ -72,19 +72,19 @@ namespace anm2ed::taskbar if (ImGui::MenuItem("Exit", settings.shortcutExit.c_str())) isQuitting = true; ImGui::EndMenu(); } - if (dialog.is_selected_file(dialog::ANM2_NEW)) + if (dialog.is_selected(dialog::ANM2_NEW)) { manager.new_(dialog.path); dialog.reset(); } - if (dialog.is_selected_file(dialog::ANM2_OPEN)) + if (dialog.is_selected(dialog::ANM2_OPEN)) { manager.open(dialog.path); dialog.reset(); } - if (dialog.is_selected_file(dialog::ANM2_SAVE)) + if (dialog.is_selected(dialog::ANM2_SAVE)) { manager.save(dialog.path); dialog.reset(); @@ -92,11 +92,16 @@ namespace anm2ed::taskbar if (ImGui::BeginMenu("Wizard")) { + auto animation = document ? document->animation_get() : nullptr; auto item = document ? document->item_get() : nullptr; ImGui::BeginDisabled(!item || document->reference.itemType != anm2::LAYER); if (ImGui::MenuItem("Generate Animation From Grid")) generatePopup.open(); if (ImGui::MenuItem("Change All Frame Properties")) changePopup.open(); ImGui::EndDisabled(); + ImGui::Separator(); + ImGui::BeginDisabled(!animation); + if (ImGui::MenuItem("Render Animation")) renderPopup.open(); + ImGui::EndDisabled(); ImGui::EndMenu(); } @@ -160,13 +165,13 @@ namespace anm2ed::taskbar ImGui::InputInt2("Start Position", value_ptr(startPosition)); ImGui::InputInt2("Frame Size", value_ptr(size)); ImGui::InputInt2("Pivot", value_ptr(pivot)); - ImGui::InputInt("Rows", &rows, step::NORMAL, step::FAST); - ImGui::InputInt("Columns", &columns, step::NORMAL, step::FAST); + ImGui::InputInt("Rows", &rows, imgui::STEP, imgui::STEP_FAST); + ImGui::InputInt("Columns", &columns, imgui::STEP, imgui::STEP_FAST); - ImGui::InputInt("Count", &count, step::NORMAL, step::FAST); + ImGui::InputInt("Count", &count, imgui::STEP, imgui::STEP_FAST); count = glm::min(count, rows * columns); - ImGui::InputInt("Delay", &delay, step::NORMAL, step::FAST); + ImGui::InputInt("Delay", &delay, imgui::STEP, imgui::STEP_FAST); } ImGui::EndChild(); @@ -176,7 +181,7 @@ namespace anm2ed::taskbar { auto& backgroundColor = settings.previewBackgroundColor; auto& time = generateTime; - auto& shaderTexture = resources.shaders[shader::TEXTURE]; + auto& shaderTexture = resources.shaders[resource::shader::TEXTURE]; auto previewSize = ImVec2(ImGui::GetContentRegionAvail().x, imgui::size_without_footer_get(2).y); @@ -191,7 +196,7 @@ namespace anm2ed::taskbar .spritesheets[document->anm2.content.layers[document->reference.itemID].spritesheetID] .texture; - auto index = std::clamp((int)(time * count), 0, count); + auto index = std::clamp((int)(time * count - 1), 0, count - 1); auto row = index / columns; auto column = index % columns; auto crop = startPosition + ivec2(size.x * column, size.y * row); @@ -212,7 +217,7 @@ namespace anm2ed::taskbar ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); ImGui::InputFloat("##Zoom", &zoom, zoomStep, zoomStep, "%.0f%%"); - zoom = glm::clamp(zoom, canvas::ZOOM_MIN, canvas::ZOOM_MAX); + zoom = glm::clamp(zoom, ZOOM_MIN, ZOOM_MAX); } ImGui::EndChild(); @@ -305,14 +310,14 @@ namespace anm2ed::taskbar auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) { start(checkboxLabel, isEnabled); - ImGui::InputFloat(valueLabel, &value, step::NORMAL, step::FAST, math::float_format_get(value)); + ImGui::InputFloat(valueLabel, &value, imgui::STEP, imgui::STEP_FAST, math::float_format_get(value)); end(); }; auto int_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) { start(checkboxLabel, isEnabled); - ImGui::InputInt(valueLabel, &value, step::NORMAL, step::FAST); + ImGui::InputInt(valueLabel, &value, imgui::STEP, imgui::STEP_FAST); end(); }; @@ -340,7 +345,7 @@ namespace anm2ed::taskbar "off, will use all frames."); ImGui::BeginDisabled(!isFromSelectedFrame); - ImGui::InputInt("Number of Frames", &numberFrames, step::NORMAL, step::FAST); + ImGui::InputInt("Number of Frames", &numberFrames, imgui::STEP, imgui::STEP_FAST); numberFrames = glm::clamp(numberFrames, anm2::FRAME_NUM_MIN, (int)document->item_get()->frames.size() - document->reference.frameIndex); ImGui::SetItemTooltip("Set the number of frames that will be changed."); @@ -350,7 +355,7 @@ namespace anm2ed::taskbar auto widgetSize = imgui::widget_size_with_row_get(4); - auto frame_change = [&](frame_change::Type type) + auto frame_change = [&](anm2::ChangeType type) { anm2::FrameChange frameChange; frameChange.crop = isCrop ? std::make_optional(crop) : std::nullopt; @@ -370,7 +375,7 @@ namespace anm2ed::taskbar if (ImGui::Button("Add", widgetSize)) { - frame_change(frame_change::ADD); + frame_change(anm2::ADD); changePopup.close(); } @@ -378,7 +383,7 @@ namespace anm2ed::taskbar if (ImGui::Button("Subtract", widgetSize)) { - frame_change(frame_change::SUBTRACT); + frame_change(anm2::SUBTRACT); changePopup.close(); } @@ -386,7 +391,7 @@ namespace anm2ed::taskbar if (ImGui::Button("Adjust", widgetSize)) { - frame_change(frame_change::ADJUST); + frame_change(anm2::ADJUST); changePopup.close(); } @@ -415,7 +420,7 @@ namespace anm2ed::taskbar ImGui::SetItemTooltip("Enables autosaving of documents."); ImGui::BeginDisabled(!editSettings.fileIsAutosave); - ImGui::InputInt("Autosave Time (minutes", &editSettings.fileAutosaveTime, step::NORMAL, step::FAST); + ImGui::InputInt("Autosave Time (minutes", &editSettings.fileAutosaveTime, imgui::STEP, imgui::STEP_FAST); editSettings.fileAutosaveTime = glm::clamp(editSettings.fileAutosaveTime, 0, 10); ImGui::SetItemTooltip("If changed, will autosave documents using this interval."); ImGui::EndDisabled(); @@ -521,12 +526,79 @@ namespace anm2ed::taskbar ImGui::SameLine(); - if (ImGui::Button("Close", widgetSize)) ImGui::CloseCurrentPopup(); + if (ImGui::Button("Close", widgetSize)) configurePopup.close(); ImGui::SetItemTooltip("Close without updating settings."); ImGui::EndPopup(); } + renderPopup.trigger(); + + if (ImGui::BeginPopupModal(renderPopup.label, &renderPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto animation = document ? document->animation_get() : nullptr; + if (!animation) renderPopup.close(); + + auto& playback = document->playback; + auto& ffmpegPath = settings.renderFFmpegPath; + auto& path = settings.renderPath; + auto& format = settings.renderFormat; + auto& type = settings.renderType; + auto& start = manager.recordingStart; + auto& end = manager.recordingEnd; + auto& isRange = settings.renderIsRange; + auto widgetSize = imgui::widget_size_with_row_get(2); + auto dialogType = type == render::PNGS ? dialog::PNG_DIRECTORY_SET + : type == render::GIF ? dialog::GIF_PATH_SET + : type == render::WEBM ? dialog::WEBM_PATH_SET + : dialog::NONE; + + if (ImGui::ImageButton("##FFmpeg Path Set", resources.icons[icon::FOLDER].id, imgui::icon_size_get())) + dialog.file_open(dialog::FFMPEG_PATH_SET); + ImGui::SameLine(); + imgui::input_text_string("FFmpeg Path", &ffmpegPath); + dialog.set_string_to_selected_path(ffmpegPath, dialog::FFMPEG_PATH_SET); + + if (ImGui::ImageButton("##Path Set", resources.icons[icon::FOLDER].id, imgui::icon_size_get())) + { + if (dialogType == dialog::PNG_DIRECTORY_SET) + dialog.folder_open(dialogType); + else + dialog.file_open(dialogType); + } + ImGui::SameLine(); + imgui::input_text_string(type == render::PNGS ? "Directory" : "Path", &path); + dialog.set_string_to_selected_path(path, dialogType); + + ImGui::Combo("Type", &type, render::STRINGS, render::COUNT); + + ImGui::BeginDisabled(type != render::PNGS); + imgui::input_text_string("Format", &format); + ImGui::EndDisabled(); + + ImGui::BeginDisabled(!isRange); + imgui::input_int_range("Start", start, 0, animation->frameNum - 1); + ImGui::InputInt("End", &end, start, animation->frameNum); + ImGui::EndDisabled(); + + ImGui::Checkbox("Custom Range", &isRange); + + if (ImGui::Button("Render", widgetSize)) + { + manager.isRecording = true; + playback.time = start; + playback.isPlaying = true; + renderPopup.close(); + manager.progressPopup.open(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) renderPopup.close(); + + ImGui::EndPopup(); + } + aboutPopup.trigger(); if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize)) @@ -535,10 +607,10 @@ namespace anm2ed::taskbar ImGui::EndPopup(); } - if (imgui::shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.anm2_new(); - if (imgui::shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.anm2_open(); + if (imgui::shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_NEW); + if (imgui::shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN); if (imgui::shortcut(settings.shortcutSave, shortcut::GLOBAL)) document->save(); - if (imgui::shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.anm2_save(); + if (imgui::shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE); if (imgui::shortcut(settings.shortcutExit, shortcut::GLOBAL)) isQuitting = true; } } diff --git a/src/imgui/taskbar.h b/src/imgui/taskbar.h new file mode 100644 index 0000000..1785468 --- /dev/null +++ b/src/imgui/taskbar.h @@ -0,0 +1,31 @@ +#pragma once + +#include "canvas.h" +#include "dialog.h" +#include "imgui_.h" +#include "manager.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui +{ + class Taskbar + { + Canvas generate; + float generateTime{}; + PopupHelper generatePopup{PopupHelper("Generate Animation from Grid")}; + PopupHelper changePopup{PopupHelper("Change All Frame Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; + PopupHelper renderPopup{PopupHelper("Render Animation", imgui::POPUP_SMALL_NO_HEIGHT)}; + PopupHelper configurePopup{PopupHelper("Configure")}; + PopupHelper aboutPopup{PopupHelper("About")}; + Settings editSettings{}; + int selectedShortcut{-1}; + bool isQuittingMode{}; + + public: + float height{}; + + Taskbar(); + void update(Manager&, Settings&, Resources&, Dialog&, bool&); + }; +}; diff --git a/src/toast.cpp b/src/imgui/toast.cpp similarity index 97% rename from src/toast.cpp rename to src/imgui/toast.cpp index a0d65c1..8de56ec 100644 --- a/src/toast.cpp +++ b/src/imgui/toast.cpp @@ -5,10 +5,9 @@ #include "types.h" -using namespace anm2ed::log; using namespace anm2ed::types; -namespace anm2ed::toast +namespace anm2ed::imgui { constexpr auto LIFETIME = 3.0f; @@ -82,5 +81,6 @@ namespace anm2ed::toast logger.warning(message); } - Toasts toasts; } + +anm2ed::imgui::Toasts toasts; diff --git a/src/toast.h b/src/imgui/toast.h similarity index 85% rename from src/toast.h rename to src/imgui/toast.h index 6aff2cc..ca33d34 100644 --- a/src/toast.h +++ b/src/imgui/toast.h @@ -3,9 +3,8 @@ #include #include -namespace anm2ed::toast +namespace anm2ed::imgui { - class Toast { public: @@ -26,5 +25,6 @@ namespace anm2ed::toast void warning(const std::string&); }; - extern Toasts toasts; } + +extern anm2ed::imgui::Toasts toasts; diff --git a/src/animation_preview.cpp b/src/imgui/window/animation_preview.cpp similarity index 80% rename from src/animation_preview.cpp rename to src/imgui/window/animation_preview.cpp index 4c523b0..da3ae99 100644 --- a/src/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -1,21 +1,22 @@ #include "animation_preview.h" +#include + #include -#include "imgui.h" -#include "math.h" +#include "log.h" +#include "math_.h" +#include "toast.h" #include "tool.h" #include "types.h" -using namespace anm2ed::manager; -using namespace anm2ed::settings; using namespace anm2ed::canvas; -using namespace anm2ed::playback; -using namespace anm2ed::resources; using namespace anm2ed::types; +using namespace anm2ed::util; +using namespace anm2ed::resource; using namespace glm; -namespace anm2ed::animation_preview +namespace anm2ed::imgui { constexpr auto NULL_COLOR = vec4(0.0f, 0.0f, 1.0f, 0.90f); constexpr auto TARGET_SIZE = vec2(32, 32); @@ -27,6 +28,75 @@ namespace anm2ed::animation_preview { } + void AnimationPreview::tick(Manager& manager, Document& document, Settings& settings) + { + auto& anm2 = document.anm2; + auto& playback = document.playback; + + if (playback.isPlaying) + { + auto& isSound = settings.timelineIsSound; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; + + if (isSound && !anm2.content.sounds.empty()) + if (auto animation = document.animation_get(); animation) + if (animation->triggers.isVisible && !isOnlyShowLayers) + if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible) + anm2.content.sounds[anm2.content.events[trigger.eventID].soundID].audio.play(); + + document.reference.frameTime = playback.time; + } + + if (manager.isRecording) + { + auto pixels = pixels_get(); + renderFrames.push_back(Texture(pixels.data(), size)); + + if (playback.isFinished) + { + auto& ffmpegPath = settings.renderFFmpegPath; + auto& path = settings.renderPath; + auto& type = settings.renderType; + + if (type == render::PNGS) + { + auto& format = settings.renderFormat; + bool isSuccess{true}; + for (auto [i, frame] : std::views::enumerate(renderFrames)) + { + std::filesystem::path outputPath = + std::filesystem::path(path) / std::vformat(format, std::make_format_args(i)); + + if (!frame.write_png(outputPath)) + { + isSuccess = false; + break; + } + logger.info(std::format("Saved frame to PNG: {}", outputPath.string())); + } + + if (isSuccess) + toasts.info(std::format("Exported rendered frames to: {}", path)); + else + toasts.warning(std::format("Could not export frames to: {}", path)); + } + else + { + if (animation_render(ffmpegPath, path, renderFrames, (render::Type)type, size, anm2.info.fps)) + toasts.info(std::format("Exported rendered animation to: {}", path)); + else + toasts.warning(std::format("Could not output rendered animation: {}", path)); + } + + renderFrames.clear(); + playback.isPlaying = false; + playback.isFinished = false; + manager.isRecording = false; + manager.progressPopup.close(); + } + } + } + void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources) { auto& document = *manager.get(); @@ -62,7 +132,6 @@ namespace anm2ed::animation_preview if (ImGui::Begin("Animation Preview", &settings.windowIsAnimationPreview)) { - auto childSize = ImVec2(imgui::row_widget_width_get(4), (ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2)); @@ -110,12 +179,7 @@ namespace anm2ed::animation_preview ImGui::SameLine(); ImGui::ColorEdit4("Color", value_ptr(axesColor), ImGuiColorEditFlags_NoInputs); - std::vector animationNames{}; - animationNames.emplace_back("None"); - for (auto& animation : anm2.animations.items) - animationNames.emplace_back(animation.name); - - imgui::combo_strings("Overlay", &overlayIndex, animationNames); + imgui::combo_strings("Overlay", &overlayIndex, document.animationNamesCStr); ImGui::InputFloat("Alpha", &overlayTransparency, 0, 0, "%.0f"); } @@ -253,6 +317,8 @@ namespace anm2ed::animation_preview for (int i = 1; i <= count; i++) { float useTime = time + (float)(direction * i); + if (useTime < 0.0f || useTime > animation->frameNum) continue; + float alphaOffset = (1.0f / (count + 1)) * i; render(animation, useTime, color, alphaOffset, true); } @@ -272,10 +338,13 @@ namespace anm2ed::animation_preview auto& isEnabled = settings.onionskinIsEnabled; if (drawOrder == draw_order::BELOW && isEnabled) onionskins_render(frameTime); + render(animation, frameTime); + if (overlayIndex > 0) render(document.anm2.animation_get({overlayIndex - 1}), frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency)); + if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime); } @@ -285,7 +354,7 @@ namespace anm2ed::animation_preview isPreviewHovered = ImGui::IsItemHovered(); - if (animation && animation->triggers.isVisible) + if (animation && animation->triggers.isVisible && !isOnlyShowLayers) { if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); trigger.isVisible) { @@ -333,7 +402,7 @@ namespace anm2ed::animation_preview auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); auto frame = document.frame_get(); auto useTool = tool; - auto step = isMod ? step::FAST : step::NORMAL; + auto step = isMod ? canvas::STEP_FAST : canvas::STEP; auto isKeyPressed = isLeftPressed || isRightPressed || isUpPressed || isDownPressed; auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; auto isBegin = isMouseClick || isKeyPressed; @@ -343,6 +412,8 @@ namespace anm2ed::animation_preview if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE; if (tool == tool::SCALE && isMouseRightDown) useTool = tool::MOVE; + ImGui::SetMouseCursor(tool::INFO[useTool].cursor); + switch (useTool) { case tool::PAN: @@ -356,7 +427,7 @@ namespace anm2ed::animation_preview if (isRight) frame->position.x += step; if (isUp) frame->position.y -= step; if (isDown) frame->position.y += step; - if (isEnd) document.change(change::FRAMES); + if (isEnd) document.change(Document::FRAMES); break; case tool::SCALE: if (!frame) break; @@ -366,7 +437,7 @@ namespace anm2ed::animation_preview if (isRight) frame->scale.x += step; if (isUp) frame->scale.y -= step; if (isDown) frame->scale.y += step; - if (isEnd) document.change(change::FRAMES); + if (isEnd) document.change(Document::FRAMES); break; case tool::ROTATE: if (!frame) break; @@ -374,7 +445,7 @@ namespace anm2ed::animation_preview if (isMouseDown) frame->rotation += mouseDelta.y; if (isLeft || isDown) frame->rotation -= step; if (isUp || isRight) frame->rotation += step; - if (isEnd) document.change(change::FRAMES); + if (isEnd) document.change(Document::FRAMES); break; default: break; @@ -385,5 +456,27 @@ namespace anm2ed::animation_preview } } ImGui::End(); + + manager.progressPopup.trigger(); + + if (ImGui::BeginPopupModal(manager.progressPopup.label, &manager.progressPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + if (!animation) return; + + auto& start = manager.recordingStart; + auto& end = manager.recordingEnd; + auto progress = (playback.time - start) / (end - start); + + ImGui::ProgressBar(progress); + + if (ImGui::Button("Cancel", ImVec2(ImGui::GetContentRegionAvail().x, 0))) + { + playback.isPlaying = false; + manager.isRecording = false; + manager.progressPopup.close(); + } + + ImGui::EndPopup(); + } } } diff --git a/src/imgui/window/animation_preview.h b/src/imgui/window/animation_preview.h new file mode 100644 index 0000000..486e174 --- /dev/null +++ b/src/imgui/window/animation_preview.h @@ -0,0 +1,21 @@ +#pragma once + +#include "canvas.h" +#include "manager.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui +{ + class AnimationPreview : public Canvas + { + bool isPreviewHovered{}; + glm::ivec2 mousePos{}; + std::vector renderFrames{}; + + public: + AnimationPreview(); + void tick(Manager&, Document&, Settings&); + void update(Manager&, Settings&, Resources&); + }; +} diff --git a/src/animations.cpp b/src/imgui/window/animations.cpp similarity index 80% rename from src/animations.cpp rename to src/imgui/window/animations.cpp index 30f4ee9..c3ed704 100644 --- a/src/animations.cpp +++ b/src/imgui/window/animations.cpp @@ -2,13 +2,10 @@ #include -using namespace anm2ed::clipboard; -using namespace anm2ed::manager; -using namespace anm2ed::settings; -using namespace anm2ed::resources; +using namespace anm2ed::resource; using namespace anm2ed::types; -namespace anm2ed::animations +namespace anm2ed::imgui { void Animations::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { @@ -24,7 +21,7 @@ namespace anm2ed::animations if (ImGui::Begin("Animations", &settings.windowIsAnimations)) { - auto childSize = imgui::size_without_footer_get(); + auto childSize = size_without_footer_get(); if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders)) { @@ -44,9 +41,8 @@ namespace anm2ed::animations ImGui::PushFont(resources.fonts[font].get(), font::SIZE); ImGui::SetNextItemSelectionUserData(i); - if (imgui::selectable_input_text(animation.name, - std::format("###Document #{} Animation #{}", manager.selected, i), - animation.name, multiSelect.contains(i))) + if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i), + animation.name, multiSelect.contains(i))) document.animation_set(i); if (ImGui::IsItemHovered()) hovered = i; ImGui::PopFont(); @@ -124,9 +120,9 @@ namespace anm2ed::animations document.animations_deserialize(clipboardText); }; - if (imgui::shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); - if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); + if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); + if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); + if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -144,23 +140,23 @@ namespace anm2ed::animations } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(5); + auto widgetSize = widget_size_with_row_get(5); - imgui::shortcut(settings.shortcutAdd); + shortcut(settings.shortcutAdd); if (ImGui::Button("Add", widgetSize)) document.animation_add(); - imgui::set_item_tooltip_shortcut("Add a new animation.", settings.shortcutAdd); + set_item_tooltip_shortcut("Add a new animation.", settings.shortcutAdd); ImGui::SameLine(); ImGui::BeginDisabled(multiSelect.empty()); { - imgui::shortcut(settings.shortcutDuplicate); + shortcut(settings.shortcutDuplicate); if (ImGui::Button("Duplicate", widgetSize)) document.animation_duplicate(); - imgui::set_item_tooltip_shortcut("Duplicate the selected animation(s).", settings.shortcutDuplicate); + set_item_tooltip_shortcut("Duplicate the selected animation(s).", settings.shortcutDuplicate); ImGui::SameLine(); - if (imgui::shortcut(settings.shortcutMerge, shortcut::FOCUSED)) + if (shortcut(settings.shortcutMerge, shortcut::FOCUSED)) if (multiSelect.size() > 0) document.animations_merge_quick(); ImGui::BeginDisabled(multiSelect.size() != 1); @@ -174,24 +170,23 @@ namespace anm2ed::animations } ImGui::EndDisabled(); - imgui::set_item_tooltip_shortcut( - "Open the merge popup.\nUsing the shortcut will merge the animations with\nthe last " - "configured merge settings.", - settings.shortcutMerge); + set_item_tooltip_shortcut("Open the merge popup.\nUsing the shortcut will merge the animations with\nthe last " + "configured merge settings.", + settings.shortcutMerge); ImGui::SameLine(); - imgui::shortcut(settings.shortcutRemove); + shortcut(settings.shortcutRemove); if (ImGui::Button("Remove", widgetSize)) document.animations_remove(); - imgui::set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate); + set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate); ImGui::SameLine(); - imgui::shortcut(settings.shortcutDefault); + shortcut(settings.shortcutDefault); ImGui::BeginDisabled(multiSelect.size() != 1); if (ImGui::Button("Default", widgetSize)) document.animation_default(); ImGui::EndDisabled(); - imgui::set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDefault); + set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDefault); } ImGui::EndDisabled(); @@ -208,9 +203,9 @@ namespace anm2ed::animations auto& type = settings.mergeType; auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter; - auto footerSize = imgui::footer_size_get(); - auto optionsSize = imgui::child_size_get(2); - auto deleteAfterSize = imgui::child_size_get(); + auto footerSize = footer_size_get(); + auto optionsSize = child_size_get(2); + auto deleteAfterSize = child_size_get(); auto animationsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - (optionsSize.y + deleteAfterSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 3)); @@ -259,7 +254,7 @@ namespace anm2ed::animations ImGui::Checkbox("Delete Animations After", &isDeleteAnimationsAfter); ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button("Merge", widgetSize)) { diff --git a/src/imgui/window/animations.h b/src/imgui/window/animations.h new file mode 100644 index 0000000..6257a29 --- /dev/null +++ b/src/imgui/window/animations.h @@ -0,0 +1,17 @@ +#pragma once + +#include "clipboard.h" +#include "manager.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui +{ + class Animations + { + PopupHelper mergePopup{PopupHelper("Merge Animations")}; + + public: + void update(Manager&, Settings&, Resources&, Clipboard&); + }; +} diff --git a/src/imgui/window/events.cpp b/src/imgui/window/events.cpp new file mode 100644 index 0000000..fa3da45 --- /dev/null +++ b/src/imgui/window/events.cpp @@ -0,0 +1,160 @@ +#include "events.h" + +#include + +using namespace anm2ed::resource; +using namespace anm2ed::types; + +namespace anm2ed::imgui +{ + void Events::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) + { + auto& document = *manager.get(); + auto& anm2 = document.anm2; + auto& unused = document.unusedEventIDs; + auto& hovered = document.hoveredEvent; + auto& reference = document.referenceEvent; + auto& multiSelect = document.eventMultiSelect; + + hovered = -1; + + if (ImGui::Begin("Events", &settings.windowIsEvents)) + { + auto childSize = size_without_footer_get(); + + if (ImGui::BeginChild("##Events Child", childSize, true)) + { + multiSelect.start(anm2.content.events.size()); + + for (auto& [id, event] : anm2.content.events) + { + ImGui::PushID(id); + ImGui::SetNextItemSelectionUserData(id); + ImGui::Selectable(event.name.c_str(), multiSelect.contains(id)); + if (ImGui::IsItemHovered()) + { + hovered = id; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + reference = id; + editEvent = document.anm2.content.events[reference]; + propertiesPopup.open(); + } + } + + if (ImGui::BeginItemTooltip()) + { + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); + ImGui::TextUnformatted(event.name.c_str()); + ImGui::PopFont(); + ImGui::EndTooltip(); + } + ImGui::PopID(); + } + + multiSelect.finish(); + + auto copy = [&]() + { + if (!multiSelect.empty()) + { + std::string clipboardText{}; + for (auto& id : multiSelect) + clipboardText += anm2.content.events[id].to_string(id); + clipboard.set(clipboardText); + } + else if (hovered > -1) + clipboard.set(anm2.content.events[hovered].to_string(hovered)); + }; + + auto paste = [&](merge::Type type) + { + auto clipboardText = clipboard.get(); + document.events_deserialize(clipboardText, type); + }; + + if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); + if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + + if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) + { + ImGui::BeginDisabled(); + ImGui::MenuItem("Cut", settings.shortcutCut.c_str()); + ImGui::EndDisabled(); + + ImGui::BeginDisabled(multiSelect.empty() && hovered == -1); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); + ImGui::EndDisabled(); + + ImGui::BeginDisabled(clipboard.is_empty()); + { + if (ImGui::BeginMenu("Paste")) + { + if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); + if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); + + ImGui::EndMenu(); + } + } + ImGui::EndDisabled(); + + ImGui::EndPopup(); + } + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + shortcut(settings.shortcutAdd); + if (ImGui::Button("Add", widgetSize)) + { + reference = -1; + editEvent = anm2::Event(); + propertiesPopup.open(); + } + set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd); + ImGui::SameLine(); + + shortcut(settings.shortcutRemove); + ImGui::BeginDisabled(unused.empty()); + if (ImGui::Button("Remove Unused", widgetSize)) document.events_remove_unused(); + ImGui::EndDisabled(); + set_item_tooltip_shortcut("Remove unused events (i.e., ones not used by any trigger in any animation.)", + settings.shortcutRemove); + } + ImGui::End(); + + propertiesPopup.trigger(); + + if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto childSize = child_size_get(2); + auto& event = editEvent; + + if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders)) + { + if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); + input_text_string("Name", &event.name); + ImGui::SetItemTooltip("Set the event's name."); + combo_strings("Sound", &event.soundID, document.soundNames); + ImGui::SetItemTooltip("Set the event sound; it will play when a trigger associated with this event activates."); + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize)) + { + document.event_set(event); + propertiesPopup.close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) propertiesPopup.close(); + + propertiesPopup.end(); + ImGui::EndPopup(); + } + } +} diff --git a/src/imgui/window/events.h b/src/imgui/window/events.h new file mode 100644 index 0000000..25a3705 --- /dev/null +++ b/src/imgui/window/events.h @@ -0,0 +1,18 @@ +#pragma once + +#include "clipboard.h" +#include "manager.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui +{ + class Events + { + anm2::Event editEvent{}; + PopupHelper propertiesPopup{PopupHelper("Event Properties", POPUP_SMALL_NO_HEIGHT)}; + + public: + void update(Manager&, Settings&, Resources&, Clipboard&); + }; +} diff --git a/src/imgui/window/frame_properties.cpp b/src/imgui/window/frame_properties.cpp new file mode 100644 index 0000000..f83b002 --- /dev/null +++ b/src/imgui/window/frame_properties.cpp @@ -0,0 +1,125 @@ +#include "frame_properties.h" + +#include + +#include + +#include "math_.h" +#include "types.h" + +using namespace anm2ed::util::math; +using namespace anm2ed::types; +using namespace glm; + +namespace anm2ed::imgui +{ + void FrameProperties::update(Manager& manager, Settings& settings) + { + if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties)) + { + auto& document = *manager.get(); + auto& anm2 = document.anm2; + auto& reference = document.reference; + auto& type = reference.itemType; + auto frame = document.frame_get(); + auto useFrame = frame ? *frame : anm2::Frame(); + + ImGui::BeginDisabled(!frame); + { + if (type == anm2::TRIGGER) + { + std::vector eventNames{}; + for (auto& event : anm2.content.events | std::views::values) + eventNames.emplace_back(event.name); + + if (imgui::combo_strings("Event", frame ? &useFrame.eventID : &dummy_value(), eventNames)) + DOCUMENT_EDIT(document, "Trigger Event", Document::FRAMES, frame->eventID = useFrame.eventID); + ImGui::SetItemTooltip("Change the event this trigger uses."); + if (ImGui::InputInt("At Frame", frame ? &useFrame.atFrame : &dummy_value(), imgui::STEP, + imgui::STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) + DOCUMENT_EDIT(document, "Trigger At Frame", Document::FRAMES, frame->atFrame = useFrame.atFrame); + ImGui::SetItemTooltip("Change the frame the trigger will be activated at."); + } + else + { + ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); + { + if (ImGui::InputFloat2("Crop", frame ? value_ptr(useFrame.crop) : &dummy_value(), + frame ? vec2_format_get(useFrame.crop) : "")) + DOCUMENT_EDIT(document, "Frame Crop", Document::FRAMES, frame->crop = useFrame.crop); + ImGui::SetItemTooltip("Change the crop position the frame uses."); + + if (ImGui::InputFloat2("Size", frame ? value_ptr(useFrame.size) : &dummy_value(), + frame ? vec2_format_get(useFrame.size) : "")) + DOCUMENT_EDIT(document, "Frame Size", Document::FRAMES, frame->size = useFrame.size); + ImGui::SetItemTooltip("Change the size of the crop the frame uses."); + } + ImGui::EndDisabled(); + + if (ImGui::InputFloat2("Position", frame ? value_ptr(useFrame.position) : &dummy_value(), + frame ? vec2_format_get(useFrame.position) : "")) + DOCUMENT_EDIT(document, "Frame Position", Document::FRAMES, frame->position = useFrame.position); + ImGui::SetItemTooltip("Change the position of the frame."); + + ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); + { + if (ImGui::InputFloat2("Pivot", frame ? value_ptr(useFrame.pivot) : &dummy_value(), + frame ? vec2_format_get(useFrame.pivot) : "")) + DOCUMENT_EDIT(document, "Frame Pivot", Document::FRAMES, frame->pivot = useFrame.pivot); + ImGui::SetItemTooltip("Change the pivot of the frame; i.e., where it is centered."); + } + ImGui::EndDisabled(); + + if (ImGui::InputFloat2("Scale", frame ? value_ptr(useFrame.scale) : &dummy_value(), + frame ? vec2_format_get(useFrame.scale) : "")) + DOCUMENT_EDIT(document, "Frame Scale", Document::FRAMES, frame->scale = useFrame.scale); + ImGui::SetItemTooltip("Change the scale of the frame, in percent."); + + if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value(), imgui::STEP, + imgui::STEP_FAST, frame ? float_format_get(useFrame.rotation) : "")) + DOCUMENT_EDIT(document, "Frame Rotation", Document::FRAMES, frame->rotation = useFrame.rotation); + ImGui::SetItemTooltip("Change the rotation of the frame."); + + if (ImGui::InputInt("Duration", frame ? &useFrame.delay : &dummy_value(), imgui::STEP, imgui::STEP_FAST, + !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) + DOCUMENT_EDIT(document, "Frame Duration", Document::FRAMES, frame->delay = useFrame.delay); + ImGui::SetItemTooltip("Change how long the frame lasts."); + + if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Tint", Document::FRAMES, frame->tint = useFrame.tint); + ImGui::SetItemTooltip("Change the tint of the frame."); + + if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Color Offset", Document::FRAMES, frame->colorOffset = useFrame.colorOffset); + ImGui::SetItemTooltip("Change the color added onto the frame."); + + if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Visibility", Document::FRAMES, frame->isVisible = useFrame.isVisible); + ImGui::SetItemTooltip("Toggle the frame's visibility."); + + ImGui::SameLine(); + + if (ImGui::Checkbox("Interpolated", frame ? &useFrame.isInterpolated : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, + frame->isInterpolated = useFrame.isInterpolated); + ImGui::SetItemTooltip( + "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time."); + + auto widgetSize = imgui::widget_size_with_row_get(2); + + if (ImGui::Button("Flip X", widgetSize)) + DOCUMENT_EDIT(document, "Frame Flip X", Document::FRAMES, frame->scale.x = -frame->scale.x); + ImGui::SetItemTooltip("%s", "Flip the horizontal scale of the frame, to cheat mirroring the frame " + "horizontally.\n(Note: the format does not support mirroring.)"); + ImGui::SameLine(); + if (ImGui::Button("Flip Y", widgetSize)) + DOCUMENT_EDIT(document, "Frame Flip Y", Document::FRAMES, frame->scale.y = -frame->scale.y); + ImGui::SetItemTooltip("%s", "Flip the vertical scale of the frame, to cheat mirroring the frame " + "vertically.\n(Note: the format does not support mirroring.)"); + } + } + ImGui::EndDisabled(); + } + ImGui::End(); + } +} \ No newline at end of file diff --git a/src/frame_properties.h b/src/imgui/window/frame_properties.h similarity index 53% rename from src/frame_properties.h rename to src/imgui/window/frame_properties.h index ab092ee..d8cfd86 100644 --- a/src/frame_properties.h +++ b/src/imgui/window/frame_properties.h @@ -3,11 +3,11 @@ #include "manager.h" #include "settings.h" -namespace anm2ed::frame_properties +namespace anm2ed::imgui { class FrameProperties { public: - void update(manager::Manager&, settings::Settings&); + void update(Manager&, Settings&); }; } diff --git a/src/layers.cpp b/src/imgui/window/layers.cpp similarity index 79% rename from src/layers.cpp rename to src/imgui/window/layers.cpp index e7ae3a3..1b12e34 100644 --- a/src/layers.cpp +++ b/src/imgui/window/layers.cpp @@ -2,14 +2,10 @@ #include -using namespace anm2ed::document; -using namespace anm2ed::clipboard; -using namespace anm2ed::manager; -using namespace anm2ed::resources; -using namespace anm2ed::settings; +using namespace anm2ed::resource; using namespace anm2ed::types; -namespace anm2ed::layers +namespace anm2ed::imgui { void Layers::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { @@ -25,7 +21,7 @@ namespace anm2ed::layers if (ImGui::Begin("Layers", &settings.windowIsLayers)) { - auto childSize = imgui::size_without_footer_get(); + auto childSize = size_without_footer_get(); if (ImGui::BeginChild("##Layers Child", childSize, true)) { @@ -80,8 +76,8 @@ namespace anm2ed::layers document.layers_deserialize(clipboardText, type); }; - if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); + if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -110,19 +106,19 @@ namespace anm2ed::layers } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); - imgui::shortcut(settings.shortcutAdd); + shortcut(settings.shortcutAdd); if (ImGui::Button("Add", widgetSize)) manager.layer_properties_open(); - imgui::set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd); + set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd); ImGui::SameLine(); - imgui::shortcut(settings.shortcutRemove); + shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unused.empty()); if (ImGui::Button("Remove Unused", widgetSize)) document.layers_remove_unused(); ImGui::EndDisabled(); - imgui::set_item_tooltip_shortcut("Remove unused layers (i.e., ones not used in any animation.)", - settings.shortcutRemove); + set_item_tooltip_shortcut("Remove unused layers (i.e., ones not used in any animation.)", + settings.shortcutRemove); } ImGui::End(); @@ -130,20 +126,20 @@ namespace anm2ed::layers if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto childSize = imgui::child_size_get(2); + auto childSize = child_size_get(2); auto& layer = manager.editLayer; if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders)) { if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); - imgui::input_text_string("Name", &layer.name); + input_text_string("Name", &layer.name); ImGui::SetItemTooltip("Set the item's name."); - imgui::combo_strings("Spritesheet", &layer.spritesheetID, document.spritesheetNames); + combo_strings("Spritesheet", &layer.spritesheetID, document.spritesheetNames); ImGui::SetItemTooltip("Set the layer item's spritesheet."); } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize)) { diff --git a/src/layers.h b/src/imgui/window/layers.h similarity index 52% rename from src/layers.h rename to src/imgui/window/layers.h index e72c331..2431571 100644 --- a/src/layers.h +++ b/src/imgui/window/layers.h @@ -5,11 +5,11 @@ #include "resources.h" #include "settings.h" -namespace anm2ed::layers +namespace anm2ed::imgui { class Layers { public: - void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); + void update(Manager&, Settings&, Resources&, Clipboard&); }; } diff --git a/src/nulls.cpp b/src/imgui/window/nulls.cpp similarity index 82% rename from src/nulls.cpp rename to src/imgui/window/nulls.cpp index 42879e1..302674c 100644 --- a/src/nulls.cpp +++ b/src/imgui/window/nulls.cpp @@ -2,13 +2,10 @@ #include -using namespace anm2ed::clipboard; -using namespace anm2ed::manager; -using namespace anm2ed::settings; -using namespace anm2ed::resources; +using namespace anm2ed::resource; using namespace anm2ed::types; -namespace anm2ed::nulls +namespace anm2ed::imgui { void Nulls::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { @@ -24,7 +21,7 @@ namespace anm2ed::nulls if (ImGui::Begin("Nulls", &settings.windowIsNulls)) { - auto childSize = imgui::size_without_footer_get(); + auto childSize = size_without_footer_get(); if (ImGui::BeginChild("##Nulls Child", childSize, true)) { @@ -79,8 +76,8 @@ namespace anm2ed::nulls document.nulls_deserialize(clipboardText, type); }; - if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); + if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -109,19 +106,18 @@ namespace anm2ed::nulls } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); - imgui::shortcut(settings.shortcutAdd); + shortcut(settings.shortcutAdd); if (ImGui::Button("Add", widgetSize)) manager.null_properties_open(); - imgui::set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd); + set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd); ImGui::SameLine(); - imgui::shortcut(settings.shortcutRemove); + shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unused.empty()); if (ImGui::Button("Remove Unused", widgetSize)) document.nulls_remove_unused(); ImGui::EndDisabled(); - imgui::set_item_tooltip_shortcut("Remove unused nulls (i.e., ones not used in any animation.)", - settings.shortcutRemove); + set_item_tooltip_shortcut("Remove unused nulls (i.e., ones not used in any animation.)", settings.shortcutRemove); } ImGui::End(); @@ -129,13 +125,13 @@ namespace anm2ed::nulls if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto childSize = imgui::child_size_get(2); + auto childSize = child_size_get(2); auto& null = manager.editNull; if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders)) { if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); - imgui::input_text_string("Name", &null.name); + input_text_string("Name", &null.name); ImGui::SetItemTooltip("Set the null's name."); ImGui::Checkbox("Rect", &null.isShowRect); @@ -143,7 +139,7 @@ namespace anm2ed::nulls } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize)) { diff --git a/src/nulls.h b/src/imgui/window/nulls.h similarity index 52% rename from src/nulls.h rename to src/imgui/window/nulls.h index 1eba1bb..7ffd1fd 100644 --- a/src/nulls.h +++ b/src/imgui/window/nulls.h @@ -5,11 +5,11 @@ #include "resources.h" #include "settings.h" -namespace anm2ed::nulls +namespace anm2ed::imgui { class Nulls { public: - void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); + void update(Manager&, Settings&, Resources&, Clipboard&); }; } \ No newline at end of file diff --git a/src/imgui/window/onionskin.cpp b/src/imgui/window/onionskin.cpp new file mode 100644 index 0000000..793d5b1 --- /dev/null +++ b/src/imgui/window/onionskin.cpp @@ -0,0 +1,55 @@ +#include "onionskin.h" + +#include + +#include "imgui_.h" + +using namespace anm2ed::types; +using namespace glm; + +namespace anm2ed::imgui +{ + constexpr auto FRAMES_MAX = 100; + + void Onionskin::update(Settings& settings) + { + auto& isEnabled = settings.onionskinIsEnabled; + auto& beforeCount = settings.onionskinBeforeCount; + auto& beforeColor = settings.onionskinBeforeColor; + auto& afterCount = settings.onionskinAfterCount; + auto& afterColor = settings.onionskinAfterColor; + auto& drawOrder = settings.onionskinDrawOrder; + + if (ImGui::Begin("Onionskin", &settings.windowIsOnionskin)) + { + auto configure_widgets = [&](const char* separator, int& frames, vec3& color) + { + ImGui::PushID(separator); + ImGui::SeparatorText(separator); + input_int_range("Frames", frames, 0, FRAMES_MAX); + ImGui::SetItemTooltip("Change the amount of frames this onionskin will use."); + ImGui::ColorEdit3("Color", value_ptr(color)); + ImGui::SetItemTooltip("Change the color of the frames this onionskin will use."); + ImGui::PopID(); + }; + + ImGui::Checkbox("Enabled", &isEnabled); + set_item_tooltip_shortcut("Toggle onionskinning.", settings.shortcutOnionskin); + + configure_widgets("Before", beforeCount, beforeColor); + configure_widgets("After", afterCount, afterColor); + + ImGui::Text("Draw Order"); + ImGui::SameLine(); + ImGui::RadioButton("Below", &drawOrder, draw_order::BELOW); + ImGui::SetItemTooltip("The onionskin frames will draw below the original frames."); + ImGui::SameLine(); + ImGui::RadioButton("Above", &drawOrder, draw_order::ABOVE); + ImGui::SetItemTooltip("The onionskin frames will draw above the original frames."); + } + ImGui::End(); + + if (shortcut(settings.shortcutOnionskin, shortcut::GLOBAL)) isEnabled = !isEnabled; + } + +} diff --git a/src/onionskin.h b/src/imgui/window/onionskin.h similarity index 54% rename from src/onionskin.h rename to src/imgui/window/onionskin.h index 43f9358..6b23aed 100644 --- a/src/onionskin.h +++ b/src/imgui/window/onionskin.h @@ -2,11 +2,11 @@ #include "settings.h" -namespace anm2ed::onionskin +namespace anm2ed::imgui { class Onionskin { public: - void update(settings::Settings&); + void update(Settings&); }; } diff --git a/src/events.cpp b/src/imgui/window/sounds.cpp similarity index 54% rename from src/events.cpp rename to src/imgui/window/sounds.cpp index cc8a417..c567d62 100644 --- a/src/events.cpp +++ b/src/imgui/window/sounds.cpp @@ -1,47 +1,58 @@ -#include "events.h" +#include "sounds.h" #include -using namespace anm2ed::clipboard; -using namespace anm2ed::manager; -using namespace anm2ed::settings; -using namespace anm2ed::resources; +using namespace anm2ed::dialog; using namespace anm2ed::types; +using namespace anm2ed::resource; -namespace anm2ed::events +namespace anm2ed::imgui { - void Events::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) + void Sounds::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, Clipboard& clipboard) { auto& document = *manager.get(); auto& anm2 = document.anm2; - auto& unused = document.unusedEventIDs; - auto& hovered = document.hoveredEvent; - auto& multiSelect = document.eventMultiSelect; + auto& reference = document.referenceNull; + auto& unused = document.unusedNullIDs; + auto& hovered = document.hoveredNull; + auto& multiSelect = document.soundMultiSelect; hovered = -1; - if (ImGui::Begin("Events", &settings.windowIsEvents)) + if (ImGui::Begin("Sounds", &settings.windowIsSounds)) { auto childSize = imgui::size_without_footer_get(); - bool isRenamed{}; - if (ImGui::BeginChild("##Events Child", childSize, true)) + if (ImGui::BeginChild("##Sounds Child", childSize, true)) { - multiSelect.start(anm2.content.events.size()); + multiSelect.start(anm2.content.sounds.size()); - for (auto& [id, event] : anm2.content.events) + for (auto& [id, sound] : anm2.content.sounds) { + auto isSelected = multiSelect.contains(id); + auto isReferenced = reference == id; + ImGui::PushID(id); ImGui::SetNextItemSelectionUserData(id); - if (imgui::selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id), - event.name, multiSelect.contains(id), 0, &isRenamed)) - if (ImGui::IsItemHovered()) hovered = id; - if (isRenamed) document.change(change::EVENTS); + if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); + if (ImGui::Selectable(std::format(anm2::SOUND_FORMAT, id, sound.path.string()).c_str(), isSelected)) + sound.audio.play(); + if (ImGui::IsItemHovered()) + { + hovered = id; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + ; + } + + if (isReferenced) ImGui::PopFont(); + if (ImGui::BeginItemTooltip()) { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(event.name.c_str()); + ImGui::TextUnformatted(sound.path.c_str()); ImGui::PopFont(); + ImGui::Text("ID: %d", id); + ImGui::Text("Click to play."); ImGui::EndTooltip(); } ImGui::PopID(); @@ -55,17 +66,17 @@ namespace anm2ed::events { std::string clipboardText{}; for (auto& id : multiSelect) - clipboardText += anm2.content.events[id].to_string(id); + clipboardText += anm2.content.sounds[id].to_string(id); clipboard.set(clipboardText); } else if (hovered > -1) - clipboard.set(anm2.content.events[hovered].to_string(hovered)); + clipboard.set(anm2.content.sounds[hovered].to_string(hovered)); }; auto paste = [&](merge::Type type) { auto clipboardText = clipboard.get(); - document.events_deserialize(clipboardText, type); + document.sounds_deserialize(clipboardText, type); }; if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); @@ -101,17 +112,24 @@ namespace anm2ed::events auto widgetSize = imgui::widget_size_with_row_get(2); imgui::shortcut(settings.shortcutAdd); - if (ImGui::Button("Add", widgetSize)) document.event_add(); - imgui::set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd); + if (ImGui::Button("Add", widgetSize)) dialog.file_open(dialog::SOUND_OPEN); + imgui::set_item_tooltip_shortcut("Add a sound.", settings.shortcutAdd); ImGui::SameLine(); imgui::shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unused.empty()); - if (ImGui::Button("Remove Unused", widgetSize)) document.events_remove_unused(); + if (ImGui::Button("Remove Unused", widgetSize)) + ; ImGui::EndDisabled(); - imgui::set_item_tooltip_shortcut("Remove unused events (i.e., ones not used by any trigger in any animation.)", + imgui::set_item_tooltip_shortcut("Remove unused sounds (i.e., ones not used in any trigger.)", settings.shortcutRemove); } ImGui::End(); + + if (dialog.is_selected(dialog::SOUND_OPEN)) + { + document.sound_add(dialog.path); + dialog.reset(); + } } -} +} \ No newline at end of file diff --git a/src/imgui/window/sounds.h b/src/imgui/window/sounds.h new file mode 100644 index 0000000..c39efa6 --- /dev/null +++ b/src/imgui/window/sounds.h @@ -0,0 +1,16 @@ +#pragma once + +#include "clipboard.h" +#include "dialog.h" +#include "manager.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui +{ + class Sounds + { + public: + void update(Manager&, Settings&, Resources&, Dialog&, Clipboard&); + }; +} diff --git a/src/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp similarity index 96% rename from src/spritesheet_editor.cpp rename to src/imgui/window/spritesheet_editor.cpp index 5861fbd..8a0f9de 100644 --- a/src/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -1,18 +1,16 @@ #include "spritesheet_editor.h" -#include "imgui.h" -#include "math.h" +#include "math_.h" #include "tool.h" #include "types.h" -using namespace anm2ed::manager; -using namespace anm2ed::settings; using namespace anm2ed::canvas; -using namespace anm2ed::resources; using namespace anm2ed::types; +using namespace anm2ed::resource; +using namespace anm2ed::util; using namespace glm; -namespace anm2ed::spritesheet_editor +namespace anm2ed::imgui { constexpr auto PIVOT_COLOR = color::PINK; @@ -167,6 +165,8 @@ namespace anm2ed::spritesheet_editor if (isMouseMiddleDown) useTool = tool::PAN; + ImGui::SetMouseCursor(tool::INFO[useTool].cursor); + switch (useTool) { case tool::PAN: @@ -180,7 +180,7 @@ namespace anm2ed::spritesheet_editor if (isRight) frame->pivot.x += step; if (isUp) frame->pivot.y -= step; if (isDown) frame->pivot.y += step; - if (isEnd) document.change(change::FRAMES); + if (isEnd) document.change(Document::FRAMES); break; case tool::CROP: if (!frame) break; @@ -191,7 +191,7 @@ namespace anm2ed::spritesheet_editor if (isRight) isMod ? frame->size.x += step : frame->crop.x += step; if (isUp) isMod ? frame->size.y -= step : frame->crop.y -= step; if (isDown) isMod ? frame->size.y += step : frame->crop.y += step; - if (isEnd) document.change(change::FRAMES); + if (isEnd) document.change(Document::FRAMES); break; case tool::DRAW: case tool::ERASE: @@ -200,7 +200,7 @@ namespace anm2ed::spritesheet_editor auto color = tool == tool::DRAW ? toolColor : vec4(); if (isMouseClicked) document.snapshot(tool == tool::DRAW ? "Draw" : "Erase"); if (isMouseDown) spritesheet->texture.pixel_line(ivec2(previousMousePos), ivec2(mousePos), color); - if (isMouseReleased) document.change(change::FRAMES); + if (isMouseReleased) document.change(Document::FRAMES); break; } case tool::COLOR_PICKER: diff --git a/src/spritesheet_editor.h b/src/imgui/window/spritesheet_editor.h similarity index 55% rename from src/spritesheet_editor.h rename to src/imgui/window/spritesheet_editor.h index 13fdb13..55db23d 100644 --- a/src/spritesheet_editor.h +++ b/src/imgui/window/spritesheet_editor.h @@ -5,15 +5,15 @@ #include "resources.h" #include "settings.h" -namespace anm2ed::spritesheet_editor +namespace anm2ed::imgui { - class SpritesheetEditor : public canvas::Canvas + class SpritesheetEditor : public Canvas { glm::vec2 mousePos{}; glm::vec2 previousMousePos{}; public: SpritesheetEditor(); - void update(manager::Manager&, settings::Settings&, resources::Resources&); + void update(Manager&, Settings&, Resources&); }; } diff --git a/src/spritesheets.cpp b/src/imgui/window/spritesheets.cpp similarity index 90% rename from src/spritesheets.cpp rename to src/imgui/window/spritesheets.cpp index ea8c78f..2d22b13 100644 --- a/src/spritesheets.cpp +++ b/src/imgui/window/spritesheets.cpp @@ -2,21 +2,13 @@ #include -#include "imgui.h" #include "toast.h" -using namespace anm2ed::anm2; -using namespace anm2ed::clipboard; -using namespace anm2ed::manager; -using namespace anm2ed::settings; -using namespace anm2ed::resources; -using namespace anm2ed::dialog; -using namespace anm2ed::document; using namespace anm2ed::types; -using namespace anm2ed::toast; +using namespace anm2ed::resource; using namespace glm; -namespace anm2ed::spritesheets +namespace anm2ed::imgui { void Spritesheets::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, Clipboard& clipboard) @@ -175,7 +167,7 @@ namespace anm2ed::spritesheets spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); - ImGui::Text(SPRITESHEET_FORMAT, id, path); + ImGui::Text(anm2::SPRITESHEET_FORMAT_C, id, path); if (isReferenced) ImGui::PopFont(); context_menu(); @@ -196,10 +188,10 @@ namespace anm2ed::spritesheets auto rowOneWidgetSize = imgui::widget_size_with_row_get(4); imgui::shortcut(settings.shortcutAdd); - if (ImGui::Button("Add", rowOneWidgetSize)) dialog.spritesheet_open(); + if (ImGui::Button("Add", rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_OPEN); imgui::set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd); - if (dialog.is_selected_file(dialog::SPRITESHEET_OPEN)) + if (dialog.is_selected(dialog::SPRITESHEET_OPEN)) { document.spritesheet_add(dialog.path); dialog.reset(); @@ -213,7 +205,7 @@ namespace anm2ed::spritesheets { for (auto& id : multiSelect) { - Spritesheet& spritesheet = anm2.content.spritesheets[id]; + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; spritesheet.reload(document.directory_get()); toasts.info(std::format("Reloaded spritesheet #{}: {}", id, spritesheet.path.string())); } @@ -226,16 +218,16 @@ namespace anm2ed::spritesheets ImGui::BeginDisabled(multiSelect.size() != 1); { - if (ImGui::Button("Replace", rowOneWidgetSize)) dialog.spritesheet_replace(); + if (ImGui::Button("Replace", rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_REPLACE); ImGui::SetItemTooltip("Replace the selected spritesheet with a new one."); } ImGui::EndDisabled(); - if (dialog.is_selected_file(dialog::SPRITESHEET_REPLACE)) + if (dialog.is_selected(dialog::SPRITESHEET_REPLACE)) { auto& id = *multiSelect.begin(); - Spritesheet& spritesheet = anm2.content.spritesheets[id]; - spritesheet = Spritesheet(document.directory_get(), dialog.path); + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; + spritesheet = anm2::Spritesheet(document.directory_get(), dialog.path); toasts.info(std::format("Replaced spritesheet #{}: {}", id, spritesheet.path.string())); dialog.reset(); } @@ -249,12 +241,12 @@ namespace anm2ed::spritesheets { for (auto& id : unused) { - Spritesheet& spritesheet = anm2.content.spritesheets[id]; + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; toasts.info(std::format("Removed spritesheet #{}: {}", id, spritesheet.path.string())); anm2.spritesheet_remove(id); } unused.clear(); - document.change(change::SPRITESHEETS); + document.change(Document::SPRITESHEETS); } imgui::set_item_tooltip_shortcut("Remove all unused spritesheets (i.e., not used in any layer.).", settings.shortcutRemove); @@ -289,7 +281,7 @@ namespace anm2ed::spritesheets { for (auto& id : multiSelect) { - Spritesheet& spritesheet = anm2.content.spritesheets[id]; + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; if (spritesheet.save(document.directory_get())) toasts.info(std::format("Saved spritesheet #{}: {}", id, spritesheet.path.string())); else diff --git a/src/imgui/window/spritesheets.h b/src/imgui/window/spritesheets.h new file mode 100644 index 0000000..f012e8b --- /dev/null +++ b/src/imgui/window/spritesheets.h @@ -0,0 +1,16 @@ +#pragma once + +#include "clipboard.h" +#include "dialog.h" +#include "manager.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui +{ + class Spritesheets + { + public: + void update(Manager&, Settings&, Resources&, Dialog&, Clipboard& clipboard); + }; +} diff --git a/src/timeline.cpp b/src/imgui/window/timeline.cpp similarity index 96% rename from src/timeline.cpp rename to src/imgui/window/timeline.cpp index dfdfeb0..f845b1a 100644 --- a/src/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -4,17 +4,11 @@ #include -#include "imgui.h" - +using namespace anm2ed::resource; using namespace anm2ed::types; -using namespace anm2ed::manager; -using namespace anm2ed::resources; -using namespace anm2ed::settings; -using namespace anm2ed::playback; -using namespace anm2ed::clipboard; using namespace glm; -namespace anm2ed::timeline +namespace anm2ed::imgui { constexpr auto ROOT_COLOR = ImVec4(0.140f, 0.310f, 0.560f, 1.000f); constexpr auto ROOT_COLOR_ACTIVE = ImVec4(0.240f, 0.520f, 0.880f, 1.000f); @@ -60,20 +54,16 @@ namespace anm2ed::timeline auto copy = [&]() { - if (auto frame = document.anm2.frame_get(hoveredFrame)) clipboard.set(frame->to_string(hoveredFrame.itemType)); + if (auto frame = document.anm2.frame_get(hoveredFrame)) + { + clipboard.set(frame->to_string(hoveredFrame.itemType)); + } }; auto cut = [&]() { - if (auto frame = document.anm2.frame_get(hoveredFrame)) - { - if (auto item = document.anm2.item_get(hoveredFrame)) - { - clipboard.set(frame->to_string(hoveredFrame.itemType)); - document.frames_delete(item); - hoveredFrame = anm2::REFERENCE_DEFAULT; - } - } + copy(); + document.frames_delete(document.item_get()); }; auto paste = [&]() { document.frames_deserialize(clipboard.get()); }; @@ -104,9 +94,10 @@ namespace anm2ed::timeline { auto& anm2 = document.anm2; auto& reference = document.reference; + auto item = animation ? animation->item_get(type, id) : nullptr; - auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto isVisible = item ? item->isVisible : false; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; auto isActive = reference.itemType == type && reference.itemID == id; std::string label = "##None"; @@ -179,6 +170,7 @@ namespace anm2ed::timeline ImGui::TextUnformatted(label.c_str()); anm2::Item* item = animation->item_get(type, id); + bool& isVisible = item->isVisible; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); @@ -187,7 +179,7 @@ namespace anm2ed::timeline ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - int visibleIcon = item->isVisible ? icon::VISIBLE : icon::INVISIBLE; + int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE; if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, imgui::icon_size_get())) document.item_visible_toggle(item); @@ -214,31 +206,33 @@ namespace anm2ed::timeline else { auto cursorPos = ImGui::GetCursorPos(); + auto& isShowUnused = settings.timelineIsShowUnused; + + ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); - ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, - (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - auto& isShowUnused = settings.timelineIsShowUnused; auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, imgui::icon_size_get())) isShowUnused = !isShowUnused; ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide." : "Unused layers/nulls are hidden. Press to show."); - auto onlyShowLayersIcon = isOnlyShowLayers ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; + auto layersIcon = isOnlyShowLayers ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; + ImGui::SetCursorPos( ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - if (ImGui::ImageButton("##Layers Visibility Toggle", resources.icons[onlyShowLayersIcon].id, - imgui::icon_size_get())) + + if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, imgui::icon_size_get())) isOnlyShowLayers = !isOnlyShowLayers; - ImGui::SetItemTooltip(isOnlyShowLayers - ? "Only layers are visible. Press to toggle visibility for all items." - : "Non-layer items are visible. Press to toggle visiblity only for layers."); + ImGui::SetItemTooltip(isOnlyShowLayers ? "Only layers are visible. Press to show all items." + : "All items are visible. Press to only show layers."); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); @@ -368,8 +362,8 @@ namespace anm2ed::timeline auto& reference = document.reference; auto& hoveredFrame = document.hoveredFrame; auto item = animation ? animation->item_get(type, id) : nullptr; - auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto isVisible = item ? item->isVisible : false; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); @@ -521,8 +515,6 @@ namespace anm2ed::timeline auto isSelected = baseReference == frameReference; auto isFrameVisible = isVisible && frame.isVisible; - frameTime += frame.delay; - ImGui::PushID(i); auto size = ImVec2(frameSize.x * frame.delay, frameSize.y); @@ -564,6 +556,8 @@ namespace anm2ed::timeline drawList->AddImage(resources.icons[icon].id, imageMin, imageMax); + frameTime += frame.delay; + ImGui::PopID(); } @@ -703,7 +697,7 @@ namespace anm2ed::timeline ImGui::SetCursorPos( ImVec2(ImGui::GetStyle().WindowPadding.x, ImGui::GetCursorPos().y + ImGui::GetStyle().ItemSpacing.y)); - auto widgetSize = imgui::widget_size_with_row_get(9); + auto widgetSize = imgui::widget_size_with_row_get(10); ImGui::BeginDisabled(!animation); { @@ -749,8 +743,8 @@ namespace anm2ed::timeline ImGui::SameLine(); ImGui::SetNextItemWidth(widgetSize.x); - ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value(), step::NORMAL, - step::FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); + ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value(), imgui::STEP, + imgui::STEP_FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX); ImGui::SetItemTooltip("Set the animation's length."); @@ -775,6 +769,12 @@ namespace anm2ed::timeline imgui::input_text_string("Author", &anm2.info.createdBy); ImGui::SetItemTooltip("Set the author of the document."); + ImGui::SameLine(); + + ImGui::SetNextItemWidth(widgetSize.x); + ImGui::Checkbox("Sound", &settings.timelineIsSound); + ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window."); + ImGui::PopStyleVar(); } ImGui::EndChild(); @@ -961,9 +961,8 @@ namespace anm2ed::timeline auto frame = document.frame_get(); - ImGui::InputInt("Interval", &interval, step::NORMAL, step::FAST); + imgui::input_int_range("Interval", interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); ImGui::SetItemTooltip("Set the maximum delay of each frame that will be baked."); - interval = glm::clamp(interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); ImGui::Checkbox("Round Rotation", &isRoundRotation); ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number."); @@ -993,7 +992,6 @@ namespace anm2ed::timeline { auto& document = *manager.get(); auto& playback = document.playback; - auto& anm2 = document.anm2; auto& reference = document.reference; auto animation = document.animation_get(); diff --git a/src/imgui/window/timeline.h b/src/imgui/window/timeline.h new file mode 100644 index 0000000..3c0560d --- /dev/null +++ b/src/imgui/window/timeline.h @@ -0,0 +1,39 @@ +#pragma once + +#include "clipboard.h" +#include "document.h" +#include "manager.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui +{ + class Timeline + { + bool isDragging{}; + bool isWindowHovered{}; + bool isHorizontalScroll{}; + PopupHelper propertiesPopup{PopupHelper("Item Properties", POPUP_NORMAL)}; + PopupHelper bakePopup{PopupHelper("Bake", POPUP_TO_CONTENT)}; + std::string addItemName{}; + int addItemSpritesheetID{}; + bool addItemIsRect{}; + int addItemID{-1}; + bool isUnusedItemsSet{}; + std::set unusedItems{}; + glm::vec2 scroll{}; + ImDrawList* pickerLineDrawList{}; + ImGuiStyle style{}; + + void context_menu(Document&, Settings&, Clipboard&); + void item_child(Manager&, Document&, anm2::Animation*, Settings&, Resources&, Clipboard&, anm2::Type, int, int&); + void items_child(Manager&, Document&, anm2::Animation*, Settings&, Resources&, Clipboard&); + void frame_child(Document&, anm2::Animation*, Settings&, Resources&, Clipboard&, anm2::Type, int, int&, float); + void frames_child(Document&, anm2::Animation*, Settings&, Resources&, Clipboard&); + + void popups(Document&, anm2::Animation*, Settings&); + + public: + void update(Manager&, Settings&, Resources&, Clipboard&); + }; +} diff --git a/src/tools.cpp b/src/imgui/window/tools.cpp similarity index 66% rename from src/tools.cpp rename to src/imgui/window/tools.cpp index c0d6073..608806d 100644 --- a/src/tools.cpp +++ b/src/imgui/window/tools.cpp @@ -3,20 +3,15 @@ #include -#include "imgui.h" #include "tool.h" #include "types.h" -using namespace anm2ed::settings; -using namespace anm2ed::manager; -using namespace anm2ed::resources; +using namespace anm2ed::resource; using namespace anm2ed::types; using namespace glm; -namespace anm2ed::tools +namespace anm2ed::imgui { - constexpr auto COLOR_EDIT_LABEL = "##Color Edit"; - void Tools::update(Manager& manager, Settings& settings, Resources& resources) { auto& document = *manager.get(); @@ -29,21 +24,18 @@ namespace anm2ed::tools auto size = vec2(ImGui::GetTextLineHeightWithSpacing() * 1.5f); auto usedWidth = ImGui::GetStyle().WindowPadding.x; - auto tool_switch = [&](tool::Type type) + auto tool_use = [&](tool::Type type) { switch (type) { case tool::UNDO: - if (document.is_undo()) document.undo(); + if (document.is_able_to_undo()) document.undo(); break; case tool::REDO: - if (document.is_redo()) document.redo(); + if (document.is_able_to_redo()) document.redo(); break; case tool::COLOR: - if (ImGui::IsPopupOpen(COLOR_EDIT_LABEL)) - ImGui::CloseCurrentPopup(); - else - isOpenColorEdit = true; + colorEditPopup.open(); break; default: settings.tool = type; @@ -60,23 +52,22 @@ namespace anm2ed::tools auto member = SHORTCUT_MEMBERS[info.shortcut]; - if (imgui::shortcut(settings.*member, shortcut::GLOBAL_SET)) tool_switch((tool::Type)i); + if (shortcut(settings.*member, shortcut::GLOBAL_SET)) tool_use((tool::Type)i); if (i == tool::COLOR) { size += to_vec2(ImGui::GetStyle().FramePadding) * 2.0f; if (ImGui::ColorButton(info.label, to_imvec4(settings.toolColor), ImGuiColorEditFlags_NoTooltip, to_imvec2(size))) - tool_switch((tool::Type)i); + tool_use((tool::Type)i); colorEditPosition = ImGui::GetCursorScreenPos(); } else { - if (i == tool::UNDO) ImGui::BeginDisabled(!document.is_undo()); - if (i == tool::REDO) ImGui::BeginDisabled(!document.is_redo()); - if (ImGui::ImageButton(info.label, resources.icons[info.icon].id, to_imvec2(size))) - tool_switch((tool::Type)i); + if (i == tool::UNDO) ImGui::BeginDisabled(!document.is_able_to_undo()); + if (i == tool::REDO) ImGui::BeginDisabled(!document.is_able_to_redo()); + if (ImGui::ImageButton(info.label, resources.icons[info.icon].id, to_imvec2(size))) tool_use((tool::Type)i); if (i == tool::UNDO) ImGui::EndDisabled(); if (i == tool::REDO) ImGui::EndDisabled(); } @@ -88,25 +79,18 @@ namespace anm2ed::tools ImGui::SameLine(); else usedWidth = ImGui::GetStyle().WindowPadding.x; - - imgui::set_item_tooltip_shortcut(info.tooltip, settings.*SHORTCUT_MEMBERS[info.shortcut]); + set_item_tooltip_shortcut(info.tooltip, settings.*SHORTCUT_MEMBERS[info.shortcut]); if (isSelected) ImGui::PopStyleColor(); } ImGui::PopStyleVar(); - if (isOpenColorEdit) - { - ImGui::OpenPopup(COLOR_EDIT_LABEL); - isOpenColorEdit = false; - } + colorEditPopup.trigger(); - ImGui::SetNextWindowPos(colorEditPosition, ImGuiCond_None); - - if (ImGui::BeginPopup(COLOR_EDIT_LABEL)) + if (ImGui::BeginPopup(colorEditPopup.label)) { - ImGui::ColorPicker4(COLOR_EDIT_LABEL, value_ptr(settings.toolColor)); + ImGui::ColorPicker4(colorEditPopup.label, value_ptr(settings.toolColor)); ImGui::EndPopup(); } } diff --git a/src/tools.h b/src/imgui/window/tools.h similarity index 51% rename from src/tools.h rename to src/imgui/window/tools.h index 377b636..ad1ea2c 100644 --- a/src/tools.h +++ b/src/imgui/window/tools.h @@ -4,14 +4,16 @@ #include "resources.h" #include "settings.h" -namespace anm2ed::tools +namespace anm2ed::imgui { class Tools { bool isOpenColorEdit{}; ImVec2 colorEditPosition{}; + PopupHelper colorEditPopup{PopupHelper("##Color Edit", POPUP_TO_CONTENT, POPUP_BY_ITEM)}; + public: - void update(manager::Manager&, settings::Settings&, resources::Resources&); + void update(Manager&, Settings&, Resources&); }; } diff --git a/src/welcome.cpp b/src/imgui/window/welcome.cpp similarity index 69% rename from src/welcome.cpp rename to src/imgui/window/welcome.cpp index 2114906..38c69c2 100644 --- a/src/welcome.cpp +++ b/src/imgui/window/welcome.cpp @@ -2,13 +2,9 @@ #include -using namespace anm2ed::dialog; -using namespace anm2ed::taskbar; -using namespace anm2ed::documents; -using namespace anm2ed::resources; -using namespace anm2ed::manager; +using namespace anm2ed::resource; -namespace anm2ed::welcome +namespace anm2ed::imgui { void Welcome::update(Manager& manager, Resources& resources, Dialog& dialog, Taskbar& taskbar, Documents& documents) { @@ -19,28 +15,28 @@ namespace anm2ed::welcome ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height)); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight)); - if (ImGui::Begin("##Welcome", nullptr, + if (ImGui::Begin("Welcome", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); - ImGui::Text("Anm2Ed"); + ImGui::TextUnformatted("Anm2Ed"); ImGui::PopFont(); - ImGui::Text( - "Select a recent file or an option below. You can also drag and drop files into the window to open them."); + ImGui::TextUnformatted("Select a recent file or open a new or existing document. You can also drag and drop " + "files into the window to open them."); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); - if (ImGui::Button("New", widgetSize)) dialog.anm2_new(); // handled in taskbar.cpp + if (ImGui::Button("New", widgetSize)) dialog.file_open(dialog::ANM2_NEW); // handled in taskbar.cpp ImGui::SameLine(); - if (ImGui::Button("Open", widgetSize)) dialog.anm2_open(); // handled in taskbar.cpp + if (ImGui::Button("Open", widgetSize)) dialog.file_open(dialog::ANM2_OPEN); // handled in taskbar.cpp - if (ImGui::BeginChild("##Recent Child", ImVec2(), ImGuiChildFlags_Borders)) + if (ImGui::BeginChild("##Recent Files Child", ImVec2(), ImGuiChildFlags_Borders)) { for (auto [i, file] : std::views::enumerate(manager.recentFiles)) { @@ -68,11 +64,11 @@ namespace anm2ed::welcome if (ImGui::BeginPopupModal(restorePopup.label, &restorePopup.isOpen, ImGuiWindowFlags_NoResize)) { - ImGui::Text("Autosaved files detected. Would you like to restore them?"); + ImGui::TextUnformatted("Autosaved documents detected. Would you like to restore them?"); - auto childSize = imgui::child_size_get(5); + auto childSize = child_size_get(5); - if (ImGui::BeginChild("##Autosave Documents", childSize, ImGuiChildFlags_Borders, + if (ImGui::BeginChild("##Restore Files Child", childSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar)) { for (auto& file : manager.autosaveFiles) @@ -83,7 +79,7 @@ namespace anm2ed::welcome } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button("Yes", widgetSize)) { diff --git a/src/imgui/window/welcome.h b/src/imgui/window/welcome.h new file mode 100644 index 0000000..5a7d3f7 --- /dev/null +++ b/src/imgui/window/welcome.h @@ -0,0 +1,16 @@ +#pragma once + +#include "documents.h" +#include "manager.h" +#include "taskbar.h" + +namespace anm2ed::imgui +{ + class Welcome + { + PopupHelper restorePopup{PopupHelper("Restore", imgui::POPUP_SMALL_NO_HEIGHT)}; + + public: + void update(Manager&, Resources&, Dialog&, Taskbar&, Documents&); + }; +}; \ No newline at end of file diff --git a/src/loader.cpp b/src/loader.cpp index bcb82d5..5113398 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -3,16 +3,17 @@ #include #include -#include "filesystem.h" +#include + +#include "filesystem_.h" #include "log.h" -using namespace anm2ed::log; -using namespace anm2ed::settings; using namespace anm2ed::types; +using namespace anm2ed::util; -namespace anm2ed::loader +namespace anm2ed { - std::string settings_path() + std::string Loader::settings_path() { return filesystem::path_preferences_get() + "settings.ini"; } @@ -22,6 +23,8 @@ namespace anm2ed::loader for (int i = 1; i < argc; i++) arguments.emplace_back(argv[i]); + settings = Settings(settings_path()); + if (!SDL_Init(SDL_INIT_VIDEO)) { logger.fatal(std::format("Could not initialize SDL! {}", SDL_GetError())); @@ -29,7 +32,12 @@ namespace anm2ed::loader return; } - settings = Settings(settings_path()); + logger.info("Initialized SDL"); + + if (!MIX_Init()) + logger.warning(std::format("Could not initialize SDL_mixer! {}", SDL_GetError())); + else + logger.info("Initialized SDL_mixer"); window = SDL_CreateWindow("Anm2Ed", settings.windowSize.x, settings.windowSize.y, SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL | SDL_WINDOW_HIGH_PIXEL_DENSITY); @@ -54,7 +62,7 @@ namespace anm2ed::loader return; } - logger.info(std::format("OpenGL {}", (const char*)glGetString(GL_VERSION))); + logger.info(std::format("Initialized OpenGL {}", (const char*)glGetString(GL_VERSION))); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -65,7 +73,15 @@ namespace anm2ed::loader glClearColor(color::BLACK.r, color::BLACK.g, color::BLACK.b, color::BLACK.a); IMGUI_CHECKVERSION(); - ImGui::CreateContext(); + if (!ImGui::CreateContext()) + { + logger.fatal("Could not initialize Dear ImGui!"); + isError = true; + return; + } + + logger.info("Initialized Dear ImGui"); + ImGui::StyleColorsDark(); ImGui_ImplSDL3_InitForOpenGL(window, glContext); @@ -81,13 +97,22 @@ namespace anm2ed::loader Loader::~Loader() { - settings.save(settings_path(), ImGui::SaveIniSettingsToMemory(nullptr)); + if (ImGui::GetCurrentContext()) + { + settings.save(settings_path(), ImGui::SaveIniSettingsToMemory(nullptr)); - ImGui_ImplSDL3_Shutdown(); - ImGui_ImplOpenGL3_Shutdown(); - ImGui::DestroyContext(); - SDL_GL_DestroyContext(glContext); - SDL_DestroyWindow(window); - SDL_Quit(); + ImGui_ImplSDL3_Shutdown(); + ImGui_ImplOpenGL3_Shutdown(); + ImGui::DestroyContext(); + } + + MIX_Quit(); + + if (SDL_WasInit(0)) + { + SDL_GL_DestroyContext(glContext); + SDL_DestroyWindow(window); + SDL_Quit(); + } } } \ No newline at end of file diff --git a/src/loader.h b/src/loader.h index d3cbbb1..31c751a 100644 --- a/src/loader.h +++ b/src/loader.h @@ -7,14 +7,16 @@ #include "settings.h" -namespace anm2ed::loader +namespace anm2ed { class Loader { + std::string settings_path(); + public: SDL_Window* window{}; SDL_GLContext glContext{}; - settings::Settings settings; + Settings settings; std::vector arguments; bool isError{}; diff --git a/src/log.cpp b/src/log.cpp index 3cdfba3..4aba110 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -2,13 +2,12 @@ #include -#include "filesystem.h" -#include "util.h" +#include "filesystem_.h" +#include "time_.h" -using namespace anm2ed::filesystem; using namespace anm2ed::util; -namespace anm2ed::log +namespace anm2ed { void Logger::write(const Level level, const std::string& message) { @@ -37,6 +36,11 @@ namespace anm2ed::log write(FATAL, message); } + void Logger::command(const std::string& message) + { + write(COMMAND, message); + } + void Logger::open(const std::filesystem::path& path) { file.open(path, std::ios::out | std::ios::app); @@ -44,7 +48,7 @@ namespace anm2ed::log Logger::Logger() { - open(path_preferences_get() + "log.txt"); + open(filesystem::path_preferences_get() + "log.txt"); info("Initializing Anm2Ed"); } @@ -54,5 +58,6 @@ namespace anm2ed::log if (file.is_open()) file.close(); } - Logger logger; } + +anm2ed::Logger logger; diff --git a/src/log.h b/src/log.h index 1870827..f1ee6fc 100644 --- a/src/log.h +++ b/src/log.h @@ -3,13 +3,14 @@ #include #include -namespace anm2ed::log +namespace anm2ed { #define LEVELS \ X(INFO, "[INFO]") \ X(WARNING, "[WARNING]") \ X(ERROR, "[ERROR]") \ - X(FATAL, "[FATAL]") + X(FATAL, "[FATAL]") \ + X(COMMAND, "[COMMAND]") enum Level { @@ -35,10 +36,12 @@ namespace anm2ed::log void warning(const std::string&); void error(const std::string&); void fatal(const std::string&); + void command(const std::string&); void open(const std::filesystem::path&); Logger(); ~Logger(); }; - extern Logger logger; } + +extern anm2ed::Logger logger; diff --git a/src/main.cpp b/src/main.cpp index ea5ef6a..ea7d4da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,16 +1,13 @@ #include "loader.h" #include "state.h" -using namespace anm2ed::loader; -using namespace anm2ed::state; - int main(int argc, const char** argv) { - Loader loader(argc, argv); + anm2ed::Loader loader(argc, argv); if (loader.isError) return EXIT_FAILURE; - State state(loader.window, loader.arguments); + anm2ed::State state(loader.window, loader.arguments); while (!state.isQuit) state.loop(loader.window, loader.settings); diff --git a/src/manager.cpp b/src/manager.cpp index 7be966d..7d47128 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -2,17 +2,15 @@ #include -#include "filesystem.h" +#include "filesystem_.h" #include "log.h" #include "toast.h" -#include "util.h" +#include "vector_.h" -using namespace anm2ed::log; -using namespace anm2ed::toast; using namespace anm2ed::types; using namespace anm2ed::util; -namespace anm2ed::manager +namespace anm2ed { constexpr std::size_t RECENT_LIMIT = 10; @@ -121,7 +119,7 @@ namespace anm2ed::manager selected = std::clamp(selected, 0, (int)documents.size() - 1); pendingSelected = selected; - if (selected >= 0 && selected < (int)documents.size()) documents[selected].change(change::ALL); + if (selected >= 0 && selected < (int)documents.size()) documents[selected].change(Document::ALL); } void Manager::set(int index) @@ -136,7 +134,7 @@ namespace anm2ed::manager index = std::clamp(index, 0, (int)documents.size() - 1); selected = index; - if (auto document = get()) document->change(change::ALL); + if (auto document = get()) document->change(Document::ALL); } void Manager::layer_properties_open(int id) @@ -262,7 +260,7 @@ namespace anm2ed::manager { document->isForceDirty = true; document->path = restorePath; - document->change(change::ALL); + document->change(Document::ALL); } } diff --git a/src/manager.h b/src/manager.h index 4a4417c..549de70 100644 --- a/src/manager.h +++ b/src/manager.h @@ -3,11 +3,8 @@ #include #include "document.h" -#include "imgui.h" -using namespace anm2ed::document; - -namespace anm2ed::manager +namespace anm2ed { constexpr auto FILE_LABEL_FORMAT = "{} [{}]"; @@ -24,11 +21,17 @@ namespace anm2ed::manager int selected{-1}; int pendingSelected{-1}; + bool isRecording{}; + int recordingStart{}; + int recordingEnd{}; + anm2::Layer editLayer{}; - imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL, true)}; + imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; anm2::Null editNull{}; - imgui::PopupHelper nullPropertiesPopup{imgui::PopupHelper("Null Properties", imgui::POPUP_SMALL, true)}; + imgui::PopupHelper nullPropertiesPopup{imgui::PopupHelper("Null Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; + + imgui::PopupHelper progressPopup{imgui::PopupHelper("Rendering...", imgui::POPUP_SMALL_NO_HEIGHT)}; Manager(); ~Manager(); diff --git a/src/onionskin.cpp b/src/onionskin.cpp deleted file mode 100644 index 1ec35b6..0000000 --- a/src/onionskin.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "onionskin.h" - -#include - -#include "imgui.h" -#include "types.h" - -using namespace anm2ed::settings; -using namespace anm2ed::types; -using namespace glm; - -namespace anm2ed::onionskin -{ - void Onionskin::update(Settings& settings) - { - if (ImGui::Begin("Onionskin", &settings.windowIsOnionskin)) - { - auto order_configure = [&](const std::string& separator, int& frames, vec3& color) - { - ImGui::PushID(separator.c_str()); - ImGui::SeparatorText(separator.c_str()); - ImGui::InputInt("Frames", &frames, 1, 5); - frames = glm::clamp(frames, 0, 100); - ImGui::ColorEdit3("Color", value_ptr(color)); - ImGui::PopID(); - }; - - ImGui::Checkbox("Enabled", &settings.onionskinIsEnabled); - - order_configure("Before", settings.onionskinBeforeCount, settings.onionskinBeforeColor); - order_configure("After", settings.onionskinAfterCount, settings.onionskinAfterColor); - - ImGui::Text("Order"); - ImGui::SameLine(); - ImGui::RadioButton("Before", &settings.onionskinDrawOrder, draw_order::BELOW); - ImGui::SameLine(); - ImGui::RadioButton("After", &settings.onionskinDrawOrder, draw_order::ABOVE); - } - ImGui::End(); - - if (imgui::shortcut(settings.shortcutOnionskin, shortcut::GLOBAL)) - settings.onionskinIsEnabled = !settings.onionskinIsEnabled; - } - -} diff --git a/src/playback.cpp b/src/playback.cpp index 30c2413..03c2117 100644 --- a/src/playback.cpp +++ b/src/playback.cpp @@ -2,7 +2,7 @@ #include -namespace anm2ed::playback +namespace anm2ed { void Playback::toggle() { diff --git a/src/playback.h b/src/playback.h index 291c527..ad4034c 100644 --- a/src/playback.h +++ b/src/playback.h @@ -1,6 +1,6 @@ #pragma once -namespace anm2ed::playback +namespace anm2ed { class Playback { diff --git a/src/render.cpp b/src/render.cpp new file mode 100644 index 0000000..3258c2f --- /dev/null +++ b/src/render.cpp @@ -0,0 +1,96 @@ +#include "render.h" + +#include +#include + +#ifdef _WIN32 + #include "util.h" + #define POPEN _popen + #define PCLOSE _pclose + #define PWRITE_MODE "wb" + #define PREAD_MODE "r" +#else + #define POPEN popen + #define PCLOSE pclose + #define PWRITE_MODE "w" + #define PREAD_MODE "r" +#endif + +#include "log.h" + +using namespace anm2ed::resource; +using namespace glm; + +namespace anm2ed +{ + constexpr auto FFMPEG_POPEN_ERROR = "popen() (for FFmpeg) failed!\n{}"; + + constexpr auto GIF_FORMAT = "\"{0}\" -y " + "-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " + "-lavfi \"split[s0][s1];" + "[s0]palettegen=stats_mode=full[p];" + "[s1][p]paletteuse=dither=floyd_steinberg\" " + "-loop 0 \"{4}\""; + + constexpr auto WEBM_FORMAT = "\"{0}\" -y " + "-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " + "-c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 " + "-auto-alt-ref 0 -an \"{4}\""; + + constexpr auto* MP4_FORMAT = "\"{0}\" -y " + "-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " + "-vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" " + "-c:v libx265 -crf 20 -preset slow " + "-tag:v hvc1 -movflags +faststart -an \"{4}\""; + + bool animation_render(const std::string& ffmpegPath, const std::string& path, std::vector& frames, + render::Type type, ivec2 size, int fps) + { + if (frames.empty() || size.x <= 0 || size.y <= 0 || fps <= 0 || ffmpegPath.empty() || path.empty()) return false; + + std::string command{}; + + switch (type) + { + case render::GIF: + command = std::format(GIF_FORMAT, ffmpegPath, size.x, size.y, fps, path); + break; + case render::WEBM: + command = std::format(WEBM_FORMAT, ffmpegPath, size.x, size.y, fps, path); + break; + case render::MP4: + command = std::format(MP4_FORMAT, ffmpegPath, size.x, size.y, fps, path); + break; + default: + break; + } + +#if _WIN32 + command = string::quote(command); +#endif + + logger.command(command); + + FILE* fp = POPEN(command.c_str(), PWRITE_MODE); + + if (!fp) + { + logger.error(std::format(FFMPEG_POPEN_ERROR, strerror(errno))); + return false; + } + + for (auto& frame : frames) + { + auto frameSize = frame.pixel_size_get(); + + if (fwrite(frame.pixels.data(), 1, frameSize, fp) != frameSize) + { + PCLOSE(fp); + return false; + } + } + + auto code = PCLOSE(fp); + return (code == 0); + } +} \ No newline at end of file diff --git a/src/render.h b/src/render.h new file mode 100644 index 0000000..6e9cdfb --- /dev/null +++ b/src/render.h @@ -0,0 +1,32 @@ +#pragma once + +#include "texture.h" + +namespace anm2ed::render +{ +#define RENDER_LIST \ + X(PNGS, "PNGs") \ + X(GIF, "GIF") \ + X(WEBM, "WebM") \ + X(MP4, "MP4") + + enum Type + { +#define X(symbol, string) symbol, + RENDER_LIST +#undef X + COUNT + }; + + constexpr const char* STRINGS[] = { +#define X(symbol, string) string, + RENDER_LIST +#undef X + }; +} + +namespace anm2ed +{ + bool animation_render(const std::string&, const std::string&, std::vector&, render::Type, + glm::ivec2, int); +} \ No newline at end of file diff --git a/src/resource/audio.cpp b/src/resource/audio.cpp new file mode 100644 index 0000000..11979f2 --- /dev/null +++ b/src/resource/audio.cpp @@ -0,0 +1,49 @@ +#include "audio.h" + +#include + +namespace anm2ed::resource +{ + MIX_Mixer* Audio::mixer_get() + { + static MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); + return mixer; + } + + Audio::Audio(const char* path) + { + if (path && *path) internal = MIX_LoadAudio(mixer_get(), path, true); + } + + void Audio::unload() + { + if (!internal) return; + MIX_DestroyAudio(internal); + internal = nullptr; + } + + void Audio::play() + { + MIX_PlayAudio(mixer_get(), internal); + } + + Audio::Audio(Audio&& other) noexcept + { + internal = std::exchange(other.internal, nullptr); + } + + Audio& Audio::operator=(Audio&& other) noexcept + { + if (this != &other) + { + unload(); + internal = std::exchange(other.internal, nullptr); + } + return *this; + } + + Audio::~Audio() + { + unload(); + } +} diff --git a/src/resource/audio.h b/src/resource/audio.h new file mode 100644 index 0000000..6c2cc52 --- /dev/null +++ b/src/resource/audio.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace anm2ed::resource +{ + class Audio + { + MIX_Audio* internal{nullptr}; + MIX_Mixer* mixer_get(); + void unload(); + + public: + Audio(const char*); + ~Audio(); + Audio() = default; + Audio(Audio&&) noexcept; + Audio& operator=(Audio&&) noexcept; + Audio(const Audio&) = delete; + Audio& operator=(const Audio&) = delete; + + void play(); + }; +} diff --git a/src/font.cpp b/src/resource/font.cpp similarity index 95% rename from src/font.cpp rename to src/resource/font.cpp index 39a58ee..5504b19 100644 --- a/src/font.cpp +++ b/src/resource/font.cpp @@ -1,6 +1,6 @@ #include "font.h" -namespace anm2ed::font +namespace anm2ed::resource { Font::Font() = default; diff --git a/src/font.h b/src/resource/font.h similarity index 99% rename from src/font.h rename to src/resource/font.h index f5e4802..a0708bb 100644 --- a/src/font.h +++ b/src/resource/font.h @@ -99,10 +99,10 @@ OTHER DEALINGS IN THE FONT SOFTWARE. #include #include -namespace anm2ed::font +namespace anm2ed::resource::font { - constexpr int SIZE = 16; - constexpr int SIZE_LARGE = 32; + constexpr auto SIZE = 16; + constexpr auto SIZE_LARGE = 32; enum Type { @@ -5224,6 +5224,10 @@ namespace anm2ed::font {.data = BOLD_DATA, .length = std::size(BOLD_DATA)}, {.data = BOLD_ITALICS_DATA, .length = std::size(BOLD_ITALICS_DATA)}}; +}; + +namespace anm2ed::resource +{ class Font { ImFont* pointer{}; @@ -5235,4 +5239,4 @@ namespace anm2ed::font ImFont* get(); Font& operator=(Font&&) noexcept; }; -}; +} diff --git a/src/icon.h b/src/resource/icon.h similarity index 99% rename from src/icon.h rename to src/resource/icon.h index 50ab178..147a884 100644 --- a/src/icon.h +++ b/src/resource/icon.h @@ -2,7 +2,7 @@ #include #include -namespace icon +namespace anm2ed::resource::icon { constexpr auto SIZE_SMALL = glm::ivec2(64, 64); constexpr auto SIZE_NORMAL = glm::ivec2(128, 128); diff --git a/src/shader.cpp b/src/resource/shader.cpp similarity index 96% rename from src/shader.cpp rename to src/resource/shader.cpp index b205363..ba1e234 100644 --- a/src/shader.cpp +++ b/src/resource/shader.cpp @@ -2,9 +2,7 @@ #include "log.h" -using namespace anm2ed::log; - -namespace anm2ed::shader +namespace anm2ed::resource { Shader::Shader() = default; diff --git a/src/shader.h b/src/resource/shader.h similarity index 98% rename from src/shader.h rename to src/resource/shader.h index ab7e0f4..9794080 100644 --- a/src/shader.h +++ b/src/resource/shader.h @@ -2,7 +2,7 @@ #include -namespace anm2ed::shader +namespace anm2ed::resource::shader { struct Info { @@ -202,7 +202,7 @@ namespace anm2ed::shader constexpr auto UNIFORM_DASH_GAP = "u_dash_gap"; constexpr auto UNIFORM_DASH_OFFSET = "u_dash_offset"; - enum Type + enum ShaderType { LINE, DASHED, @@ -217,7 +217,10 @@ namespace anm2ed::shader {VERTEX, TEXTURE_FRAGMENT}, {AXIS_VERTEX, FRAGMENT}, {GRID_VERTEX, GRID_FRAGMENT}}; +} +namespace anm2ed::resource +{ class Shader { public: diff --git a/src/texture.cpp b/src/resource/texture.cpp similarity index 64% rename from src/texture.cpp rename to src/resource/texture.cpp index f508c47..89170ec 100644 --- a/src/texture.cpp +++ b/src/resource/texture.cpp @@ -20,49 +20,32 @@ #pragma GCC diagnostic pop #endif -#include "math.h" +#include "math_.h" -using namespace anm2ed::math; +using namespace anm2ed::resource::texture; +using namespace anm2ed::util::math; using namespace glm; -namespace anm2ed::texture +namespace anm2ed::resource { bool Texture::is_valid() { return id != 0; } - std::vector Texture::pixels_get() + size_t Texture::pixel_size_get() { - ensure_pixels(); - return pixels; - } - - void Texture::download() - { - if (size.x <= 0 || size.y <= 0 || !is_valid()) return; - pixels.resize(static_cast(size.x) * static_cast(size.y) * CHANNELS); - glBindTexture(GL_TEXTURE_2D, id); - glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); - glBindTexture(GL_TEXTURE_2D, 0); - isPixelsDirty = false; + return size.x * size.y * CHANNELS; } void Texture::upload(const uint8_t* data) { if (!data || size.x <= 0 || size.y <= 0) return; - const size_t pixelCount = static_cast(size.x) * static_cast(size.y) * CHANNELS; - pixels.assign(data, data + pixelCount); - upload(); - } - - void Texture::upload() - { - if (pixels.empty() || size.x <= 0 || size.y <= 0) return; - if (!is_valid()) glGenTextures(1, &id); + pixels.assign(data, data + pixel_size_get()); + glBindTexture(GL_TEXTURE_2D, id); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); @@ -74,7 +57,6 @@ namespace anm2ed::texture glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); - isPixelsDirty = false; } Texture::Texture() = default; @@ -94,35 +76,31 @@ namespace anm2ed::texture *this = std::move(other); } - Texture& Texture::operator=(const Texture& other) + Texture& Texture::operator=(const Texture& other) // Copy { if (this != &other) { if (is_valid()) glDeleteTextures(1, &id); - id = 0; - other.ensure_pixels(); size = other.size; filter = other.filter; channels = other.channels; pixels = other.pixels; - if (!pixels.empty()) upload(); + upload(pixels.data()); } return *this; } - Texture& Texture::operator=(Texture&& other) + Texture& Texture::operator=(Texture&& other) // Move { if (this != &other) { if (is_valid()) glDeleteTextures(1, &id); - id = std::exchange(other.id, 0); - size = std::exchange(other.size, {}); + id = other.id; + size = other.size; filter = other.filter; channels = other.channels; pixels = std::move(other.pixels); - isPixelsDirty = other.isPixelsDirty; - other.isPixelsDirty = true; - if (!pixels.empty()) upload(); + other.id = 0; } return *this; } @@ -159,7 +137,6 @@ namespace anm2ed::texture bool Texture::write_png(const std::string& path) { - ensure_pixels(); return stbi_write_png(path.c_str(), size.x, size.y, CHANNELS, this->pixels.data(), size.x * CHANNELS); } @@ -167,30 +144,26 @@ namespace anm2ed::texture { if (position.x < 0 || position.y < 0 || position.x >= size.x || position.y >= size.y) return; - ensure_pixels(); uint8 rgba8[4] = {(uint8)float_to_uint8(color.r), (uint8)float_to_uint8(color.g), (uint8)float_to_uint8(color.b), (uint8)float_to_uint8(color.a)}; - glBindTexture(GL_TEXTURE_2D, id); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glTexSubImage2D(GL_TEXTURE_2D, 0, position.x, position.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba8); - glBindTexture(GL_TEXTURE_2D, 0); - if (!pixels.empty()) + if (is_valid()) { - auto index = (position.y * size.x + position.x) * CHANNELS; - pixels[index + 0] = rgba8[0]; - pixels[index + 1] = rgba8[1]; - pixels[index + 2] = rgba8[2]; - pixels[index + 3] = rgba8[3]; - isPixelsDirty = false; + glBindTexture(GL_TEXTURE_2D, id); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexSubImage2D(GL_TEXTURE_2D, 0, position.x, position.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba8); + glBindTexture(GL_TEXTURE_2D, 0); + } + + if (pixels.size() == pixel_size_get()) + { + size_t idx = (position.y * size.x + position.x) * CHANNELS; + memcpy(&pixels[idx], rgba8, 4); } - else - isPixelsDirty = true; } void Texture::pixel_line(ivec2 start, ivec2 end, vec4 color) { - ensure_pixels(); auto plot = [&](ivec2 pos) { pixel_set(pos, color); }; int x0 = start.x; @@ -221,23 +194,4 @@ namespace anm2ed::texture } } } - - void Texture::ensure_pixels() const - { - if (size.x <= 0 || size.y <= 0) return; - if (!pixels.empty() && !isPixelsDirty) return; - const_cast(this)->download(); - } - - void Texture::bind(GLuint unit) - { - glActiveTexture(GL_TEXTURE0 + unit); - glBindTexture(GL_TEXTURE_2D, id); - } - - void Texture::unbind(GLuint unit) - { - glActiveTexture(GL_TEXTURE0 + unit); - glBindTexture(GL_TEXTURE_2D, 0); - } } diff --git a/src/texture.h b/src/resource/texture.h similarity index 74% rename from src/texture.h rename to src/resource/texture.h index 419c64c..c73e138 100644 --- a/src/texture.h +++ b/src/resource/texture.h @@ -6,10 +6,13 @@ #include #include -namespace anm2ed::texture +namespace anm2ed::resource::texture { constexpr auto CHANNELS = 4; +} +namespace anm2ed::resource +{ class Texture { public: @@ -17,13 +20,12 @@ namespace anm2ed::texture glm::ivec2 size{}; GLint filter = GL_NEAREST; int channels{}; - mutable std::vector pixels{}; - mutable bool isPixelsDirty{true}; + std::vector pixels{}; bool is_valid(); - void download(); - void upload(const uint8_t*); + size_t pixel_size_get(); void upload(); + void upload(const uint8_t*); Texture(); ~Texture(); @@ -36,10 +38,6 @@ namespace anm2ed::texture Texture(const std::string&); bool write_png(const std::string&); void pixel_set(glm::ivec2, glm::vec4); - void ensure_pixels() const; - std::vector pixels_get(); void pixel_line(glm::ivec2, glm::ivec2, glm::vec4); - void bind(GLuint = 0); - void unbind(GLuint = 0); }; } diff --git a/src/resources.cpp b/src/resources.cpp index eec7ecf..d4bfa54 100644 --- a/src/resources.cpp +++ b/src/resources.cpp @@ -1,11 +1,10 @@ #include "resources.h" + #include -using namespace anm2ed::texture; -using namespace anm2ed::shader; -using namespace anm2ed::font; +using namespace anm2ed::resource; -namespace anm2ed::resources +namespace anm2ed { Resources::Resources() { diff --git a/src/resources.h b/src/resources.h index 3554be5..d703b4a 100644 --- a/src/resources.h +++ b/src/resources.h @@ -7,14 +7,14 @@ #include "shader.h" #include "texture.h" -namespace anm2ed::resources +namespace anm2ed { class Resources { public: - font::Font fonts[font::COUNT]{}; - texture::Texture icons[icon::COUNT]{}; - shader::Shader shaders[shader::COUNT]{}; + resource::Font fonts[resource::font::COUNT]{}; + resource::Texture icons[resource::icon::COUNT]{}; + resource::Shader shaders[resource::shader::COUNT]{}; Resources(); }; diff --git a/src/settings.cpp b/src/settings.cpp index d801dab..84e22f8 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1,13 +1,12 @@ #include "settings.h" -#include "filesystem.h" +#include "filesystem_.h" #include "log.h" -using namespace anm2ed::filesystem; -using namespace anm2ed::log; +using namespace anm2ed::util; using namespace glm; -namespace anm2ed::settings +namespace anm2ed { constexpr auto IMGUI_DEFAULT = R"( # Dear ImGui @@ -104,11 +103,9 @@ DockSpace ID=0xFC02A410 Window=0x0E46F4F7 Pos=8,40 Size=1584,852 Split=Y DockNode ID=0x00000004 Parent=0xFC02A410 SizeRef=1902,334 Selected=0x4F89F0DC )"; - Settings::Settings() = default; - Settings::Settings(const std::string& path) { - if (path_is_exist(path)) + if (filesystem::path_is_exist(path)) logger.info(std::format("Using settings from: {}", path)); else { diff --git a/src/settings.h b/src/settings.h index 9f9aa3d..61eba79 100644 --- a/src/settings.h +++ b/src/settings.h @@ -4,10 +4,11 @@ #include -#include "anm2.h" +#include "anm2/anm2.h" +#include "render.h" #include "types.h" -namespace anm2ed::settings +namespace anm2ed { #ifdef _WIN32 constexpr auto FFMPEG_PATH_DEFAULT = "C:\\ffmpeg\\bin\\ffmpeg.exe"; @@ -127,6 +128,7 @@ namespace anm2ed::settings X(TIMELINE_ADD_ITEM_SOURCE, timelineAddItemSource, "Add Item Source", INT, types::source::NEW) \ X(TIMELINE_IS_SHOW_UNUSED, timelineIsShowUnused, "##Show Unused", BOOL, true) \ X(TIMELINE_IS_ONLY_SHOW_LAYERS, timelineIsOnlyShowLayers, "##Only Show Layers", BOOL, true) \ + X(TIMELINE_IS_SOUND, timelineIsSound, "Sound", BOOL, true) \ \ X(ONIONSKIN_IS_ENABLED, onionskinIsEnabled, "Enabled", BOOL, false) \ X(ONIONSKIN_DRAW_ORDER, onionskinDrawOrder, "Draw Order", INT, 0) \ @@ -138,11 +140,12 @@ namespace anm2ed::settings X(TOOL, tool, "##Tool", INT, 0) \ X(TOOL_COLOR, toolColor, "##Color", VEC4, {1.0, 1.0, 1.0, 1.0}) \ \ - X(RENDER_TYPE, renderType, "Output", INT, 0) \ + X(RENDER_TYPE, renderType, "Output", INT, render::PNGS) \ X(RENDER_PATH, renderPath, "Path", STRING, ".") \ X(RENDER_FORMAT, renderFormat, "Format", STRING, "{}.png") \ X(RENDER_IS_USE_ANIMATION_BOUNDS, renderIsUseAnimationBounds, "Use Animation Bounds", BOOL, true) \ X(RENDER_IS_TRANSPARENT, renderIsTransparent, "Transparent", BOOL, true) \ + X(RENDER_IS_RANGE, renderIsRange, "Range", BOOL, false) \ X(RENDER_SCALE, renderScale, "Scale", FLOAT, 1.0f) \ X(RENDER_FFMPEG_PATH, renderFFmpegPath, "FFmpeg Path", STRING, FFMPEG_PATH_DEFAULT) @@ -197,6 +200,7 @@ namespace anm2ed::settings X(WINDOW_NULLS, windowIsNulls, "Nulls", BOOL, true) \ X(WINDOW_ONIONSKIN, windowIsOnionskin, "Onionskin", BOOL, true) \ X(WINDOW_PREVIEW, windowIsSpritesheets, "Spritesheets", BOOL, true) \ + X(WINDOW_SOUNDS, windowIsSounds, "Sounds", BOOL, true) \ X(WINDOW_SPRITESHEET_EDITOR, windowIsSpritesheetEditor, "Spritesheet Editor", BOOL, true) \ X(WINDOW_TIMELINE, windowIsTimeline, "Timeline", BOOL, true) \ X(WINDOW_TOOLS, windowIsTools, "Tools", BOOL, true) @@ -208,7 +212,7 @@ namespace anm2ed::settings SETTINGS_MEMBERS SETTINGS_SHORTCUTS SETTINGS_WINDOWS #undef X - Settings(); + Settings() = default; Settings(const std::string&); void save(const std::string&, const std::string&); diff --git a/src/snapshots.cpp b/src/snapshots.cpp index b473529..304624e 100644 --- a/src/snapshots.cpp +++ b/src/snapshots.cpp @@ -1,16 +1,9 @@ #include "snapshots.h" -namespace anm2ed::snapshots -{ - namespace - { - void textures_ensure(anm2::Anm2& anm2) - { - for (auto& [id, spritesheet] : anm2.content.spritesheets) - spritesheet.texture.ensure_pixels(); - } - } +using namespace anm2ed::snapshots; +namespace anm2ed +{ bool SnapshotStack::is_empty() { return top == 0; @@ -40,7 +33,6 @@ namespace anm2ed::snapshots void Snapshots::push(const anm2::Anm2& anm2, anm2::Reference reference, const std::string& message) { - textures_ensure(const_cast(anm2)); Snapshot snapshot = {anm2, reference, message}; undoStack.push(snapshot); redoStack.clear(); @@ -50,10 +42,8 @@ namespace anm2ed::snapshots { if (auto current = undoStack.pop()) { - textures_ensure(anm2); Snapshot snapshot = {anm2, reference, message}; redoStack.push(snapshot); - textures_ensure(current->anm2); anm2 = current->anm2; reference = current->reference; message = current->message; @@ -64,10 +54,8 @@ namespace anm2ed::snapshots { if (auto current = redoStack.pop()) { - textures_ensure(anm2); Snapshot snapshot = {anm2, reference, message}; undoStack.push(snapshot); - textures_ensure(current->anm2); anm2 = current->anm2; reference = current->reference; message = current->message; diff --git a/src/snapshots.h b/src/snapshots.h index e0f12ff..a18d4a7 100644 --- a/src/snapshots.h +++ b/src/snapshots.h @@ -1,24 +1,27 @@ #pragma once -#include "anm2.h" +#include "anm2/anm2.h" namespace anm2ed::snapshots { constexpr auto ACTION = "Action"; constexpr auto MAX = 100; +}; +namespace anm2ed +{ class Snapshot { public: anm2::Anm2 anm2{}; anm2::Reference reference{}; - std::string message = ACTION; + std::string message = snapshots::ACTION; }; class SnapshotStack { public: - Snapshot snapshots[MAX]; + Snapshot snapshots[snapshots::MAX]; int top{}; bool is_empty(); diff --git a/src/spritesheets.h b/src/spritesheets.h deleted file mode 100644 index 27a30d5..0000000 --- a/src/spritesheets.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "clipboard.h" -#include "dialog.h" -#include "manager.h" -#include "resources.h" -#include "settings.h" - -namespace anm2ed::spritesheets -{ - class Spritesheets - { - public: - void update(manager::Manager&, settings::Settings&, resources::Resources&, dialog::Dialog&, - clipboard::Clipboard& clipboard); - }; -} diff --git a/src/state.cpp b/src/state.cpp index fae94f2..ff0fb2a 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -3,15 +3,14 @@ #include #include -#include "filesystem.h" +#include "filesystem_.h" #include "toast.h" -using namespace anm2ed::settings; -using namespace anm2ed::dialog; -using namespace anm2ed::toast; +using namespace anm2ed::imgui; +using namespace anm2ed::util; using namespace anm2ed::types; -namespace anm2ed::state +namespace anm2ed { constexpr auto TICK_RATE = 30; constexpr auto TICK_INTERVAL = (1000 / TICK_RATE); @@ -28,11 +27,15 @@ namespace anm2ed::state void State::tick(Settings& settings) { + dockspace.tick(manager, settings); + if (auto document = manager.get()) + { if (auto animation = document->animation_get()) if (document->playback.isPlaying) document->playback.tick(document->anm2.info.fps, animation->frameNum, - animation->isLoop || settings.playbackIsLoop); + (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording); + } } void State::update(SDL_Window*& window, Settings& settings) diff --git a/src/state.h b/src/state.h index 6f04633..07cc3f0 100644 --- a/src/state.h +++ b/src/state.h @@ -4,31 +4,31 @@ #include "dockspace.h" -namespace anm2ed::state +namespace anm2ed { class State { - void tick(settings::Settings&); - void update(SDL_Window*&, settings::Settings&); - void render(SDL_Window*&, settings::Settings&); + void tick(Settings&); + void update(SDL_Window*&, Settings&); + void render(SDL_Window*&, Settings&); public: bool isQuit{}; bool isQuitting{}; - manager::Manager manager; - resources::Resources resources; - dialog::Dialog dialog; - clipboard::Clipboard clipboard; + Manager manager; + Resources resources; + Dialog dialog; + Clipboard clipboard; - taskbar::Taskbar taskbar; - documents::Documents documents; - dockspace::Dockspace dockspace; + imgui::Taskbar taskbar; + imgui::Documents documents; + imgui::Dockspace dockspace; uint64_t previousTick{}; uint64_t previousUpdate{}; State(SDL_Window*&, std::vector&); - void loop(SDL_Window*&, settings::Settings&); + void loop(SDL_Window*&, Settings&); }; }; diff --git a/src/taskbar.h b/src/taskbar.h deleted file mode 100644 index 9549ae2..0000000 --- a/src/taskbar.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "canvas.h" -#include "dialog.h" -#include "imgui.h" -#include "manager.h" -#include "resources.h" -#include "settings.h" - -namespace anm2ed::taskbar -{ - class Taskbar - { - canvas::Canvas generate; - float generateTime{}; - imgui::PopupHelper generatePopup{imgui::PopupHelper("Generate Animation from Grid")}; - imgui::PopupHelper changePopup{imgui::PopupHelper("Change All Frame Properties", imgui::POPUP_SMALL, true)}; - imgui::PopupHelper renderPopup{imgui::PopupHelper("Render Animation")}; - imgui::PopupHelper configurePopup{imgui::PopupHelper("Configure")}; - imgui::PopupHelper aboutPopup{imgui::PopupHelper("About")}; - settings::Settings editSettings{}; - int selectedShortcut{-1}; - bool isQuittingMode{}; - - public: - float height{}; - - Taskbar(); - void update(manager::Manager&, settings::Settings&, resources::Resources&, dialog::Dialog&, bool&); - }; -}; diff --git a/src/timeline.h b/src/timeline.h deleted file mode 100644 index 9bb6f16..0000000 --- a/src/timeline.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include "anm2.h" -#include "clipboard.h" -#include "document.h" -#include "manager.h" -#include "resources.h" -#include "settings.h" - -namespace anm2ed::timeline -{ - class Timeline - { - bool isDragging{}; - bool isWindowHovered{}; - bool isHorizontalScroll{}; - imgui::PopupHelper propertiesPopup{imgui::PopupHelper("Item Properties")}; - imgui::PopupHelper bakePopup{imgui::PopupHelper("Bake", imgui::POPUP_SMALL, true)}; - std::string addItemName{}; - int addItemSpritesheetID{}; - bool addItemIsRect{}; - int addItemID{-1}; - bool isUnusedItemsSet{}; - std::set unusedItems{}; - glm::vec2 scroll{}; - ImDrawList* pickerLineDrawList{}; - ImGuiStyle style{}; - - void context_menu(document::Document&, settings::Settings&, clipboard::Clipboard&); - void item_child(manager::Manager&, Document&, anm2::Animation*, settings::Settings&, resources::Resources&, - clipboard::Clipboard&, anm2::Type, int, int&); - void items_child(manager::Manager&, Document&, anm2::Animation*, settings::Settings&, resources::Resources&, - clipboard::Clipboard&); - void frame_child(document::Document&, anm2::Animation*, settings::Settings&, resources::Resources&, - clipboard::Clipboard&, anm2::Type, int, int&, float); - void frames_child(document::Document&, anm2::Animation*, settings::Settings&, resources::Resources&, - clipboard::Clipboard&); - - void popups(document::Document&, anm2::Animation*, settings::Settings&); - - public: - void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); - }; -} diff --git a/src/tool.h b/src/tool.h index 8814899..1c143cf 100644 --- a/src/tool.h +++ b/src/tool.h @@ -24,50 +24,50 @@ namespace anm2ed::tool struct Info { ImGuiMouseCursor cursor{ImGuiMouseCursor_None}; - icon::Type icon{}; - settings::ShortcutType shortcut{}; + resource::icon::Type icon{}; + ShortcutType shortcut{}; const char* label{}; const char* tooltip{}; }; constexpr Info INFO[] = { - {ImGuiMouseCursor_Hand, icon::PAN, settings::SHORTCUT_PAN, "##Pan", + {ImGuiMouseCursor_Hand, resource::icon::PAN, SHORTCUT_PAN, "##Pan", "Use the pan tool.\nWill shift the view as the cursor is dragged.\nYou can also use the middle mouse button to " "pan at any time."}, - {ImGuiMouseCursor_ResizeAll, icon::MOVE, settings::SHORTCUT_MOVE, "##Move", + {ImGuiMouseCursor_ResizeAll, resource::icon::MOVE, SHORTCUT_MOVE, "##Move", "Use the move tool.\nAnimation Preview: Will move the position of the frame." "\nSpritesheet Editor: Will move the pivot, and holding right click will use the Crop functionality instead." "\nUse mouse or directional keys to change the value."}, - {ImGuiMouseCursor_Arrow, icon::ROTATE, settings::SHORTCUT_ROTATE, "##Rotate", + {ImGuiMouseCursor_Arrow, resource::icon::ROTATE, SHORTCUT_ROTATE, "##Rotate", "Use the rotate tool.\nWill rotate the selected item as the cursor is dragged, or directional keys are " "pressed.\n(Animation Preview only.)"}, - {ImGuiMouseCursor_ResizeNWSE, icon::SCALE, settings::SHORTCUT_SCALE, "##Scale", + {ImGuiMouseCursor_ResizeNESW, resource::icon::SCALE, SHORTCUT_SCALE, "##Scale", "Use the scale tool.\nWill scale the selected item as the cursor is dragged, or directional keys are " "pressed.\n(Animation Preview only.)"}, - {ImGuiMouseCursor_ResizeAll, icon::CROP, settings::SHORTCUT_CROP, "##Crop", + {ImGuiMouseCursor_Arrow, resource::icon::CROP, SHORTCUT_CROP, "##Crop", "Use the crop tool.\nWill produce a crop rectangle based on how the cursor is dragged." "\nAlternatively, you can use the arrow keys and Ctrl/Shift to move the size/position, respectively." "\nHolding right click will use the Move tool's functionality." "\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_Hand, icon::DRAW, settings::SHORTCUT_DRAW, "##Draw", + {ImGuiMouseCursor_Arrow, resource::icon::DRAW, SHORTCUT_DRAW, "##Draw", "Draws pixels onto the selected spritesheet, with the current color.\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_Arrow, icon::ERASE, settings::SHORTCUT_ERASE, "##Erase", + {ImGuiMouseCursor_Arrow, resource::icon::ERASE, SHORTCUT_ERASE, "##Erase", "Erases pixels from the selected spritesheet.\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_Arrow, icon::COLOR_PICKER, settings::SHORTCUT_COLOR_PICKER, "##Color Picker", + {ImGuiMouseCursor_Arrow, resource::icon::COLOR_PICKER, SHORTCUT_COLOR_PICKER, "##Color Picker", "Selects a color from the canvas.\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_None, icon::UNDO, settings::SHORTCUT_UNDO, "##Undo", "Undoes the last action."}, + {ImGuiMouseCursor_None, resource::icon::UNDO, SHORTCUT_UNDO, "##Undo", "Undoes the last action."}, - {ImGuiMouseCursor_None, icon::REDO, settings::SHORTCUT_REDO, "##Redo", "Redoes the last action."}, + {ImGuiMouseCursor_None, resource::icon::REDO, SHORTCUT_REDO, "##Redo", "Redoes the last action."}, - {ImGuiMouseCursor_None, icon::NONE, settings::SHORTCUT_COLOR, "##Color", + {ImGuiMouseCursor_None, resource::icon::NONE, SHORTCUT_COLOR, "##Color", "Selects the color to be used for drawing.\n(Spritesheet Editor only.)"}, }; } \ No newline at end of file diff --git a/src/types.h b/src/types.h index acc12b3..80b58e7 100644 --- a/src/types.h +++ b/src/types.h @@ -4,22 +4,6 @@ #include #include -namespace anm2ed::types::change -{ - enum Type - { - LAYERS, - NULLS, - SPRITESHEETS, - EVENTS, - ANIMATIONS, - ITEMS, - FRAMES, - ALL, - COUNT - }; -} - namespace anm2ed::types::draw_order { enum Type @@ -69,16 +53,6 @@ namespace anm2ed::types::merge }; } -namespace anm2ed::types::frame_change -{ - enum Type - { - ADD, - SUBTRACT, - ADJUST - }; -} - namespace anm2ed::types::color { using namespace glm; @@ -88,17 +62,14 @@ namespace anm2ed::types::color constexpr auto RED = vec4(1.0, 0.0, 0.0, 1.0); constexpr auto GREEN = vec4(0.0, 1.0, 0.0, 1.0); constexpr auto BLUE = vec4(0.0, 0.0, 1.0, 1.0); + constexpr auto PINK = vec4(1.0, 0.0, 1.0, 1.0); constexpr auto TRANSPARENT = vec4(); } -namespace anm2ed::types::step -{ - constexpr auto NORMAL = 1; - constexpr auto FAST = 10; -} - namespace anm2ed::types { + constexpr auto ID_NONE = -1; + constexpr ImVec2 to_imvec2(const glm::vec2& v) noexcept { return {v.x, v.y}; diff --git a/src/filesystem.cpp b/src/util/filesystem_.cpp similarity index 87% rename from src/filesystem.cpp rename to src/util/filesystem_.cpp index c62dcaa..e3c3783 100644 --- a/src/filesystem.cpp +++ b/src/util/filesystem_.cpp @@ -1,16 +1,11 @@ -#include "filesystem.h" +#include "filesystem_.h" #include #include #include -namespace anm2ed::filesystem +namespace anm2ed::util::filesystem { - bool directories_create(const std::string& path) - { - return std::filesystem::create_directories(path); - } - std::string path_preferences_get() { char* preferencesPath = SDL_GetPrefPath("", "anm2ed"); diff --git a/src/filesystem.h b/src/util/filesystem_.h similarity index 82% rename from src/filesystem.h rename to src/util/filesystem_.h index 2c2db96..7a78ef3 100644 --- a/src/filesystem.h +++ b/src/util/filesystem_.h @@ -3,12 +3,11 @@ #include #include -namespace anm2ed::filesystem +namespace anm2ed::util::filesystem { std::string path_preferences_get(); bool path_is_exist(const std::string&); bool path_is_extension(const std::string&, const std::string&); - bool directories_create(const std::string&); class WorkingDirectory { diff --git a/src/util/map_.h b/src/util/map_.h new file mode 100644 index 0000000..6a75c89 --- /dev/null +++ b/src/util/map_.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace anm2ed::util::map +{ + template int next_id_get(std::map& map) + { + int id = 0; + + for (auto& [key, value] : map) + { + if (key != id) break; + ++id; + } + + return id; + } + + template T1* find(std::map& map, T0 index) + { + return map.contains(index) ? &map[index] : nullptr; + } +} diff --git a/src/math.cpp b/src/util/math_.cpp similarity index 98% rename from src/math.cpp rename to src/util/math_.cpp index edeca35..a34b62a 100644 --- a/src/math.cpp +++ b/src/util/math_.cpp @@ -5,7 +5,7 @@ using namespace glm; -namespace anm2ed::math +namespace anm2ed::util::math { constexpr auto FLOAT_FORMAT_MAX_DECIMALS = 7; constexpr auto FLOAT_FORMAT_EPSILON = 1e-7f; @@ -78,4 +78,4 @@ namespace anm2ed::math return glm::translate(mat4(1.0f), vec3(position, 0.0f)) * local; } -} +} \ No newline at end of file diff --git a/src/math.h b/src/util/math_.h similarity index 88% rename from src/math.h rename to src/util/math_.h index 6e928be..5a1beac 100644 --- a/src/math.h +++ b/src/util/math_.h @@ -1,9 +1,9 @@ #pragma once #include -#include +#include -namespace anm2ed::math +namespace anm2ed::util::math { template constexpr T percent_to_unit(T value) { @@ -39,7 +39,6 @@ namespace anm2ed::math const char* vec2_format_get(glm::vec2&); - glm::mat4 quad_model_get(glm::vec2 = {}, glm::vec2 = {}, glm::vec2 = {}, - glm::vec2 = glm::vec2(1.0f), float = {}); + glm::mat4 quad_model_get(glm::vec2 = {}, glm::vec2 = {}, glm::vec2 = {}, glm::vec2 = glm::vec2(1.0f), float = {}); glm::mat4 quad_model_parent_get(glm::vec2 = {}, glm::vec2 = {}, glm::vec2 = glm::vec2(1.0f), float = {}); -} +} \ No newline at end of file diff --git a/src/util.cpp b/src/util/string_.cpp similarity index 55% rename from src/util.cpp rename to src/util/string_.cpp index 110a494..c814326 100644 --- a/src/util.cpp +++ b/src/util/string_.cpp @@ -1,20 +1,6 @@ -#include "util.h" +#include "string_.h" #include -#include - -namespace anm2ed::util::time -{ - std::string get(const char* format) - { - auto now = std::chrono::system_clock::now(); - auto time = std::chrono::system_clock::to_time_t(now); - auto localTime = *std::localtime(&time); - std::ostringstream timeString; - timeString << std::put_time(&localTime, format); - return timeString.str(); - } -} namespace anm2ed::util::string { @@ -25,7 +11,7 @@ namespace anm2ed::util::string return transformed; } - std::string replace_backslash(const std::string& string) + std::string backslash_replace(const std::string& string) { std::string transformed = string; for (char& character : transformed) @@ -33,6 +19,18 @@ namespace anm2ed::util::string return transformed; } + std::string backslash_replace_to_lower(const std::string& string) + { + std::string transformed = string; + transformed = backslash_replace(transformed); + return to_lower(transformed); + } + + std::string quote(const std::string& string) + { + return "\"" + string + "\""; + } + bool to_bool(const std::string& string) { return to_lower(string) == "true"; diff --git a/src/util/string_.h b/src/util/string_.h new file mode 100644 index 0000000..ce4d623 --- /dev/null +++ b/src/util/string_.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace anm2ed::util::string +{ + std::string to_lower(const std::string&); + std::string backslash_replace(const std::string&); + std::string backslash_replace_to_lower(const std::string&); + bool to_bool(const std::string&); +} diff --git a/src/util/time_.cpp b/src/util/time_.cpp new file mode 100644 index 0000000..96021c4 --- /dev/null +++ b/src/util/time_.cpp @@ -0,0 +1,16 @@ +#include "time_.h" + +#include + +namespace anm2ed::util::time +{ + std::string get(const char* format) + { + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + auto localTime = *std::localtime(&time); + std::ostringstream timeString; + timeString << std::put_time(&localTime, format); + return timeString.str(); + } +} \ No newline at end of file diff --git a/src/util/time_.h b/src/util/time_.h new file mode 100644 index 0000000..f2f716d --- /dev/null +++ b/src/util/time_.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace anm2ed::util::time +{ + constexpr auto SECOND_M = 60; + + std::string get(const char*); +} \ No newline at end of file diff --git a/src/util/unordered_map_.h b/src/util/unordered_map_.h new file mode 100644 index 0000000..2c37e77 --- /dev/null +++ b/src/util/unordered_map_.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace anm2ed::util::unordered_map +{ + template T1* find(std::unordered_map& map, T0 index) + { + return map.contains(index) ? &map[index] : nullptr; + } +} diff --git a/src/util.h b/src/util/vector_.h similarity index 67% rename from src/util.h rename to src/util/vector_.h index b66ac09..49d4cca 100644 --- a/src/util.h +++ b/src/util/vector_.h @@ -1,57 +1,10 @@ #pragma once #include -#include #include #include -#include -#include #include -namespace anm2ed::util::time -{ - constexpr auto SECOND_S = 1.0; - constexpr auto SECOND_M = 60.0; - - std::string get(const char*); -} - -namespace anm2ed::util::string -{ - std::string to_lower(const std::string&); - std::string replace_backslash(const std::string&); - bool to_bool(const std::string&); -} - -namespace anm2ed::util::map -{ - template int next_id_get(std::map& map) - { - int id = 0; - - for (auto& [key, value] : map) - { - if (key != id) break; - ++id; - } - - return id; - } - - template T1* find(std::map& map, T0 index) - { - return map.contains(index) ? &map[index] : nullptr; - } -} - -namespace anm2ed::util::unordered_map -{ - template T1* find(std::unordered_map& map, T0 index) - { - return map.contains(index) ? &map[index] : nullptr; - } -} - namespace anm2ed::util::vector { template T* find(std::vector& v, int index) diff --git a/src/util/xml_.cpp b/src/util/xml_.cpp new file mode 100644 index 0000000..47299db --- /dev/null +++ b/src/util/xml_.cpp @@ -0,0 +1,38 @@ +#include "xml_.h" + +#include "math_.h" + +using namespace tinyxml2; + +namespace anm2ed::util::xml +{ + std::string document_to_string(XMLDocument& self) + { + XMLPrinter printer{}; + self.Print(&printer); + return std::string(printer.CStr()); + } + + XMLError query_string_attribute(XMLElement* element, const char* attribute, std::string* out) + { + const char* temp = nullptr; + auto result = element->QueryStringAttribute(attribute, &temp); + if (result == XML_SUCCESS && temp) *out = temp; + return result; + } + + XMLError query_path_attribute(XMLElement* element, const char* attribute, std::filesystem::path* out) + { + std::string temp{}; + auto result = query_string_attribute(element, attribute, &temp); + if (result == XML_SUCCESS) *out = temp; + return result; + } + + void query_color_attribute(XMLElement* element, const char* attribute, float& out) + { + int value{}; + element->QueryIntAttribute(attribute, &value); + out = math::uint8_to_float(value); + } +} \ No newline at end of file diff --git a/src/xml.h b/src/util/xml_.h similarity index 93% rename from src/xml.h rename to src/util/xml_.h index 100ec82..0a4cf2b 100644 --- a/src/xml.h +++ b/src/util/xml_.h @@ -5,7 +5,7 @@ #include -namespace anm2ed::xml +namespace anm2ed::util::xml { std::string document_to_string(tinyxml2::XMLDocument&); tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement*, const char*, std::string*); diff --git a/src/welcome.h b/src/welcome.h deleted file mode 100644 index 238470f..0000000 --- a/src/welcome.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "documents.h" -#include "manager.h" -#include "taskbar.h" - -namespace anm2ed::welcome -{ - class Welcome - { - imgui::PopupHelper restorePopup{imgui::PopupHelper("Restore", imgui::POPUP_SMALL, true)}; - - public: - void update(manager::Manager&, resources::Resources&, dialog::Dialog&, taskbar::Taskbar&, documents::Documents&); - }; -}; \ No newline at end of file diff --git a/src/xml.cpp b/src/xml.cpp deleted file mode 100644 index c4ff81b..0000000 --- a/src/xml.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "xml.h" - -#include "math.h" - -namespace anm2ed::xml -{ - std::string document_to_string(tinyxml2::XMLDocument& self) - { - tinyxml2::XMLPrinter printer{}; - self.Print(&printer); - return std::string(printer.CStr()); - } - - tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement* element, const char* attribute, std::string* out) - { - const char* temp = nullptr; - auto result = element->QueryStringAttribute(attribute, &temp); - if (result == tinyxml2::XML_SUCCESS && temp) *out = temp; - return result; - } - - tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement* element, const char* attribute, - std::filesystem::path* out) - { - std::string temp{}; - auto result = query_string_attribute(element, attribute, &temp); - if (result == tinyxml2::XML_SUCCESS) *out = temp; - return result; - } - - void query_color_attribute(tinyxml2::XMLElement* element, const char* attribute, float& out) - { - int value{}; - element->QueryIntAttribute(attribute, &value); - out = math::uint8_to_float(value); - } -} \ No newline at end of file