diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f8f257d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +You are a senior level professional software developer, etc. + +Use X Macros liberally and lookup tables (maps, etc.) for querying data, etc. Avoid functions for this purpose (i.e., don't map enums to strings with a function). Define X Macros in .cpp files if possible, headers if not. + +Try to use structs/defines for data in general. + +Prefer enums instead of bools for functions in cases where it makes sense (i.e., instead of true/false, have descriptive enums). + +If user asks for "how to", or "why", that's an indicator to generate no code and simply answer their prompt. + +Avoid hardcoding specific solutions and always see if there's ways to optimize/crunch down existing code to support whatever solution user is asking for. Your goal is the least lines/files for the same functionality, while maintaing most clarity/simplicy. Do not use comments. + +Avoid use of anonymous namespaces. For namepsaces, try to have them match files; don't create superfluous namespaces within documents. + +If an if statement has a single line inside, omit the braces. + +Functions should use snake case and should typically be written like "[noun]_[verb]". Try and use a limited amount of verbs to describe functionality; "save", "load", etc. + +For functions that check if true/false, write them like a question, starting with "is", i.e., "is_[noun]_[verb]". + + +ImGui hidden labels, etc. that aren't user-facing should be typically like "##Name Here". + +If a change is ordered that would impede past API, make it happen regardless without consideration of backwards compatibility. + +With bools, ALWAYS name them "is[VariableName]", even if the variable would be gramatically incorrect. + +When compiling/building, use as many threads as possible. \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index ce375e9..95ab13d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,8 +88,6 @@ set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp) file(GLOB PROJECT_SRC CONFIGURE_DEPENDS src/anm2/*.cpp src/anm2/*.hpp - src/anm2_new/*.cpp - src/anm2_new/*.hpp src/resource/*.cpp src/resource/*.hpp src/imgui/*.cpp @@ -100,6 +98,8 @@ file(GLOB PROJECT_SRC CONFIGURE_DEPENDS src/imgui/popup/*.hpp src/imgui/wizard/*.cpp src/imgui/wizard/*.hpp + src/util/imgui/*.cpp + src/util/imgui/*.hpp src/util/*.cpp src/util/*.hpp src/window/*.cpp diff --git a/src/anm2/animation.cpp b/src/anm2/animation.cpp deleted file mode 100644 index 8808703..0000000 --- a/src/anm2/animation.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "animation.hpp" - -#include "map_.hpp" -#include "math_.hpp" -#include "unordered_map_.hpp" -#include "xml_.hpp" -#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.emplace(id, Item(child, LAYER, &id)); - layerOrder.emplace_back(id); - } - } - - if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations")) - for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child; - child = child->NextSiblingElement("NullAnimation")) - nullAnimations.emplace(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; - } - } - - XMLElement* Animation::to_element(XMLDocument& document, Flags flags) - { - auto element = document.NewElement("Animation"); - element->SetAttribute("Name", name.c_str()); - element->SetAttribute("FrameNum", frameNum); - element->SetAttribute("Loop", isLoop); - - rootAnimation.serialize(document, element, ROOT, -1, flags); - - auto layerAnimationsElement = document.NewElement("LayerAnimations"); - for (auto& i : layerOrder) - { - Item& layerAnimation = layerAnimations.at(i); - layerAnimation.serialize(document, layerAnimationsElement, LAYER, i, flags); - } - element->InsertEndChild(layerAnimationsElement); - - auto nullAnimationsElement = document.NewElement("NullAnimations"); - for (auto& [id, nullAnimation] : nullAnimations) - nullAnimation.serialize(document, nullAnimationsElement, NULL_, id, flags); - element->InsertEndChild(nullAnimationsElement); - - triggers.serialize(document, element, TRIGGER, -1, flags); - - return element; - } - - void Animation::serialize(XMLDocument& document, XMLElement* parent, Flags flags) - { - parent->InsertEndChild(to_element(document, flags)); - } - - std::string Animation::to_string() - { - XMLDocument document{}; - document.InsertEndChild(to_element(document)); - return xml::document_to_string(document); - } - - 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; - } - - void Animation::fit_length() { frameNum = length(); } - - vec4 Animation::rect(bool isRootTransform) - { - constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; - - 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; - - for (float t = 0.0f; t < (float)frameNum; t += 1.0f) - { - mat4 transform(1.0f); - - if (isRootTransform) - { - auto root = rootAnimation.frame_generate(t, ROOT); - transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); - } - - for (auto& [id, layerAnimation] : layerAnimations) - { - if (!layerAnimation.isVisible) continue; - - auto frame = layerAnimation.frame_generate(t, 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}; - } -} diff --git a/src/anm2/animation.hpp b/src/anm2/animation.hpp deleted file mode 100644 index 727f73e..0000000 --- a/src/anm2/animation.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include - -#include "item.hpp" - -namespace anm2ed::anm2 -{ - constexpr auto FRAME_NUM_MIN = 1; - constexpr auto FRAME_NUM_MAX = FRAME_DURATION_MAX; - - class Animation - { - public: - std::string name{}; - 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); - tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0); - std::string to_string(); - int length(); - void fit_length(); - glm::vec4 rect(bool); - }; - -} diff --git a/src/anm2/animations.cpp b/src/anm2/animations.cpp deleted file mode 100644 index ff698e5..0000000 --- a/src/anm2/animations.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "animations.hpp" - -#include "xml_.hpp" - -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, Flags flags) - { - auto element = document.NewElement("Animations"); - element->SetAttribute("DefaultAnimation", defaultAnimation.c_str()); - for (auto& animation : items) - animation.serialize(document, element, flags); - return element; - } - - void Animations::serialize(XMLDocument& document, XMLElement* parent, Flags flags) - { - parent->InsertEndChild(to_element(document, flags)); - } - - int Animations::length() - { - int length{}; - - for (auto& animation : items) - if (int animationLength = animation.length(); animationLength > length) length = animationLength; - - return length; - } - -} diff --git a/src/anm2/animations.hpp b/src/anm2/animations.hpp deleted file mode 100644 index 29bf0f0..0000000 --- a/src/anm2/animations.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "animation.hpp" - -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&, Flags = 0); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0); - int length(); - }; -} diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp index 7a99f76..723a726 100644 --- a/src/anm2/anm2.cpp +++ b/src/anm2/anm2.cpp @@ -1,231 +1,1174 @@ #include "anm2.hpp" #include -#include +#include +#include +#include +#include #include +#include #include +#include #include -#include "file_.hpp" -#include "map_.hpp" -#include "math_.hpp" -#include "time_.hpp" -#include "vector_.hpp" +#include "file.hpp" +#include "math.hpp" +#include "path.hpp" +#include "time.hpp" #include "working_directory.hpp" -#include "xml_.hpp" +#include "xml.hpp" -using namespace tinyxml2; -using namespace anm2ed::types; using namespace anm2ed::util; -using namespace glm; +using namespace tinyxml2; -namespace +namespace anm2ed { - int remap_id(const std::unordered_map& table, int value) +#define ANM2_STRING_ATTRIBUTES \ + X("Name", name) \ + X("CreatedBy", createdBy) \ + X("CreatedOn", createdOn) \ + X("DefaultAnimation", defaultAnimation) + +#define ANM2_PATH_ATTRIBUTES X("Path", path) + +#define ANM2_INT_ATTRIBUTES \ + X("Id", id) \ + X("LayerId", layerId) \ + X("NullId", nullId) \ + X("SpritesheetId", spritesheetId) \ + X("Fps", fps) \ + X("Version", version) \ + X("FrameNum", frameNum) \ + X("Delay", duration) \ + X("AtFrame", atFrame) \ + X("EventId", eventId) \ + X("RegionId", regionId) \ + X("SoundId", soundId) \ + X("GroupId", groupId) + +#define ANM2_BOOL_ATTRIBUTES \ + X("Loop", isLoop) \ + X("Visible", isVisible) \ + X("ShowRect", isShowRect) \ + X("Expanded", isExpanded) + +#define ANM2_FLOAT_ATTRIBUTES \ + X("Rotation", rotation) \ + X("XPivot", pivot.x) \ + X("YPivot", pivot.y) \ + X("XCrop", crop.x) \ + X("YCrop", crop.y) \ + X("XPosition", position.x) \ + X("YPosition", position.y) \ + X("Width", size.x) \ + X("Height", size.y) \ + X("XScale", scale.x) \ + X("YScale", scale.y) + +#define ANM2_COLOR_ATTRIBUTES \ + X("RedTint", tint.r) \ + X("GreenTint", tint.g) \ + X("BlueTint", tint.b) \ + X("AlphaTint", tint.a) \ + X("RedOffset", colorOffset.r) \ + X("GreenOffset", colorOffset.g) \ + X("BlueOffset", colorOffset.b) + + constexpr std::string_view ANIMATION_MERGED_SUFFIX = " (Merged)"; + + constexpr std::array ELEMENT_TAGS = { +#define X(symbol, tag) std::string_view{tag}, + ANM2_ELEMENT_TYPES +#undef X + }; + + constexpr std::array INTERPOLATION_VALUES = { + "", "", "EaseIn", "EaseOut", "EaseInOut"}; + + constexpr std::array ORIGIN_VALUES = {"", "TopLeft", "Center"}; + + constexpr std::array COMPATIBILITY_FLAGS = { + NO_SOUNDS | NO_REGIONS | NO_GROUPS | FRAME_NO_REGION_VALUES | INTERPOLATION_BOOL_ONLY, 0, + NO_GROUPS | FRAME_NO_REGION_VALUES}; + + Flags flags_for(Compatibility compatibility) { - if (value < 0) return value; - if (auto it = table.find(value); it != table.end()) return it->second; - return value; + auto index = (std::size_t)compatibility; + if (index >= COMPATIBILITY_FLAGS.size()) return 0; + return COMPATIBILITY_FLAGS[index]; } - void region_frames_sync(anm2ed::anm2::Anm2& anm2, bool clearInvalid) + ElementType element_type_get(std::string_view tag) { - for (auto& animation : anm2.animations.items) - { - for (auto& [layerId, layerAnimation] : animation.layerAnimations) - { - if (!anm2.content.layers.contains(layerId)) continue; - auto& layer = anm2.content.layers.at(layerId); - auto spritesheet = anm2.spritesheet_get(layer.spritesheetID); - if (!spritesheet) continue; - - for (auto& frame : layerAnimation.frames) - { - if (frame.regionID == -1) continue; - auto regionIt = spritesheet->regions.find(frame.regionID); - if (regionIt == spritesheet->regions.end()) - { - if (clearInvalid) frame.regionID = -1; - continue; - } - frame.crop = regionIt->second.crop; - frame.size = regionIt->second.size; - frame.pivot = regionIt->second.pivot; - } - } - } - } -} - -namespace anm2ed::anm2 -{ - Anm2::Anm2() { info.createdOn = time::get("%m/%d/%Y %I:%M:%S %p"); } - - Frame Anm2::frame_effective(int layerId, const Frame& frame) const - { - auto resolved = frame; - if (frame.regionID == -1) return resolved; - if (!content.layers.contains(layerId)) return resolved; - - auto spritesheet = const_cast(this)->spritesheet_get(content.layers.at(layerId).spritesheetID); - if (!spritesheet) return resolved; - - auto regionIt = spritesheet->regions.find(frame.regionID); - if (regionIt == spritesheet->regions.end()) return resolved; - - resolved.crop = regionIt->second.crop; - resolved.size = regionIt->second.size; - resolved.pivot = regionIt->second.pivot; - return resolved; + for (std::size_t i = 0; i < ELEMENT_TAGS.size(); ++i) + if (ELEMENT_TAGS[i] == tag) return (ElementType)i; + return ElementType::UNKNOWN; } - vec4 Anm2::animation_rect(Animation& animation, bool isRootTransform) const + std::string_view element_tag_get(ElementType type) { - constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; - - 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; - - for (float t = 0.0f; t < (float)animation.frameNum; t += 1.0f) - { - mat4 transform(1.0f); - - if (isRootTransform) - { - auto root = animation.rootAnimation.frame_generate(t, ROOT); - transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); - } - - for (auto& [id, layerAnimation] : animation.layerAnimations) - { - if (!layerAnimation.isVisible) continue; - - auto frame = frame_effective(id, layerAnimation.frame_generate(t, 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}; + auto index = (std::size_t)type; + if (index >= ELEMENT_TAGS.size()) return {}; + return ELEMENT_TAGS[index]; } - void Anm2::bake_special_interpolated_frames(int interval, bool isRoundScale, bool isRoundRotation) + ElementType element_container_type_get(ElementType type) { - auto bake_item = [&](Item& item) + switch (type) { - for (int i = (int)item.frames.size() - 1; i >= 0; --i) - { - auto interpolation = item.frames[i].interpolation; - if (interpolation == Frame::Interpolation::NONE || interpolation == Frame::Interpolation::LINEAR) continue; - item.frames_bake(i, interval, isRoundScale, isRoundRotation); - } - }; - - for (auto& animation : animations.items) - { - bake_item(animation.rootAnimation); - for (auto& item : animation.layerAnimations | std::views::values) - bake_item(item); - for (auto& item : animation.nullAnimations | std::views::values) - bake_item(item); + case ElementType::SPRITESHEET: + return ElementType::SPRITESHEETS; + case ElementType::LAYER_ELEMENT: + return ElementType::LAYERS; + case ElementType::NULL_ELEMENT: + return ElementType::NULLS; + case ElementType::EVENT_ELEMENT: + return ElementType::EVENTS; + case ElementType::SOUND_ELEMENT: + return ElementType::SOUNDS; + case ElementType::ANIMATION: + return ElementType::ANIMATIONS; + default: + return ElementType::UNKNOWN; } } - bool Anm2::has_special_interpolated_frames() const + Element element_make(ElementType type) { - auto item_has_special_frames = [](const Item& item) - { - for (const auto& frame : item.frames) - { - auto interpolation = frame.interpolation; - if (interpolation != Frame::Interpolation::NONE && interpolation != Frame::Interpolation::LINEAR) return true; - } - return false; - }; + Element element{}; + element.type = type; + element.tag = std::string(element_tag_get(type)); + return element; + } - for (const auto& animation : animations.items) + bool string_query(const XMLElement* element, const char* name, std::string& out) + { + if (!element) return false; + if (auto value = element->Attribute(name); value) { - if (item_has_special_frames(animation.rootAnimation)) return true; - for (const auto& item : animation.layerAnimations | std::views::values) - if (item_has_special_frames(item)) return true; - for (const auto& item : animation.nullAnimations | std::views::values) - if (item_has_special_frames(item)) return true; + out = value; + return true; } - return false; } - Anm2::Anm2(const std::filesystem::path& path, std::string* errorString) + bool path_query(const XMLElement* element, const char* name, std::filesystem::path& out) { - XMLDocument document; + std::string value{}; + if (!string_query(element, name, value)) return false; + out = path::from_utf8(value); + return true; + } + void path_set(XMLElement* element, const char* name, const std::filesystem::path& value) + { + if (!element || value.empty()) return; + auto valueUtf8 = path::to_utf8(value); + element->SetAttribute(name, valueUtf8.c_str()); + } + + float color_read(const XMLElement* element, const char* name, float fallback) + { + int value{}; + if (!element || element->QueryIntAttribute(name, &value) != XML_SUCCESS) return fallback; + return math::uint8_to_float(value); + } + + int color_write(float value) { return math::float_to_uint8(glm::clamp(value, 0.0f, 1.0f)); } + + Interpolation interpolation_read(const XMLElement* element) + { + bool isFallback{}; + if (element) element->QueryBoolAttribute("Interpolated", &isFallback); + + auto value = element ? element->Attribute("Interpolated") : nullptr; + if (!value) return Interpolation::NONE; + + for (std::size_t i = 0; i < INTERPOLATION_VALUES.size(); ++i) + if (!INTERPOLATION_VALUES[i].empty() && INTERPOLATION_VALUES[i] == value) return (Interpolation)i; + + return isFallback ? Interpolation::LINEAR : Interpolation::NONE; + } + + void interpolation_write(XMLElement* element, Interpolation interpolation, Flags flags) + { + if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE || + interpolation == Interpolation::LINEAR) + { + element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR); + return; + } + + auto value = INTERPOLATION_VALUES[(std::size_t)interpolation]; + if (!value.empty()) element->SetAttribute("Interpolated", value.data()); + } + + Origin origin_read(const XMLElement* element) + { + auto value = element ? element->Attribute("Origin") : nullptr; + if (!value) return Origin::CUSTOM; + + for (std::size_t i = 0; i < ORIGIN_VALUES.size(); ++i) + if (!ORIGIN_VALUES[i].empty() && ORIGIN_VALUES[i] == value) return (Origin)i; + + return Origin::CUSTOM; + } + + void origin_write(XMLElement* out, const Element& element) + { + auto origin = ORIGIN_VALUES[(std::size_t)element.origin]; + if (!origin.empty()) + { + out->SetAttribute("Origin", origin.data()); + return; + } + + out->SetAttribute("XPivot", element.pivot.x); + out->SetAttribute("YPivot", element.pivot.y); + } + + void element_attributes_read(Element& out, const XMLElement* element) + { +#define X(attribute, member) string_query(element, attribute, out.member); + ANM2_STRING_ATTRIBUTES +#undef X +#define X(attribute, member) path_query(element, attribute, out.member); + ANM2_PATH_ATTRIBUTES +#undef X +#define X(attribute, member) element->QueryIntAttribute(attribute, &out.member); + ANM2_INT_ATTRIBUTES +#undef X +#define X(attribute, member) element->QueryBoolAttribute(attribute, &out.member); + ANM2_BOOL_ATTRIBUTES +#undef X +#define X(attribute, member) element->QueryFloatAttribute(attribute, &out.member); + ANM2_FLOAT_ATTRIBUTES +#undef X +#define X(attribute, member) out.member = color_read(element, attribute, out.member); + ANM2_COLOR_ATTRIBUTES +#undef X + + out.interpolation = interpolation_read(element); + out.origin = origin_read(element); + if (out.type == ElementType::REGION && out.origin == Origin::TOP_LEFT) out.pivot = {}; + if (out.type == ElementType::REGION && out.origin == Origin::CENTER) out.pivot = glm::ivec2(out.size / 2.0f); + } + + Element element_read(const XMLElement* element) + { + Element out{}; + if (!element) return out; + + out.tag = element->Name() ? element->Name() : ""; + out.type = element_type_get(out.tag); + element_attributes_read(out, element); + + for (auto child = element->FirstChildElement(); child; child = child->NextSiblingElement()) + out.children.push_back(element_read(child)); + + if (out.type == ElementType::TRIGGER && out.soundId != -1) + { + bool isSoundFound{}; + for (const auto& child : out.children) + if (child.type == ElementType::SOUND_ELEMENT && child.id == out.soundId) isSoundFound = true; + + if (!isSoundFound) + { + auto sound = element_make(ElementType::SOUND_ELEMENT); + sound.id = out.soundId; + out.children.insert(out.children.begin(), sound); + } + } + + if (out.type == ElementType::TRIGGER) + for (const auto& child : out.children) + if (child.type == ElementType::SOUND_ELEMENT && child.id != -1) out.soundIds.push_back(child.id); + + return out; + } + + bool element_write_skip(const Element& element, ElementType parentType, Flags flags) + { + if (element.type == ElementType::SOUNDS && (has_flag(flags, NO_SOUNDS) || element.children.empty())) return true; + if (element.type == ElementType::SOUND_ELEMENT && parentType == ElementType::TRIGGER && has_flag(flags, NO_SOUNDS)) + return true; + if (element.type == ElementType::REGION && has_flag(flags, NO_REGIONS)) return true; + if (element.type == ElementType::GROUP && has_flag(flags, NO_GROUPS)) return true; + return false; + } + + void frame_attributes_write(XMLElement* out, const Element& element, ElementType parentType, Flags flags) + { + if (parentType == ElementType::LAYER_ANIMATION) + { + bool isNoRegions = has_flag(flags, NO_REGIONS); + bool isFrameNoRegionValues = has_flag(flags, FRAME_NO_REGION_VALUES); + bool isHasValidRegion = !isNoRegions && element.regionId != -1; + bool isWriteRegionValues = !isFrameNoRegionValues || !isHasValidRegion; + + if (isHasValidRegion) out->SetAttribute("RegionId", element.regionId); + if (isWriteRegionValues) + { + out->SetAttribute("XPivot", element.pivot.x); + out->SetAttribute("YPivot", element.pivot.y); + out->SetAttribute("XCrop", element.crop.x); + out->SetAttribute("YCrop", element.crop.y); + out->SetAttribute("Width", element.size.x); + out->SetAttribute("Height", element.size.y); + } + } + + out->SetAttribute("XPosition", element.position.x); + out->SetAttribute("YPosition", element.position.y); + out->SetAttribute("Delay", element.duration); + out->SetAttribute("Visible", element.isVisible); + out->SetAttribute("XScale", element.scale.x); + out->SetAttribute("YScale", element.scale.y); + out->SetAttribute("RedTint", color_write(element.tint.r)); + out->SetAttribute("GreenTint", color_write(element.tint.g)); + out->SetAttribute("BlueTint", color_write(element.tint.b)); + out->SetAttribute("AlphaTint", color_write(element.tint.a)); + out->SetAttribute("RedOffset", color_write(element.colorOffset.r)); + out->SetAttribute("GreenOffset", color_write(element.colorOffset.g)); + out->SetAttribute("BlueOffset", color_write(element.colorOffset.b)); + out->SetAttribute("Rotation", element.rotation); + interpolation_write(out, element.interpolation, flags); + } + + void element_attributes_write(XMLElement* out, const Element& element, ElementType parentType, Flags flags) + { + if (element.type == ElementType::INFO) + { + out->SetAttribute("CreatedBy", element.createdBy.c_str()); + out->SetAttribute("CreatedOn", element.createdOn.c_str()); + out->SetAttribute("Fps", element.fps); + out->SetAttribute("Version", element.version); + } + else if (element.type == ElementType::ANIMATIONS) + out->SetAttribute("DefaultAnimation", element.defaultAnimation.c_str()); + else if (element.type == ElementType::SPRITESHEET) + { + out->SetAttribute("Id", element.id); + path_set(out, "Path", element.path); + } + else if (element.type == ElementType::REGION) + { + out->SetAttribute("Id", element.id); + out->SetAttribute("Name", element.name.c_str()); + out->SetAttribute("XCrop", element.crop.x); + out->SetAttribute("YCrop", element.crop.y); + out->SetAttribute("Width", element.size.x); + out->SetAttribute("Height", element.size.y); + origin_write(out, element); + } + else if (element.type == ElementType::LAYER_ELEMENT) + { + out->SetAttribute("Id", element.id); + out->SetAttribute("Name", element.name.c_str()); + out->SetAttribute("SpritesheetId", element.spritesheetId); + } + else if (element.type == ElementType::NULL_ELEMENT) + { + out->SetAttribute("Id", element.id); + out->SetAttribute("Name", element.name.c_str()); + if (element.isShowRect) out->SetAttribute("ShowRect", element.isShowRect); + } + else if (element.type == ElementType::GROUP) + { + out->SetAttribute("Id", element.id); + out->SetAttribute("Name", element.name.c_str()); + out->SetAttribute("Expanded", element.isExpanded); + out->SetAttribute("Visible", element.isVisible); + } + else if (element.type == ElementType::EVENT_ELEMENT) + { + out->SetAttribute("Id", element.id); + out->SetAttribute("Name", element.name.c_str()); + } + else if (element.type == ElementType::SOUND_ELEMENT) + { + out->SetAttribute("Id", element.id); + if (parentType != ElementType::TRIGGER) path_set(out, "Path", element.path); + } + else if (element.type == ElementType::ANIMATION) + { + out->SetAttribute("Name", element.name.c_str()); + out->SetAttribute("FrameNum", element.frameNum); + out->SetAttribute("Loop", element.isLoop); + } + else if (element.type == ElementType::LAYER_ANIMATION) + { + out->SetAttribute("LayerId", element.layerId); + out->SetAttribute("Visible", element.isVisible); + if (element.groupId != -1 && !has_flag(flags, NO_GROUPS)) out->SetAttribute("GroupId", element.groupId); + } + else if (element.type == ElementType::NULL_ANIMATION) + { + out->SetAttribute("NullId", element.nullId); + out->SetAttribute("Visible", element.isVisible); + if (element.groupId != -1 && !has_flag(flags, NO_GROUPS)) out->SetAttribute("GroupId", element.groupId); + } + else if (element.type == ElementType::FRAME) + frame_attributes_write(out, element, parentType, flags); + else if (element.type == ElementType::TRIGGER) + { + if (element.eventId != -1) out->SetAttribute("EventId", element.eventId); + out->SetAttribute("AtFrame", element.atFrame); + } + } + + XMLElement* element_to_xml(XMLDocument& document, const Element& element, ElementType parentType, Flags flags) + { + auto tag = element.type == ElementType::UNKNOWN ? std::string_view(element.tag) : element_tag_get(element.type); + auto out = document.NewElement(tag.empty() ? element.tag.c_str() : tag.data()); + element_attributes_write(out, element, parentType, flags); + + for (const auto& child : element.children) + { + if (!element_write_skip(child, element.type, flags)) + out->InsertEndChild(element_to_xml(document, child, element.type, flags)); + } + + return out; + } + + XMLElement* element_to_xml(XMLDocument& document, const Element& element, Flags flags) + { + return element_to_xml(document, element, ElementType::UNKNOWN, flags); + } + + std::string element_to_string(const Element& element, Flags flags) + { + XMLDocument document{}; + document.InsertEndChild(element_to_xml(document, element, flags)); + return xml::document_to_string(document); + } + + void groups_flatten(Element& element) + { + auto container_flatten = [](Element& container, ElementType trackType) + { + int nextGroupId{}; + for (const auto& item : container.children) + if (item.type == ElementType::GROUP) nextGroupId = std::max(nextGroupId, item.id + 1); + + std::vector flattened{}; + for (auto item : container.children) + { + if (item.type != ElementType::GROUP) + { + flattened.push_back(item); + continue; + } + + if (item.id < 0) item.id = nextGroupId++; + auto groupId = item.id; + auto children = std::move(item.children); + item.children.clear(); + flattened.push_back(item); + + for (auto child : children) + { + if (child.type != trackType) continue; + child.groupId = groupId; + flattened.push_back(child); + } + } + + std::set groupIds{}; + for (const auto& item : flattened) + if (item.type == ElementType::GROUP) groupIds.insert(item.id); + + for (auto& item : flattened) + if (item.type == trackType && !groupIds.contains(item.groupId)) item.groupId = -1; + + container.children = std::move(flattened); + }; + + if (element.type == ElementType::LAYER_ANIMATIONS) + container_flatten(element, ElementType::LAYER_ANIMATION); + else if (element.type == ElementType::NULL_ANIMATIONS) + container_flatten(element, ElementType::NULL_ANIMATION); + + for (auto& child : element.children) + groups_flatten(child); + } + + Element* child_first_get(Element& element, ElementType type) + { + for (auto& child : element.children) + if (child.type == type) return &child; + return nullptr; + } + + const Element* child_first_get(const Element& element, ElementType type) + { + for (const auto& child : element.children) + if (child.type == type) return &child; + return nullptr; + } + + Element* element_child_first_get(Element& element, ElementType type) { return child_first_get(element, type); } + + const Element* element_child_first_get(const Element& element, ElementType type) + { + return child_first_get(element, type); + } + + Element* child_id_get(Element& element, ElementType type, int id) + { + for (auto& child : element.children) + if (child.type == type && child.id == id) return &child; + return nullptr; + } + + const Element* child_id_get(const Element& element, ElementType type, int id) + { + for (const auto& child : element.children) + if (child.type == type && child.id == id) return &child; + return nullptr; + } + + Element* element_child_id_get(Element& element, ElementType type, int id) { return child_id_get(element, type, id); } + + const Element* element_child_id_get(const Element& element, ElementType type, int id) + { + return child_id_get(element, type, id); + } + + int element_child_max_id_get(const Element& element, ElementType type) + { + int maxId{-1}; + for (const auto& child : element.children) + if (child.type == type) maxId = std::max(maxId, child.id); + return maxId; + } + + int element_child_next_id_get(const Element& element, ElementType type) + { + return element_child_max_id_get(element, type) + 1; + } + + bool element_child_id_erase(Element& element, ElementType type, int id) + { + for (auto it = element.children.begin(); it != element.children.end(); ++it) + { + if (it->type != type || it->id != id) continue; + element.children.erase(it); + return true; + } + return false; + } + + Element* element_first_get(Element& element, ElementType type) + { + if (element.type == type) return &element; + for (auto& child : element.children) + if (auto result = element_first_get(child, type); result) return result; + return nullptr; + } + + const Element* element_first_get(const Element& element, ElementType type) + { + if (element.type == type) return &element; + for (const auto& child : element.children) + if (auto result = element_first_get(child, type); result) return result; + return nullptr; + } + + float interpolation_factor(Interpolation interpolation, float value) + { + value = glm::clamp(value, 0.0f, 1.0f); + if (interpolation == Interpolation::LINEAR) return value; + if (interpolation == Interpolation::EASE_IN) return value * value; + if (interpolation == Interpolation::EASE_OUT) return 1.0f - ((1.0f - value) * (1.0f - value)); + if (interpolation == Interpolation::EASE_IN_OUT) + return value < 0.5f ? (2.0f * value * value) : (1.0f - std::pow(-2.0f * value + 2.0f, 2.0f) * 0.5f); + return 0.0f; + } + + bool is_track(const Element& element) + { + return element.type == ElementType::ROOT_ANIMATION || element.type == ElementType::LAYER_ANIMATION || + element.type == ElementType::NULL_ANIMATION || element.type == ElementType::TRIGGERS; + } + + ElementType track_frame_type_get(const Element& track) + { + return track.type == ElementType::TRIGGERS ? ElementType::TRIGGER : ElementType::FRAME; + } + + ElementType item_type_to_track_type_get(ItemType type) + { + if (type == ItemType::ROOT) return ElementType::ROOT_ANIMATION; + if (type == ItemType::LAYER) return ElementType::LAYER_ANIMATION; + if (type == ItemType::NULL_) return ElementType::NULL_ANIMATION; + if (type == ItemType::TRIGGER) return ElementType::TRIGGERS; + return ElementType::UNKNOWN; + } + + ElementType item_type_to_container_type_get(ItemType type) + { + if (type == ItemType::LAYER) return ElementType::LAYER_ANIMATIONS; + if (type == ItemType::NULL_) return ElementType::NULL_ANIMATIONS; + return ElementType::UNKNOWN; + } + + int track_id_get(const Element& track) + { + if (track.type == ElementType::LAYER_ANIMATION) return track.layerId; + if (track.type == ElementType::NULL_ANIMATION) return track.nullId; + return -1; + } + + template void tracks_each(Element& parent, ElementType trackType, Callback&& callback) + { + for (auto& child : parent.children) + { + if (child.type == trackType) + callback(child); + else if (child.type == ElementType::GROUP) + tracks_each(child, trackType, callback); + } + } + + template void tracks_each(const Element& parent, ElementType trackType, Callback&& callback) + { + for (const auto& child : parent.children) + { + if (child.type == trackType) + callback(child); + else if (child.type == ElementType::GROUP) + tracks_each(child, trackType, callback); + } + } + + Element* track_find(Element& parent, ElementType trackType, int id) + { + for (auto& child : parent.children) + { + if (child.type == trackType && track_id_get(child) == id) return &child; + if (child.type == ElementType::GROUP) + if (auto found = track_find(child, trackType, id)) return found; + } + return nullptr; + } + + const Element* track_find(const Element& parent, ElementType trackType, int id) + { + for (const auto& child : parent.children) + { + if (child.type == trackType && track_id_get(child) == id) return &child; + if (child.type == ElementType::GROUP) + if (auto found = track_find(child, trackType, id)) return found; + } + return nullptr; + } + + bool is_track_group_visible(const Element& container, const Element& track) + { + if (track.groupId == -1) return true; + for (const auto& child : container.children) + if (child.type == ElementType::GROUP && child.id == track.groupId) return child.isVisible; + return true; + } + + Element* animation_item_get(Element& animation, ItemType type, int id) + { + auto trackType = item_type_to_track_type_get(type); + if (type == ItemType::ROOT || type == ItemType::TRIGGER) return child_first_get(animation, trackType); + + auto container = child_first_get(animation, item_type_to_container_type_get(type)); + return container ? track_find(*container, trackType, id) : nullptr; + } + + const Element* animation_item_get(const Element& animation, ItemType type, int id) + { + auto trackType = item_type_to_track_type_get(type); + if (type == ItemType::ROOT || type == ItemType::TRIGGER) return child_first_get(animation, trackType); + + auto container = child_first_get(animation, item_type_to_container_type_get(type)); + return container ? track_find(*container, trackType, id) : nullptr; + } + + Element* track_frame_get(Element& track, int index) + { + if (index < 0) return nullptr; + auto frameType = track_frame_type_get(track); + int frameIndex{}; + for (auto& frame : track.children) + { + if (frame.type != frameType) continue; + if (frameIndex == index) return &frame; + ++frameIndex; + } + return nullptr; + } + + const Element* track_frame_get(const Element& track, int index) + { + if (index < 0) return nullptr; + auto frameType = track_frame_type_get(track); + int frameIndex{}; + for (const auto& frame : track.children) + { + if (frame.type != frameType) continue; + if (frameIndex == index) return &frame; + ++frameIndex; + } + return nullptr; + } + + Element frame_generate(const Element& track, float time) + { + auto frame = element_make(track_frame_type_get(track)); + frame.isVisible = false; + if (track.children.empty()) return frame; + + time = std::max(time, 0.0f); + + const Element* frameNext = nullptr; + int durationCurrent{}; + int durationNext{}; + auto frameType = track_frame_type_get(track); + + for (int i = 0; i < (int)track.children.size(); ++i) + { + const auto& iFrame = track.children[i]; + if (iFrame.type != frameType) continue; + + if (frameType == ElementType::TRIGGER) + { + if ((int)time == iFrame.atFrame) + { + frame = iFrame; + break; + } + continue; + } + + frame = iFrame; + durationNext += frame.duration; + + if (time >= durationCurrent && time < durationNext) + { + for (int next = i + 1; next < (int)track.children.size(); ++next) + if (track.children[next].type == ElementType::FRAME) + { + frameNext = &track.children[next]; + break; + } + break; + } + + durationCurrent += frame.duration; + } + + if (frameType != ElementType::TRIGGER && frame.interpolation != Interpolation::NONE && frameNext && + frame.duration > 1) + { + auto amount = + interpolation_factor(frame.interpolation, (time - durationCurrent) / (durationNext - durationCurrent)); + frame.rotation = glm::mix(frame.rotation, frameNext->rotation, amount); + frame.position = glm::mix(frame.position, frameNext->position, amount); + frame.scale = glm::mix(frame.scale, frameNext->scale, amount); + frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, amount); + frame.tint = glm::mix(frame.tint, frameNext->tint, amount); + } + + return frame; + } + + int frame_index_from_at_frame_get(const Element& track, int atFrame) + { + int index{}; + for (const auto& frame : track.children) + { + if (frame.type != ElementType::TRIGGER) continue; + if (frame.atFrame == atFrame) return index; + ++index; + } + return -1; + } + + int frame_index_from_time_get(const Element& track, float time) + { + if (track.type == ElementType::TRIGGERS) return frame_index_from_at_frame_get(track, (int)time); + + auto frameType = track_frame_type_get(track); + int frameCount{}; + for (const auto& frame : track.children) + if (frame.type == frameType) ++frameCount; + if (frameCount == 0) return -1; + if (time <= 0.0f) return 0; + + float duration{}; + int index{}; + for (const auto& frame : track.children) + { + if (frame.type != frameType) continue; + duration += frame.duration; + if (time < duration) return index; + ++index; + } + + return frameCount - 1; + } + + float frame_time_from_index_get(const Element& track, int index) + { + if (index < 0) return 0.0f; + auto frameType = track_frame_type_get(track); + float time{}; + int frameIndex{}; + for (const auto& frame : track.children) + { + if (frame.type != frameType) continue; + if (frameIndex == index) return frameType == ElementType::TRIGGER ? (float)frame.atFrame : time; + time += frame.duration; + ++frameIndex; + } + return 0.0f; + } + + void frame_bake(Element& track, int index, int interval, bool isRoundScale, bool isRoundRotation) + { + auto frame = track_frame_get(track, index); + if (!frame) return; + + auto original = *frame; + if (original.duration <= FRAME_DURATION_MIN) + { + frame->interpolation = Interpolation::NONE; + return; + } + + auto nextFrame = track_frame_get(track, index + 1); + auto next = nextFrame ? *nextFrame : original; + int duration{}; + int insertIndex = index; + interval = std::max(interval, FRAME_DURATION_MIN); + + while (duration < original.duration) + { + auto baked = original; + auto amount = interpolation_factor(original.interpolation, (float)duration / original.duration); + baked.duration = std::min(interval, original.duration - duration); + baked.interpolation = Interpolation::NONE; + baked.rotation = glm::mix(original.rotation, next.rotation, amount); + baked.position = glm::mix(original.position, next.position, amount); + baked.scale = glm::mix(original.scale, next.scale, amount); + baked.colorOffset = glm::mix(original.colorOffset, next.colorOffset, amount); + baked.tint = glm::mix(original.tint, next.tint, amount); + if (isRoundScale) baked.scale = glm::vec2(glm::ivec2(baked.scale)); + if (isRoundRotation) baked.rotation = (int)baked.rotation; + + if (insertIndex == index) + track.children[insertIndex] = baked; + else + track.children.insert(track.children.begin() + insertIndex, baked); + + duration += baked.duration; + ++insertIndex; + } + } + + void frames_generate_from_grid(Element& track, glm::ivec2 startPosition, glm::ivec2 size, glm::ivec2 pivot, + int columns, int count, int duration) + { + for (int i = 0; i < count; ++i) + { + auto frame = element_make(ElementType::FRAME); + frame.duration = duration; + frame.pivot = pivot; + frame.size = size; + frame.crop = startPosition + glm::ivec2(size.x * (i % columns), size.y * (i / columns)); + track.children.emplace_back(frame); + } + } + + void frames_sort_by_at_frame(Element& track) + { + std::sort(track.children.begin(), track.children.end(), + [](const Element& a, const Element& b) { return a.atFrame < b.atFrame; }); + } + + bool frames_deserialize(Element& track, const std::string& string, int start, std::set& indices, + std::string* errorString) + { + XMLDocument document{}; + if (document.Parse(string.c_str()) != XML_SUCCESS) + { + if (errorString) *errorString = document.ErrorStr(); + return false; + } + + auto frameType = track_frame_type_get(track); + auto first = + frameType == ElementType::TRIGGER ? document.FirstChildElement("Trigger") : document.FirstChildElement("Frame"); + if (!first) + { + if (errorString) *errorString = frameType == ElementType::TRIGGER ? "No valid trigger(s)." : "No valid frame(s)."; + return false; + } + + if (frameType == ElementType::FRAME) + { + start = std::clamp(start, 0, (int)track.children.size()); + int count{}; + for (auto element = first; element; element = element->NextSiblingElement("Frame")) + { + auto frame = element_read(element); + if (frame.type != ElementType::FRAME) continue; + auto index = start + count; + track.children.insert(track.children.begin() + index, frame); + indices.insert(index); + ++count; + } + return !indices.empty(); + } + + auto has_conflict = [&](int value) + { + for (auto& trigger : track.children) + if (trigger.type == ElementType::TRIGGER && trigger.atFrame == value) return true; + return false; + }; + + std::vector atFrames{}; + int count{}; + for (auto element = first; element; element = element->NextSiblingElement("Trigger")) + { + auto trigger = element_read(element); + if (trigger.type != ElementType::TRIGGER) continue; + trigger.atFrame = start + count; + while (has_conflict(trigger.atFrame)) + ++trigger.atFrame; + atFrames.push_back(trigger.atFrame); + track.children.push_back(trigger); + ++count; + } + frames_sort_by_at_frame(track); + for (auto atFrame : atFrames) + if (auto index = frame_index_from_at_frame_get(track, atFrame); index != -1) indices.insert(index); + return !indices.empty(); + } + + void frames_change(Element& track, FrameChange change, ItemType itemType, ChangeType changeType, + const std::set& selection) + { + const auto clamp_identity = [](auto value) { return value; }; + const auto clamp01 = [](auto value) { return glm::clamp(value, 0.0f, 1.0f); }; + const auto clamp_duration = [](int value) { return std::max(FRAME_DURATION_MIN, value); }; + + if (selection.empty()) return; + + auto apply_scalar_with_clamp = [&](auto& target, const auto& optionalValue, auto clampFunc) + { + if (!optionalValue) return; + auto value = *optionalValue; + + switch (changeType) + { + case ChangeType::ADJUST: + target = clampFunc(value); + break; + case ChangeType::ADD: + target = clampFunc(target + value); + break; + case ChangeType::SUBTRACT: + target = clampFunc(target - value); + break; + case ChangeType::MULTIPLY: + target = clampFunc(target * value); + break; + case ChangeType::DIVIDE: + if (value == decltype(value){}) return; + target = clampFunc(target / value); + break; + } + }; + + auto apply_scalar = [&](auto& target, const auto& optionalValue) + { apply_scalar_with_clamp(target, optionalValue, clamp_identity); }; + + for (auto index : selection) + { + auto frame = track_frame_get(track, index); + if (!frame) continue; + + if (change.isVisible) frame->isVisible = *change.isVisible; + if (change.interpolation) frame->interpolation = *change.interpolation; + if (change.isFlipX) frame->scale.x = -frame->scale.x; + if (change.isFlipY) frame->scale.y = -frame->scale.y; + + apply_scalar(frame->rotation, change.rotation); + apply_scalar_with_clamp(frame->duration, change.duration, clamp_duration); + + if (itemType == ItemType::LAYER) + { + apply_scalar(frame->crop.x, change.cropX); + apply_scalar(frame->crop.y, change.cropY); + apply_scalar(frame->pivot.x, change.pivotX); + apply_scalar(frame->pivot.y, change.pivotY); + apply_scalar(frame->size.x, change.sizeX); + apply_scalar(frame->size.y, change.sizeY); + if (change.regionId) frame->regionId = *change.regionId; + } + + apply_scalar(frame->position.x, change.positionX); + apply_scalar(frame->position.y, change.positionY); + apply_scalar(frame->scale.x, change.scaleX); + apply_scalar(frame->scale.y, change.scaleY); + apply_scalar_with_clamp(frame->colorOffset.x, change.colorOffsetR, clamp01); + apply_scalar_with_clamp(frame->colorOffset.y, change.colorOffsetG, clamp01); + apply_scalar_with_clamp(frame->colorOffset.z, change.colorOffsetB, clamp01); + apply_scalar_with_clamp(frame->tint.x, change.tintR, clamp01); + apply_scalar_with_clamp(frame->tint.y, change.tintG, clamp01); + apply_scalar_with_clamp(frame->tint.z, change.tintB, clamp01); + apply_scalar_with_clamp(frame->tint.w, change.tintA, clamp01); + } + } + + bool is_special_interpolated_frames(const Element& element) + { + if (element.type == ElementType::FRAME && element.interpolation != Interpolation::NONE && + element.interpolation != Interpolation::LINEAR) + return true; + + for (const auto& child : element.children) + if (is_special_interpolated_frames(child)) return true; + return false; + } + + void track_frames_bake(Element& track, int interval, bool isRoundScale, bool isRoundRotation) + { + for (int index = (int)track.children.size() - 1; index >= 0; --index) + { + auto original = track.children[index]; + if (original.type != ElementType::FRAME) continue; + if (original.interpolation == Interpolation::NONE || original.interpolation == Interpolation::LINEAR) continue; + + if (original.duration <= FRAME_DURATION_MIN) + { + track.children[index].interpolation = Interpolation::NONE; + continue; + } + + auto nextFrame = index + 1 < (int)track.children.size() && track.children[index + 1].type == ElementType::FRAME + ? track.children[index + 1] + : original; + int duration{}; + int insertIndex = index; + + while (duration < original.duration) + { + auto baked = original; + float amount = interpolation_factor(original.interpolation, (float)duration / original.duration); + baked.duration = std::min(interval, original.duration - duration); + baked.interpolation = Interpolation::NONE; + baked.rotation = glm::mix(original.rotation, nextFrame.rotation, amount); + baked.position = glm::mix(original.position, nextFrame.position, amount); + baked.scale = glm::mix(original.scale, nextFrame.scale, amount); + baked.colorOffset = glm::mix(original.colorOffset, nextFrame.colorOffset, amount); + baked.tint = glm::mix(original.tint, nextFrame.tint, amount); + if (isRoundScale) baked.scale = glm::vec2(glm::ivec2(baked.scale)); + if (isRoundRotation) baked.rotation = (int)baked.rotation; + + if (insertIndex == index) + track.children[insertIndex] = baked; + else + track.children.insert(track.children.begin() + insertIndex, baked); + + duration += baked.duration; + ++insertIndex; + } + } + } + + void special_interpolated_frames_bake(Element& element, int interval, bool isRoundScale, bool isRoundRotation) + { + if (is_track(element)) + { + track_frames_bake(element, interval, isRoundScale, isRoundRotation); + return; + } + + for (auto& child : element.children) + special_interpolated_frames_bake(child, interval, isRoundScale, isRoundRotation); + } + + void layer_animation_ids_remap(Element& element, const std::unordered_map& remap) + { + if (element.type == ElementType::LAYER_ANIMATION) + if (auto it = remap.find(element.layerId); it != remap.end()) element.layerId = it->second; + + for (auto& child : element.children) + layer_animation_ids_remap(child, remap); + } + + Anm2::Anm2() + { + root = element_make(ElementType::ANIMATED_ACTOR); + + auto info = element_make(ElementType::INFO); + info.createdOn = time::get("%m/%d/%Y %I:%M:%S %p"); + + auto content = element_make(ElementType::CONTENT); + content.children.push_back(element_make(ElementType::SPRITESHEETS)); + content.children.push_back(element_make(ElementType::LAYERS)); + content.children.push_back(element_make(ElementType::NULLS)); + content.children.push_back(element_make(ElementType::EVENTS)); + + root.children.push_back(std::move(info)); + root.children.push_back(std::move(content)); + root.children.push_back(element_make(ElementType::ANIMATIONS)); + } + + Anm2::Anm2(const std::filesystem::path& path, std::string* errorString) : Anm2() { load(path, errorString); } + + bool Anm2::load(const std::filesystem::path& path, std::string* errorString) + { + XMLDocument document{}; File file(path, "rb"); if (!file) { - if (errorString) *errorString = localize.get(ERROR_FILE_NOT_FOUND); + if (errorString) *errorString = "File not found."; isValid = false; - return; + return false; } if (document.LoadFile(file.get()) != XML_SUCCESS) { if (errorString) *errorString = document.ErrorStr(); isValid = false; - return; + return false; } - WorkingDirectory workingDirectory(path, WorkingDirectory::FILE); + auto rootElement = document.RootElement(); + if (!rootElement) + { + if (errorString) *errorString = "No root element."; + isValid = false; + return false; + } - 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); - - region_frames_sync(*this, true); + root = element_read(rootElement); + groups_flatten(root); + region_frames_sync(true); + isValid = true; + return true; } - XMLElement* Anm2::to_element(XMLDocument& document, Flags flags) + bool Anm2::save(const std::filesystem::path& path, std::string* errorString, Options options) const { - auto normalized = normalized_for_serialize(); - region_frames_sync(normalized, true); - auto element = document.NewElement("AnimatedActor"); - - normalized.info.serialize(document, element); - normalized.content.serialize(document, element, flags); - normalized.animations.serialize(document, element, flags); - - return element; - } - - bool Anm2::serialize(const std::filesystem::path& path, std::string* errorString, Flags flags, - bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) - { - XMLDocument document; - auto serialized = *this; - if (isBakeSpecialInterpolatedFramesOnSave) serialized.bake_special_interpolated_frames(1, isRoundScale, isRoundRotation); - document.InsertFirstChild(serialized.to_element(document, flags)); + XMLDocument document{}; + document.InsertFirstChild(to_element(document, options)); File file(path, "wb"); if (!file) { - if (errorString) *errorString = localize.get(ERROR_FILE_PERMISSIONS); + if (errorString) *errorString = "File permissions."; return false; } @@ -234,331 +1177,737 @@ namespace anm2ed::anm2 if (errorString) *errorString = document.ErrorStr(); return false; } + return true; } - std::string Anm2::to_string(Flags flags) + std::string Anm2::to_string(Options options) const { XMLDocument document{}; - document.InsertEndChild(to_element(document, flags)); + document.InsertEndChild(to_element(document, options)); return xml::document_to_string(document); } - uint64_t Anm2::hash() { return std::hash{}(to_string()); } + XMLElement* Anm2::to_element(XMLDocument& document, Options options) const + { + auto serialized = normalized_for_serialize(); + if (options.isBakeSpecialInterpolatedFrames) + serialized.special_interpolated_frames_bake(1, options.isRoundScale, options.isRoundRotation); + serialized.region_frames_sync(true); + return element_to_xml(document, serialized.root, flags_for(options.compatibility)); + } + + std::uint64_t Anm2::hash(Options options) const { return std::hash{}(to_string(options)); } + + bool Anm2::is_special_interpolated_frames() const { return ::anm2ed::is_special_interpolated_frames(root); } + + void Anm2::special_interpolated_frames_bake(int interval, bool isRoundScale, bool isRoundRotation) + { + ::anm2ed::special_interpolated_frames_bake(root, std::max(interval, FRAME_DURATION_MIN), isRoundScale, + isRoundRotation); + } + + void Anm2::region_frames_sync(bool isClearInvalid) + { + auto content = child_first_get(root, ElementType::CONTENT); + auto layers = content ? child_first_get(*content, ElementType::LAYERS) : nullptr; + auto spritesheets = content ? child_first_get(*content, ElementType::SPRITESHEETS) : nullptr; + auto animations = element_first_get(root, ElementType::ANIMATIONS); + if (!layers || !spritesheets || !animations) return; + + for (auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS); + if (!layerAnimations) continue; + + tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION, [&](Element& layerAnimation) + { + auto layer = child_id_get(*layers, ElementType::LAYER_ELEMENT, layerAnimation.layerId); + auto spritesheet = + layer ? child_id_get(*spritesheets, ElementType::SPRITESHEET, layer->spritesheetId) : nullptr; + if (!spritesheet) return; + + for (auto& frame : layerAnimation.children) + { + if (frame.type != ElementType::FRAME || frame.regionId == -1) continue; + auto region = child_id_get(*spritesheet, ElementType::REGION, frame.regionId); + if (!region) + { + if (isClearInvalid) frame.regionId = -1; + continue; + } + + frame.crop = region->crop; + frame.size = region->size; + frame.pivot = region->pivot; + } + }); + } + } Anm2 Anm2::normalized_for_serialize() const { auto normalized = *this; - auto sanitize_layer_order = [](Animation& animation) + groups_flatten(normalized.root); + + auto content = child_first_get(normalized.root, ElementType::CONTENT); + if (content) std::erase_if(content->children, [](const Element& element) { return element.tag == "Groups"; }); + auto layers = content ? child_first_get(*content, ElementType::LAYERS) : nullptr; + if (!layers) return normalized; + + std::unordered_map remap{}; + int nextId{}; + for (auto& layer : layers->children) { - std::vector sanitized{}; - sanitized.reserve(animation.layerAnimations.size()); - std::set seen{}; - - for (auto id : animation.layerOrder) - { - if (!animation.layerAnimations.contains(id)) continue; - if (!seen.insert(id).second) continue; - sanitized.push_back(id); - } - - std::vector missing{}; - missing.reserve(animation.layerAnimations.size()); - for (auto& id : animation.layerAnimations | std::views::keys) - if (!seen.contains(id)) missing.push_back(id); - - std::sort(missing.begin(), missing.end()); - sanitized.insert(sanitized.end(), missing.begin(), missing.end()); - animation.layerOrder = std::move(sanitized); - }; - std::unordered_map layerRemap{}; - - int normalizedID = 0; - for (auto& [layerID, layer] : content.layers) - { - layerRemap[layerID] = normalizedID; - ++normalizedID; - } - - normalized.content.layers.clear(); - for (auto& [layerID, layer] : content.layers) - normalized.content.layers[remap_id(layerRemap, layerID)] = layer; - - for (auto& animation : normalized.animations.items) - { - sanitize_layer_order(animation); - std::unordered_map layerAnimations{}; - std::vector layerOrder{}; - - for (auto layerID : animation.layerOrder) - { - auto mappedID = remap_id(layerRemap, layerID); - if (mappedID >= 0) layerOrder.push_back(mappedID); - } - - for (auto& [layerID, item] : animation.layerAnimations) - { - auto mappedID = remap_id(layerRemap, layerID); - if (mappedID >= 0) layerAnimations[mappedID] = item; - } - - animation.layerAnimations = std::move(layerAnimations); - animation.layerOrder = std::move(layerOrder); - sanitize_layer_order(animation); + if (layer.type != ElementType::LAYER_ELEMENT) continue; + remap[layer.id] = nextId; + layer.id = nextId++; } + layer_animation_ids_remap(normalized.root, remap); return normalized; } - Frame* Anm2::frame_get(int animationIndex, Type itemType, int frameIndex, int itemID) + Element* Anm2::element_get(ElementType type) { - if (auto item = item_get(animationIndex, itemType, itemID); item) - if (vector::in_bounds(item->frames, frameIndex)) return &item->frames[frameIndex]; + if (type == ElementType::ANIMATED_ACTOR) return &root; + if (type == ElementType::ANIMATIONS) return element_first_get(root, type); + + auto content = child_first_get(root, ElementType::CONTENT); + if (!content) return nullptr; + if (type == ElementType::CONTENT) return content; + if (auto container = child_first_get(*content, type)) return container; + + switch (type) + { + case ElementType::SPRITESHEETS: + case ElementType::LAYERS: + case ElementType::NULLS: + case ElementType::EVENTS: + case ElementType::SOUNDS: + content->children.push_back(element_make(type)); + return &content->children.back(); + default: + break; + } + + return element_first_get(root, type); + } + + const Element* Anm2::element_get(ElementType type) const + { + if (type == ElementType::ANIMATED_ACTOR) return &root; + if (type == ElementType::ANIMATIONS) return element_first_get(root, type); + + auto content = child_first_get(root, ElementType::CONTENT); + if (!content) return nullptr; + if (type == ElementType::CONTENT) return content; + if (auto container = child_first_get(*content, type)) return container; + return element_first_get(root, type); + } + + Element* Anm2::element_get(ElementType type, int id) + { + if (type == ElementType::ANIMATION) + { + auto animations = element_get(ElementType::ANIMATIONS); + if (!animations || id < 0) return nullptr; + int current{}; + for (auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + if (current == id) return &animation; + ++current; + } + return nullptr; + } + + auto container = element_get(element_container_type_get(type)); + return container ? child_id_get(*container, type, id) : nullptr; + } + + const Element* Anm2::element_get(ElementType type, int id) const + { + if (type == ElementType::ANIMATION) + { + auto animations = element_get(ElementType::ANIMATIONS); + if (!animations || id < 0) return nullptr; + int current{}; + for (const auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + if (current == id) return &animation; + ++current; + } + return nullptr; + } + + auto container = element_get(element_container_type_get(type)); + return container ? child_id_get(*container, type, id) : nullptr; + } + + Element* Anm2::element_get(int animationIndex, ItemType type, int id) + { + auto animation = element_get(ElementType::ANIMATION, animationIndex); + return animation ? animation_item_get(*animation, type, id) : nullptr; + } + + const Element* Anm2::element_get(int animationIndex, ItemType type, int id) const + { + auto animation = element_get(ElementType::ANIMATION, animationIndex); + return animation ? animation_item_get(*animation, type, id) : nullptr; + } + + Element* Anm2::element_get(int animationIndex, ItemType type, int frameIndex, int id) + { + auto item = element_get(animationIndex, type, id); + return item ? track_frame_get(*item, frameIndex) : nullptr; + } + + const Element* Anm2::element_get(int animationIndex, ItemType type, int frameIndex, int id) const + { + auto item = element_get(animationIndex, type, id); + return item ? track_frame_get(*item, frameIndex) : nullptr; + } + + std::set Anm2::element_unused(ElementType type) const + { + std::set used{}; + + if (type == ElementType::SPRITESHEET) + { + if (auto layers = element_get(ElementType::LAYERS)) + for (const auto& layer : layers->children) + if (layer.type == ElementType::LAYER_ELEMENT && layer.spritesheetId != -1) used.insert(layer.spritesheetId); + } + else if (auto animations = element_get(ElementType::ANIMATIONS)) + for (const auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + if (type == ElementType::EVENT_ELEMENT) + { + auto triggers = child_first_get(animation, ElementType::TRIGGERS); + if (!triggers) continue; + for (const auto& trigger : triggers->children) + if (trigger.type == ElementType::TRIGGER && trigger.eventId != -1) used.insert(trigger.eventId); + } + else if (type == ElementType::SOUND_ELEMENT) + { + auto triggers = child_first_get(animation, ElementType::TRIGGERS); + if (!triggers) continue; + for (const auto& trigger : triggers->children) + { + if (trigger.type != ElementType::TRIGGER) continue; + for (auto id : trigger.soundIds) + used.insert(id); + } + } + else if (type == ElementType::LAYER_ELEMENT || type == ElementType::NULL_ELEMENT) + { + auto containerType = + type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATIONS : ElementType::NULL_ANIMATIONS; + auto tracks = child_first_get(animation, containerType); + if (!tracks) continue; + auto trackType = + type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATION : ElementType::NULL_ANIMATION; + tracks_each(*tracks, trackType, + [&](const Element& track) + { + if (track.type == ElementType::LAYER_ANIMATION) + used.insert(track.layerId); + else if (track.type == ElementType::NULL_ANIMATION) + used.insert(track.nullId); + }); + } + } + + std::set unused{}; + if (auto container = element_get(element_container_type_get(type))) + for (const auto& element : container->children) + if (element.type == type && !used.contains(element.id)) unused.insert(element.id); + return unused; + } + + std::set Anm2::element_unused(ElementType type, const Element& animation) const + { + if (type != ElementType::LAYER_ELEMENT && type != ElementType::NULL_ELEMENT) return {}; + + std::set used{}; + auto containerType = + type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATIONS : ElementType::NULL_ANIMATIONS; + if (auto tracks = child_first_get(animation, containerType)) + { + auto trackType = type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATION : ElementType::NULL_ANIMATION; + tracks_each(*tracks, trackType, + [&](const Element& track) + { + if (track.type == ElementType::LAYER_ANIMATION) + used.insert(track.layerId); + else if (track.type == ElementType::NULL_ANIMATION) + used.insert(track.nullId); + }); + } + + std::set unused{}; + if (auto container = element_get(element_container_type_get(type))) + for (const auto& element : container->children) + if (element.type == type && !used.contains(element.id)) unused.insert(element.id); + return unused; + } + + std::set Anm2::element_unused(ElementType type, int parentId) const + { + if (type != ElementType::REGION) return {}; + + std::set used{}; + auto animations = element_first_get(root, ElementType::ANIMATIONS); + if (animations) + for (const auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS); + if (!layerAnimations) continue; + tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION, [&](const Element& layerAnimation) + { + for (const auto& frame : layerAnimation.children) + if (frame.type == ElementType::FRAME && frame.regionId != -1) used.insert(frame.regionId); + }); + } + + std::set unused{}; + if (auto spritesheet = element_get(ElementType::SPRITESHEET, parentId)) + for (const auto& region : spritesheet->children) + if (region.type == ElementType::REGION && !used.contains(region.id)) unused.insert(region.id); + return unused; + } + + bool Anm2::deserialize(ElementType type, const std::string& string, bool isAppend, std::string* errorString, + const std::filesystem::path& directory) + { + XMLDocument document{}; + if (document.Parse(string.c_str()) != XML_SUCCESS) + { + if (errorString) *errorString = document.ErrorStr(); + return false; + } + + auto tag = element_tag_get(type); + if (tag.empty() || !document.FirstChildElement(tag.data())) + { + if (errorString) *errorString = std::format("No valid {}(s).", tag); + return false; + } + + auto containerType = element_container_type_get(type); + auto container = element_get(containerType); + if (!container) + { + if (errorString) *errorString = std::format("No {} container.", element_tag_get(containerType)); + return false; + } + + std::optional workingDirectory{}; + if ((type == ElementType::SOUND_ELEMENT || type == ElementType::SPRITESHEET) && !directory.empty()) + workingDirectory.emplace(directory); + + for (auto xmlElement = document.FirstChildElement(tag.data()); xmlElement; + xmlElement = xmlElement->NextSiblingElement(tag.data())) + { + auto element = element_read(xmlElement); + if (element.type != type) continue; + if (isAppend) + element.id = element_child_next_id_get(*container, type); + else + element_child_id_erase(*container, type, element.id); + if (type == ElementType::SOUND_ELEMENT || type == ElementType::SPRITESHEET) + element.path = path::lower_case_backslash_handle(element.path); + container->children.push_back(element); + } + + return true; + } + + bool Anm2::regions_deserialize(int spritesheetId, const std::string& string, bool isAppend, std::string* errorString) + { + XMLDocument document{}; + if (document.Parse(string.c_str()) != XML_SUCCESS) + { + if (errorString) *errorString = document.ErrorStr(); + return false; + } + + if (!document.FirstChildElement("Region")) + { + if (errorString) *errorString = "No valid region(s)."; + return false; + } + + auto spritesheet = element_get(ElementType::SPRITESHEET, spritesheetId); + if (!spritesheet) + { + if (errorString) *errorString = "No spritesheet."; + return false; + } + + for (auto element = document.FirstChildElement("Region"); element; element = element->NextSiblingElement("Region")) + { + auto region = element_read(element); + if (region.type != ElementType::REGION) continue; + if (isAppend) + region.id = element_child_next_id_get(*spritesheet, ElementType::REGION); + else + element_child_id_erase(*spritesheet, ElementType::REGION, region.id); + spritesheet->children.push_back(region); + } + + return true; + } + + Element Anm2::frame_effective(int layerId, const Element& frame) const + { + auto resolved = frame; + if (frame.regionId == -1) return resolved; + + auto layer = element_get(ElementType::LAYER_ELEMENT, layerId); + if (!layer) return resolved; + + auto spritesheet = element_get(ElementType::SPRITESHEET, layer->spritesheetId); + if (!spritesheet) return resolved; + + auto region = element_child_id_get(*spritesheet, ElementType::REGION, frame.regionId); + if (!region) return resolved; + + resolved.crop = region->crop; + resolved.size = region->size; + resolved.pivot = region->pivot; + return resolved; + } + + glm::vec4 Anm2::animation_rect(const Element& animation, bool isRootTransform) const + { + constexpr glm::ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; + + 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 isAny{}; + + for (float t = 0.0f; t < (float)animation.frameNum; t += 1.0f) + { + glm::mat4 transform(1.0f); + + auto root = animation_item_get(animation, ItemType::ROOT); + if (isRootTransform && root) + { + auto rootFrame = frame_generate(*root, t); + transform *= math::quad_model_parent_get(rootFrame.position, {}, math::percent_to_unit(rootFrame.scale), + rootFrame.rotation); + } + + auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS); + if (!layerAnimations) continue; + + tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION, [&](const Element& layerAnimation) + { + if (!layerAnimation.isVisible || !is_track_group_visible(*layerAnimations, layerAnimation)) return; + + auto frame = frame_effective(layerAnimation.layerId, frame_generate(layerAnimation, t)); + if (frame.size == glm::vec2() || !frame.isVisible) return; + + 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) + { + auto world = layerTransform * glm::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); + isAny = true; + } + }); + } + + if (!isAny) return glm::vec4(-1.0f); + return {minX, minY, maxX - minX, maxY - minY}; + } + + Element* animation_container_get(Element& animation, ElementType type) + { + if (auto container = child_first_get(animation, type)) return container; + animation.children.push_back(element_make(type)); + return &animation.children.back(); + } + + int Anm2::layer_animation_add(int animationIndex, int id, int insertBeforeId, std::string name, int spritesheetId, + types::destination::Type destination) + { + auto layers = element_get(ElementType::LAYERS); + if (!layers) return -1; + + id = id == -1 ? element_child_next_id_get(*layers, ElementType::LAYER_ELEMENT) : id; + auto layer = element_child_id_get(*layers, ElementType::LAYER_ELEMENT, id); + if (!layer) + { + layers->children.push_back(element_make(ElementType::LAYER_ELEMENT)); + layer = &layers->children.back(); + layer->id = id; + } + + if (!name.empty()) layer->name = name; + layer->spritesheetId = element_get(ElementType::SPRITESHEET, spritesheetId) ? spritesheetId : 0; + + auto add = [&](Element& animation) + { + if (animation_item_get(animation, ItemType::LAYER, id)) return; + auto layerAnimations = animation_container_get(animation, ElementType::LAYER_ANIMATIONS); + auto item = element_make(ElementType::LAYER_ANIMATION); + item.layerId = id; + + if (insertBeforeId != -1) + for (auto it = layerAnimations->children.begin(); it != layerAnimations->children.end(); ++it) + if (it->type == ElementType::LAYER_ANIMATION && it->layerId == insertBeforeId) + { + layerAnimations->children.insert(it, item); + return; + } + + layerAnimations->children.push_back(item); + }; + + if (destination == types::destination::ALL) + { + if (auto animations = element_get(ElementType::ANIMATIONS)) + for (auto& animation : animations->children) + if (animation.type == ElementType::ANIMATION) add(animation); + } + else if (auto animation = element_get(ElementType::ANIMATION, animationIndex)) + add(*animation); + + return id; + } + + int Anm2::null_animation_add(int animationIndex, int id, std::string name, bool isShowRect, + types::destination::Type destination) + { + auto nulls = element_get(ElementType::NULLS); + if (!nulls) return -1; + + id = id == -1 ? element_child_next_id_get(*nulls, ElementType::NULL_ELEMENT) : id; + auto null = element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id); + if (!null) + { + nulls->children.push_back(element_make(ElementType::NULL_ELEMENT)); + null = &nulls->children.back(); + null->id = id; + } + + if (!name.empty()) null->name = name; + null->isShowRect = isShowRect; + + auto add = [&](Element& animation) + { + if (animation_item_get(animation, ItemType::NULL_, id)) return; + auto nullAnimations = animation_container_get(animation, ElementType::NULL_ANIMATIONS); + auto item = element_make(ElementType::NULL_ANIMATION); + item.nullId = id; + nullAnimations->children.push_back(item); + }; + + if (destination == types::destination::ALL) + { + if (auto animations = element_get(ElementType::ANIMATIONS)) + for (auto& animation : animations->children) + if (animation.type == ElementType::ANIMATION) add(animation); + } + else if (auto animation = element_get(ElementType::ANIMATION, animationIndex)) + add(*animation); + + return id; + } + + bool Anm2::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 (errorString) *errorString = document.ErrorStr(); + return false; + } + + if (!document.FirstChildElement("Animation")) + { + if (errorString) *errorString = "No valid animation(s)."; + return false; + } + + auto animations = element_get(ElementType::ANIMATIONS); + if (!animations) + { + if (errorString) *errorString = "No animations container."; + return false; + } + + start = std::clamp(start, 0, (int)animations->children.size()); + int count{}; + for (auto element = document.FirstChildElement("Animation"); element; + element = element->NextSiblingElement("Animation")) + { + auto index = start + count; + animations->children.insert(animations->children.begin() + index, element_read(element)); + indices.insert(index); + ++count; + } + + return true; + } + + int track_length_get(const Element& track) + { + int length{}; + if (track.type == ElementType::TRIGGERS) + { + for (const auto& trigger : track.children) + if (trigger.type == ElementType::TRIGGER) length = std::max(length, trigger.atFrame); + return length; + } + + for (const auto& frame : track.children) + if (frame.type == ElementType::FRAME) length += frame.duration; + return length; + } + + int animation_length_get(const Element& animation) + { + int length{}; + if (auto rootAnimation = child_first_get(animation, ElementType::ROOT_ANIMATION)) + length = std::max(length, track_length_get(*rootAnimation)); + if (auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS)) + tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION, + [&](const Element& track) { length = std::max(length, track_length_get(track)); }); + if (auto nullAnimations = child_first_get(animation, ElementType::NULL_ANIMATIONS)) + tracks_each(*nullAnimations, ElementType::NULL_ANIMATION, + [&](const Element& track) { length = std::max(length, track_length_get(track)); }); + if (auto triggers = child_first_get(animation, ElementType::TRIGGERS)) + length = std::max(length, track_length_get(*triggers)); + return std::max(length, FRAME_DURATION_MIN); + } + + Element* track_get(Element& tracks, const Element& source) + { + for (auto& track : tracks.children) + { + if (track.type == ElementType::GROUP) + if (auto result = track_get(track, source)) return result; + if (track.type != source.type) continue; + if (track.type == ElementType::LAYER_ANIMATION && track.layerId == source.layerId) return &track; + if (track.type == ElementType::NULL_ANIMATION && track.nullId == source.nullId) return &track; + } return nullptr; } - void Anm2::merge(const Anm2& source, const std::filesystem::path& destinationDirectory, - const std::filesystem::path& sourceDirectory) + void track_merge(Element& destination, const Element& source, types::merge::Type type) { - using util::map::next_id_get; - - auto remap_path = [&](const std::filesystem::path& original) -> std::filesystem::path + switch (type) { - if (destinationDirectory.empty()) return original; - std::error_code ec{}; - std::filesystem::path absolute{}; - bool hasAbsolute = false; - - if (!original.empty()) - { - if (original.is_absolute()) - { - absolute = original; - hasAbsolute = true; - } - else if (!sourceDirectory.empty()) - { - absolute = std::filesystem::weakly_canonical(sourceDirectory / original, ec); - if (ec) - { - ec.clear(); - absolute = sourceDirectory / original; - } - hasAbsolute = true; - } - } - - if (!hasAbsolute) return original; - - auto relative = std::filesystem::relative(absolute, destinationDirectory, ec); - if (!ec) return relative; - ec.clear(); - try - { - return std::filesystem::relative(absolute, destinationDirectory); - } - catch (const std::filesystem::filesystem_error&) - { - return original.empty() ? absolute : original; - } - return original; - }; - - std::unordered_map spritesheetRemap{}; - std::unordered_map layerRemap{}; - std::unordered_map nullRemap{}; - std::unordered_map eventRemap{}; - std::unordered_map soundRemap{}; - - // Spritesheets - for (auto& [sourceID, sprite] : source.content.spritesheets) - { - auto sheet = sprite; - sheet.path = remap_path(sheet.path); - if (!destinationDirectory.empty() && !sheet.path.empty()) sheet.reload(destinationDirectory); - - int destinationID = next_id_get(content.spritesheets); - content.spritesheets[destinationID] = std::move(sheet); - spritesheetRemap[sourceID] = destinationID; - } - - // Sounds - for (auto& [sourceID, soundEntry] : source.content.sounds) - { - auto sound = soundEntry; - sound.path = remap_path(sound.path); - if (!destinationDirectory.empty() && !sound.path.empty()) sound.reload(destinationDirectory); - - int destinationID = -1; - for (auto& [id, existing] : content.sounds) - if (existing.path == sound.path) - { - destinationID = id; - existing = sound; - break; - } - - if (destinationID == -1) - { - destinationID = next_id_get(content.sounds); - content.sounds[destinationID] = sound; - } - soundRemap[sourceID] = destinationID; - } - - auto find_by_name = [](auto& container, const std::string& name) -> int - { - for (auto& [id, value] : container) - if (value.name == name) return id; - return -1; - }; - - // Layers - for (auto& [sourceID, sourceLayer] : source.content.layers) - { - auto layer = sourceLayer; - layer.spritesheetID = remap_id(spritesheetRemap, layer.spritesheetID); - - int destinationID = find_by_name(content.layers, layer.name); - if (destinationID != -1) - content.layers[destinationID] = layer; - else - { - destinationID = next_id_get(content.layers); - content.layers[destinationID] = layer; - } - layerRemap[sourceID] = destinationID; - } - - // Nulls - for (auto& [sourceID, sourceNull] : source.content.nulls) - { - auto null = sourceNull; - int destinationID = find_by_name(content.nulls, null.name); - if (destinationID != -1) - content.nulls[destinationID] = null; - else - { - destinationID = next_id_get(content.nulls); - content.nulls[destinationID] = null; - } - nullRemap[sourceID] = destinationID; - } - - // Events - for (auto& [sourceID, sourceEvent] : source.content.events) - { - auto event = sourceEvent; - - int destinationID = find_by_name(content.events, event.name); - if (destinationID != -1) - content.events[destinationID] = event; - else - { - destinationID = next_id_get(content.events); - content.events[destinationID] = event; - } - eventRemap[sourceID] = destinationID; - } - - auto remap_item = [&](Item& item) - { - for (auto& frame : item.frames) - { - for (auto& soundID : frame.soundIDs) - soundID = remap_id(soundRemap, soundID); - frame.eventID = remap_id(eventRemap, frame.eventID); - } - }; - - auto build_animation = [&](const Animation& incoming) -> Animation - { - Animation remapped{}; - remapped.name = incoming.name; - remapped.frameNum = incoming.frameNum; - remapped.isLoop = incoming.isLoop; - remapped.rootAnimation = incoming.rootAnimation; - remapped.triggers = incoming.triggers; - remap_item(remapped.rootAnimation); - remap_item(remapped.triggers); - - for (auto layerID : incoming.layerOrder) - { - auto mapped = remap_id(layerRemap, layerID); - if (mapped >= 0 && - std::find(remapped.layerOrder.begin(), remapped.layerOrder.end(), mapped) == remapped.layerOrder.end()) - remapped.layerOrder.push_back(mapped); - } - - for (auto& [layerID, item] : incoming.layerAnimations) - { - auto mapped = remap_id(layerRemap, layerID); - if (mapped < 0) continue; - auto copy = item; - remap_item(copy); - remapped.layerAnimations[mapped] = std::move(copy); - if (std::find(remapped.layerOrder.begin(), remapped.layerOrder.end(), mapped) == remapped.layerOrder.end()) - remapped.layerOrder.push_back(mapped); - } - - for (auto& [nullID, item] : incoming.nullAnimations) - { - auto mapped = remap_id(nullRemap, nullID); - if (mapped < 0) continue; - auto copy = item; - remap_item(copy); - remapped.nullAnimations[mapped] = std::move(copy); - } - - remap_item(remapped.triggers); - return remapped; - }; - - auto find_animation = [&](const std::string& name) -> Animation* - { - for (auto& animation : animations.items) - if (animation.name == name) return &animation; - return nullptr; - }; - - auto merge_item_map = [&](auto& destination, const auto& incoming) - { - for (auto& [id, item] : incoming) - { - if (!item.frames.empty()) - destination[id] = item; - else if (!destination.contains(id)) - destination[id] = item; - } - }; - - for (auto& animation : source.animations.items) - { - auto processed = build_animation(animation); - if (auto destination = find_animation(processed.name)) - { - destination->frameNum = std::max(destination->frameNum, processed.frameNum); - destination->isLoop = processed.isLoop; - if (!processed.rootAnimation.frames.empty()) destination->rootAnimation = processed.rootAnimation; - if (!processed.triggers.frames.empty()) destination->triggers = processed.triggers; - - merge_item_map(destination->layerAnimations, processed.layerAnimations); - merge_item_map(destination->nullAnimations, processed.nullAnimations); - - for (auto id : processed.layerOrder) - if (std::find(destination->layerOrder.begin(), destination->layerOrder.end(), id) == - destination->layerOrder.end()) - destination->layerOrder.push_back(id); - - destination->fit_length(); - } - else - animations.items.push_back(std::move(processed)); - } - - if (animations.defaultAnimation.empty() && !source.animations.defaultAnimation.empty()) - { - animations.defaultAnimation = source.animations.defaultAnimation; + case types::merge::APPEND: + destination.children.insert(destination.children.end(), source.children.begin(), source.children.end()); + break; + case types::merge::PREPEND: + destination.children.insert(destination.children.begin(), source.children.begin(), source.children.end()); + break; + case types::merge::REPLACE: + if (destination.children.size() < source.children.size()) destination.children.resize(source.children.size()); + for (int i = 0; i < (int)source.children.size(); ++i) + destination.children[i] = source.children[i]; + break; + case types::merge::IGNORE: + default: + break; } } + + void animation_tracks_merge(Element& destination, const Element& source, ElementType containerType, + types::merge::Type type) + { + auto sourceTracks = child_first_get(source, containerType); + if (!sourceTracks) return; + + auto destinationTracks = child_first_get(destination, containerType); + if (!destinationTracks) + { + destination.children.push_back(element_make(containerType)); + destinationTracks = &destination.children.back(); + } + + for (const auto& sourceTrack : sourceTracks->children) + { + if (sourceTrack.type == ElementType::GROUP) + destinationTracks->children.push_back(sourceTrack); + else if (auto destinationTrack = track_get(*destinationTracks, sourceTrack)) + track_merge(*destinationTrack, sourceTrack, type); + else + destinationTracks->children.push_back(sourceTrack); + } + } + + int Anm2::animations_merge(int target, std::set& sources, types::merge::Type type, bool isDeleteAfter) + { + auto animations = element_get(ElementType::ANIMATIONS); + auto targetAnimation = element_get(ElementType::ANIMATION, target); + if (!animations || !targetAnimation) return target; + + if (!targetAnimation->name.ends_with(ANIMATION_MERGED_SUFFIX)) + targetAnimation->name += std::string(ANIMATION_MERGED_SUFFIX); + + for (auto index : sources) + { + if (index == target) continue; + auto source = element_get(ElementType::ANIMATION, index); + targetAnimation = element_get(ElementType::ANIMATION, target); + if (!source || !targetAnimation) continue; + + if (auto sourceRoot = child_first_get(*source, ElementType::ROOT_ANIMATION)) + { + auto targetRoot = child_first_get(*targetAnimation, ElementType::ROOT_ANIMATION); + if (!targetRoot) + { + targetAnimation->children.push_back(element_make(ElementType::ROOT_ANIMATION)); + targetRoot = &targetAnimation->children.back(); + } + track_merge(*targetRoot, *sourceRoot, type); + } + + animation_tracks_merge(*targetAnimation, *source, ElementType::LAYER_ANIMATIONS, type); + animation_tracks_merge(*targetAnimation, *source, ElementType::NULL_ANIMATIONS, type); + + if (auto sourceTriggers = child_first_get(*source, ElementType::TRIGGERS)) + { + auto targetTriggers = child_first_get(*targetAnimation, ElementType::TRIGGERS); + if (!targetTriggers) + { + targetAnimation->children.push_back(element_make(ElementType::TRIGGERS)); + targetTriggers = &targetAnimation->children.back(); + } + track_merge(*targetTriggers, *sourceTriggers, type); + } + } + + int finalIndex = target; + if (isDeleteAfter) + for (auto it = sources.rbegin(); it != sources.rend(); ++it) + { + auto source = *it; + if (source == target || source < 0 || source >= (int)animations->children.size()) continue; + animations->children.erase(animations->children.begin() + source); + if (source < finalIndex) --finalIndex; + } + + if (auto finalAnimation = element_get(ElementType::ANIMATION, finalIndex)) + finalAnimation->frameNum = animation_length_get(*finalAnimation); + return finalIndex; + } } diff --git a/src/anm2/anm2.hpp b/src/anm2/anm2.hpp index 086fac7..1de462a 100644 --- a/src/anm2/anm2.hpp +++ b/src/anm2/anm2.hpp @@ -1,97 +1,359 @@ #pragma once +#include #include +#include +#include #include +#include + +#include #include +#include "icon.hpp" +#include "strings.hpp" #include "types.hpp" -#include "animations.hpp" -#include "content.hpp" -#include "info.hpp" +#define ANM2_ELEMENT_TYPES \ + X(UNKNOWN, "") \ + X(ANIMATED_ACTOR, "AnimatedActor") \ + X(INFO, "Info") \ + X(CONTENT, "Content") \ + X(SPRITESHEETS, "Spritesheets") \ + X(SPRITESHEET, "Spritesheet") \ + X(REGION, "Region") \ + X(LAYERS, "Layers") \ + X(LAYER_ELEMENT, "Layer") \ + X(NULLS, "Nulls") \ + X(NULL_ELEMENT, "Null") \ + X(EVENTS, "Events") \ + X(EVENT_ELEMENT, "Event") \ + X(SOUNDS, "Sounds") \ + X(SOUND_ELEMENT, "Sound") \ + X(ANIMATIONS, "Animations") \ + X(ANIMATION, "Animation") \ + X(ROOT_ANIMATION, "RootAnimation") \ + X(LAYER_ANIMATIONS, "LayerAnimations") \ + X(LAYER_ANIMATION, "LayerAnimation") \ + X(NULL_ANIMATIONS, "NullAnimations") \ + X(NULL_ANIMATION, "NullAnimation") \ + X(GROUP, "Group") \ + X(TRIGGERS, "Triggers") \ + X(FRAME, "Frame") \ + X(TRIGGER, "Trigger") -namespace anm2ed::anm2 +namespace anm2ed { + enum class ElementType + { +#define X(symbol, tag) symbol, + ANM2_ELEMENT_TYPES +#undef X + COUNT + }; + + enum class Interpolation + { + NONE, + LINEAR, + EASE_IN, + EASE_OUT, + EASE_IN_OUT, + COUNT + }; + + enum class Origin + { + CUSTOM, + TOP_LEFT, + CENTER, + COUNT + }; + + enum class Compatibility + { + ISAAC, + ANM2ED, + ANM2ED_LIMITED, + COUNT + }; + + enum class ItemType + { + NONE, + ROOT, + LAYER, + NULL_, + TRIGGER, + COUNT + }; + + inline constexpr int NONE = (int)ItemType::NONE; + inline constexpr int ROOT = (int)ItemType::ROOT; + inline constexpr int LAYER = (int)ItemType::LAYER; + inline constexpr int NULL_ = (int)ItemType::NULL_; + inline constexpr int TRIGGER = (int)ItemType::TRIGGER; + + inline constexpr int FRAME_DURATION_MIN = 1; + inline constexpr int FRAME_DURATION_MAX = 1000000; + inline constexpr int FRAME_NUM_MIN = 1; + inline constexpr int FRAME_NUM_MAX = FRAME_DURATION_MAX; + inline constexpr int FPS_MIN = 1; + inline constexpr int FPS_MAX = 120; + + inline constexpr int ISAAC = (int)Compatibility::ISAAC; + inline constexpr int ANM2ED = (int)Compatibility::ANM2ED; + inline constexpr int ANM2ED_LIMITED = (int)Compatibility::ANM2ED_LIMITED; + + enum class SpritesheetMergeOrigin + { + APPEND_RIGHT, + APPEND_BOTTOM, + COUNT + }; + + inline constexpr int APPEND_RIGHT = (int)SpritesheetMergeOrigin::APPEND_RIGHT; + inline constexpr int APPEND_BOTTOM = (int)SpritesheetMergeOrigin::APPEND_BOTTOM; + + inline const glm::vec4 ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f); + inline const glm::vec4 ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f); + inline const glm::vec4 ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f); + + inline const glm::vec4 LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f); + inline const glm::vec4 LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f); + inline const glm::vec4 LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f); + + inline const glm::vec4 NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f); + inline const glm::vec4 NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f); + inline const glm::vec4 NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f); + + inline const glm::vec4 TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f); + inline const glm::vec4 TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f); + inline const glm::vec4 TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f); + +#define ANM2_ITEM_TYPES \ + X(NONE, STRING_UNDEFINED, "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \ + X(ROOT, BASIC_ROOT, "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \ + X(LAYER, BASIC_LAYER_ANIMATION, "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, \ + LAYER_COLOR_HOVERED) \ + X(NULL_, BASIC_NULL_ANIMATION, "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, \ + NULL_COLOR_HOVERED) \ + X(TRIGGER, BASIC_TRIGGERS, "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE, \ + TRIGGER_COLOR_HOVERED) + + constexpr StringType TYPE_STRINGS[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string, + ANM2_ITEM_TYPES +#undef X + }; + + constexpr const char* TYPE_ITEM_STRINGS[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString, + ANM2_ITEM_TYPES +#undef X + }; + + constexpr resource::icon::Type TYPE_ICONS[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon, + ANM2_ITEM_TYPES +#undef X + }; + + inline const glm::vec4 TYPE_COLOR[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color, + ANM2_ITEM_TYPES +#undef X + }; + + inline const glm::vec4 TYPE_COLOR_ACTIVE[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive, + ANM2_ITEM_TYPES +#undef X + }; + + inline const glm::vec4 TYPE_COLOR_HOVERED[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered, + ANM2_ITEM_TYPES +#undef X + }; + + enum class ChangeType + { + ADJUST, + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE + }; + + enum Flag + { + NO_SOUNDS = 1 << 0, + NO_REGIONS = 1 << 1, + FRAME_NO_REGION_VALUES = 1 << 2, + INTERPOLATION_BOOL_ONLY = 1 << 3, + NO_GROUPS = 1 << 4 + }; + + using Flags = int; + + constexpr bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; } + + struct Options + { + Compatibility compatibility{Compatibility::ANM2ED}; + bool isBakeSpecialInterpolatedFrames{}; + bool isRoundScale{true}; + bool isRoundRotation{true}; + }; + + struct FrameChange + { + std::optional isVisible{}; + std::optional interpolation{}; + std::optional rotation{}; + std::optional duration{}; + std::optional regionId{}; + std::optional pivotX{}; + std::optional pivotY{}; + std::optional cropX{}; + std::optional cropY{}; + std::optional positionX{}; + std::optional positionY{}; + std::optional sizeX{}; + std::optional sizeY{}; + std::optional scaleX{}; + std::optional scaleY{}; + std::optional colorOffsetR{}; + std::optional colorOffsetG{}; + std::optional colorOffsetB{}; + std::optional tintR{}; + std::optional tintG{}; + std::optional tintB{}; + std::optional tintA{}; + std::optional isFlipX{}; + std::optional isFlipY{}; + }; + + struct Element + { + ElementType type{ElementType::UNKNOWN}; + std::string tag{}; + std::vector children{}; + std::string name{}; + std::string createdBy{"robot"}; + std::string createdOn{}; + std::filesystem::path path{}; + std::string defaultAnimation{}; + int id{-1}; + int layerId{-1}; + int nullId{-1}; + int spritesheetId{}; + int fps{30}; + int version{}; + int frameNum{1}; + int duration{1}; + int atFrame{-1}; + int eventId{-1}; + int regionId{-1}; + int soundId{-1}; + int groupId{-1}; + bool isLoop{true}; + bool isVisible{true}; + bool isShowRect{}; + bool isExpanded{true}; + Interpolation interpolation{Interpolation::NONE}; + Origin origin{Origin::CUSTOM}; + float rotation{}; + std::vector soundIds{}; + glm::vec2 pivot{}; + glm::vec2 crop{}; + glm::vec2 position{}; + glm::vec2 size{}; + glm::vec2 scale{100.0f, 100.0f}; + glm::vec3 colorOffset{}; + glm::vec4 tint{types::color::WHITE}; + }; + struct Reference { int animationIndex{-1}; - Type itemType{NONE}; + int itemType{(int)ItemType::NONE}; int itemID{-1}; int frameIndex{-1}; auto operator<=>(const Reference&) const = default; }; + Flags flags_for(Compatibility); + Element element_make(ElementType); + Element element_read(const tinyxml2::XMLElement*); + std::string element_to_string(const Element&, Flags = 0); + tinyxml2::XMLElement* element_to_xml(tinyxml2::XMLDocument&, const Element&, Flags = 0); + Element* element_child_first_get(Element&, ElementType); + const Element* element_child_first_get(const Element&, ElementType); + Element* element_child_id_get(Element&, ElementType, int); + const Element* element_child_id_get(const Element&, ElementType, int); + int element_child_next_id_get(const Element&, ElementType); + int element_child_max_id_get(const Element&, ElementType); + bool element_child_id_erase(Element&, ElementType, int); + Element* element_first_get(Element&, ElementType); + const Element* element_first_get(const Element&, ElementType); + Element* animation_item_get(Element&, ItemType, int = -1); + const Element* animation_item_get(const Element&, ItemType, int = -1); + Element* track_frame_get(Element&, int); + const Element* track_frame_get(const Element&, int); + Element frame_generate(const Element&, float); + int frame_index_from_at_frame_get(const Element&, int); + int frame_index_from_time_get(const Element&, float); + float frame_time_from_index_get(const Element&, int); + void frame_bake(Element&, int, int, bool, bool); + void frames_generate_from_grid(Element&, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); + bool frames_deserialize(Element&, const std::string&, int, std::set&, std::string* = nullptr); + void frames_sort_by_at_frame(Element&); + void frames_change(Element&, FrameChange, ItemType, ChangeType, const std::set&); + int track_length_get(const Element&); + int animation_length_get(const Element&); + class Anm2 { public: bool isValid{true}; - Info info{}; - Content content{}; - Animations animations{}; + Element root{}; Anm2(); - tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0); - bool serialize(const std::filesystem::path&, std::string* = nullptr, Flags = 0, bool = false, bool = true, - bool = true); - std::string to_string(Flags = 0); Anm2(const std::filesystem::path&, std::string* = nullptr); - uint64_t hash(); + + bool load(const std::filesystem::path&, std::string* = nullptr); + bool save(const std::filesystem::path&, std::string* = nullptr, Options = {}) const; + std::string to_string(Options = {}) const; + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Options = {}) const; + std::uint64_t hash(Options = {}) const; + bool is_special_interpolated_frames() const; + void special_interpolated_frames_bake(int, bool, bool); + void region_frames_sync(bool); Anm2 normalized_for_serialize() const; - Spritesheet* spritesheet_get(int); - bool spritesheet_add(const std::filesystem::path&, const std::filesystem::path&, int&); - bool spritesheet_pack(int, int); - bool regions_trim(int, const std::set&); - std::vector spritesheet_labels_get(); - std::vector spritesheet_ids_get(); - std::set spritesheets_unused(); - bool spritesheets_merge(const std::set&, SpritesheetMergeOrigin, bool, bool, origin::Type); - bool spritesheets_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type type, - std::string*); - std::vector region_labels_get(Spritesheet&); - std::vector region_ids_get(Spritesheet&); - std::set regions_unused(Spritesheet&); - void scan_and_set_regions(); - - void layer_add(int&); - std::set layers_unused(); - std::set layers_unused(Animation&); - bool layers_deserialize(const std::string&, types::merge::Type, std::string*); - - void null_add(int&); - std::set nulls_unused(); - std::set nulls_unused(Animation&); - bool nulls_deserialize(const std::string&, types::merge::Type, std::string*); - - void event_add(int&); - std::vector event_labels_get(); - std::vector event_ids_get(); - std::set events_unused(); - bool events_deserialize(const std::string&, types::merge::Type, std::string*); - - bool sound_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id); - std::vector sound_labels_get(); - std::vector sound_ids_get(); - std::set sounds_unused(); - bool sounds_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type, std::string*); - - Animation* animation_get(int); - std::vector animation_labels_get(); - int animations_merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); + Element* element_get(ElementType); + const Element* element_get(ElementType) const; + Element* element_get(ElementType, int); + const Element* element_get(ElementType, int) const; + Element* element_get(int, ItemType, int = -1); + const Element* element_get(int, ItemType, int = -1) const; + Element* element_get(int, ItemType, int, int); + const Element* element_get(int, ItemType, int, int) const; + std::set element_unused(ElementType) const; + std::set element_unused(ElementType, const Element&) const; + std::set element_unused(ElementType, int) const; + bool deserialize(ElementType, const std::string&, bool, std::string* = nullptr, const std::filesystem::path& = {}); + bool regions_deserialize(int, const std::string&, bool, std::string* = nullptr); + Element frame_effective(int, const Element&) const; + glm::vec4 animation_rect(const Element&, bool) const; + int layer_animation_add(int, int = -1, int = -1, std::string = {}, int = 0, + types::destination::Type = types::destination::ALL); + int null_animation_add(int, int = -1, std::string = {}, bool = false, + types::destination::Type = types::destination::ALL); bool animations_deserialize(const std::string&, int, std::set&, std::string* = nullptr); - Frame frame_effective(int, const Frame&) const; - glm::vec4 animation_rect(Animation&, bool) const; - bool has_special_interpolated_frames() const; - void bake_special_interpolated_frames(int, bool, bool); - - Item* item_get(int, Type, int = -1); - Reference layer_animation_add(Reference = {}, int = -1, std::string = {}, int = 0, - types::destination::Type = types::destination::ALL); - Reference null_animation_add(Reference = {}, std::string = {}, bool = false, - types::destination::Type = types::destination::ALL); - - Frame* frame_get(int, Type, int, int = -1); - void merge(const Anm2&, const std::filesystem::path&, const std::filesystem::path&); + int animations_merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); }; } diff --git a/src/anm2/anm2_animations.cpp b/src/anm2/anm2_animations.cpp deleted file mode 100644 index 5bce099..0000000 --- a/src/anm2/anm2_animations.cpp +++ /dev/null @@ -1,139 +0,0 @@ -#include "anm2.hpp" - -#include "vector_.hpp" - -using namespace anm2ed::util; -using namespace anm2ed::types; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - Animation* Anm2::animation_get(int animationIndex) { return vector::find(animations.items, animationIndex); } - - std::vector Anm2::animation_labels_get() - { - std::vector labels{}; - labels.emplace_back("None"); - for (auto& animation : animations.items) - labels.emplace_back(animation.name); - return labels; - } - - bool Anm2::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; - auto& animation = *animations.items.insert(animations.items.begin() + start + count, Animation(element)); - - for (auto& trigger : animation.triggers.frames) - if (!content.events.contains(trigger.eventID)) trigger.eventID = -1; - - indices.insert(index); - count++; - } - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - int Anm2::animations_merge(int target, std::set& sources, merge::Type type, bool isDeleteAfter) - { - auto& items = animations.items; - auto& animation = animations.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; - } - - animation.frameNum = animation.length(); - - return finalIndex; - } - -} diff --git a/src/anm2/anm2_events.cpp b/src/anm2/anm2_events.cpp deleted file mode 100644 index 13a7300..0000000 --- a/src/anm2/anm2_events.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "anm2.hpp" - -#include - -#include "map_.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::util; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - void Anm2::event_add(int& id) - { - id = map::next_id_get(content.events); - content.events[id] = Event(); - } - - std::vector Anm2::event_labels_get() - { - std::vector labels{}; - labels.emplace_back(localize.get(BASIC_NONE)); - for (auto& event : content.events | std::views::values) - labels.emplace_back(event.name); - return labels; - } - - std::vector Anm2::event_ids_get() - { - std::vector ids{}; - ids.emplace_back(-1); - for (auto& id : content.events | std::views::keys) - ids.emplace_back(id); - return ids; - } - - std::set Anm2::events_unused() - { - std::set used{}; - - for (auto& animation : animations.items) - for (auto& frame : animation.triggers.frames) - if (frame.eventID != -1) used.insert(frame.eventID); - - std::set unused{}; - for (auto& id : content.events | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - bool Anm2::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 event = Event(element, id); - if (type == merge::APPEND) id = map::next_id_get(content.events); - content.events[id] = event; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } -} diff --git a/src/anm2/anm2_items.cpp b/src/anm2/anm2_items.cpp deleted file mode 100644 index 47ba193..0000000 --- a/src/anm2/anm2_items.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "anm2.hpp" - -#include "map_.hpp" -#include "types.hpp" -#include "unordered_map_.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::util; - -namespace anm2ed::anm2 -{ - Item* Anm2::item_get(int animationIndex, Type type, int id) - { - if (Animation* animation = animation_get(animationIndex)) - { - switch (type) - { - case ROOT: - return &animation->rootAnimation; - case LAYER: - return unordered_map::find(animation->layerAnimations, id); - case NULL_: - return map::find(animation->nullAnimations, id); - case TRIGGER: - return &animation->triggers; - default: - return nullptr; - } - } - return nullptr; - } - - Reference Anm2::layer_animation_add(Reference reference, int insertBeforeID, std::string name, int spritesheetID, - destination::Type destination) - { - 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, bool insertBeforeReference) - { - animation->layerAnimations[id] = Item(); - - if (insertBeforeReference && insertBeforeID != -1) - { - auto it = std::find(animation->layerOrder.begin(), animation->layerOrder.end(), insertBeforeID); - if (it != animation->layerOrder.end()) - { - animation->layerOrder.insert(it, id); - return; - } - } - - animation->layerOrder.push_back(id); - }; - - if (destination == destination::ALL) - { - for (size_t index = 0; index < animations.items.size(); ++index) - { - auto& animation = animations.items[index]; - if (!animation.layerAnimations.contains(id)) add(&animation, id, true); - } - } - else if (destination == destination::THIS) - { - if (auto animation = animation_get(reference.animationIndex)) - if (!animation->layerAnimations.contains(id)) add(animation, id, true); - } - - return {reference.animationIndex, LAYER, id}; - } - - Reference Anm2::null_animation_add(Reference reference, std::string name, bool isShowRect, destination::Type destination) - { - 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; - null.isShowRect = isShowRect; - - auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); }; - - if (destination == destination::ALL) - { - for (auto& animation : animations.items) - if (!animation.nullAnimations.contains(id)) add(&animation, id); - } - else if (destination == destination::THIS) - { - if (auto animation = animation_get(reference.animationIndex)) - if (!animation->nullAnimations.contains(id)) add(animation, id); - } - - return {reference.animationIndex, NULL_, id}; - } -} diff --git a/src/anm2/anm2_layers.cpp b/src/anm2/anm2_layers.cpp deleted file mode 100644 index 1490abf..0000000 --- a/src/anm2/anm2_layers.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "anm2.hpp" - -#include - -#include "map_.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::util; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - void Anm2::layer_add(int& id) - { - id = map::next_id_get(content.layers); - content.layers[id] = Layer(); - } - - std::set Anm2::layers_unused() - { - std::set used{}; - std::set unused{}; - - 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::layers_unused(Animation& animation) - { - std::set unused{}; - - for (auto& id : content.layers | std::views::keys) - if (!animation.layerAnimations.contains(id)) unused.insert(id); - - return unused; - } - - bool Anm2::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(content.layers); - content.layers[id] = layer; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } -} diff --git a/src/anm2/anm2_nulls.cpp b/src/anm2/anm2_nulls.cpp deleted file mode 100644 index 57ae2e0..0000000 --- a/src/anm2/anm2_nulls.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "anm2.hpp" - -#include - -#include "map_.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::util; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - void Anm2::null_add(int& id) - { - id = map::next_id_get(content.nulls); - content.nulls[id] = Null(); - } - - std::set Anm2::nulls_unused() - { - std::set used{}; - std::set unused{}; - - 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::set Anm2::nulls_unused(Animation& animation) - { - std::set unused{}; - - for (auto& id : content.nulls | std::views::keys) - if (!animation.nullAnimations.contains(id)) unused.insert(id); - - return unused; - } - - bool Anm2::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 null = Null(element, id); - if (type == merge::APPEND) id = map::next_id_get(content.nulls); - content.nulls[id] = null; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } -} diff --git a/src/anm2/anm2_sounds.cpp b/src/anm2/anm2_sounds.cpp deleted file mode 100644 index 6c07273..0000000 --- a/src/anm2/anm2_sounds.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "anm2.hpp" - -#include - -#include "map_.hpp" -#include "path_.hpp" -#include "working_directory.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::util; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - bool Anm2::sound_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id) - { - id = map::next_id_get(content.sounds); - content.sounds[id] = Sound(directory, path); - return true; - } - - std::vector Anm2::sound_labels_get() - { - std::vector labels{}; - labels.emplace_back(localize.get(BASIC_NONE)); - for (auto& [id, sound] : content.sounds) - { - auto pathString = path::to_utf8(sound.path); - labels.emplace_back(std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString))); - } - return labels; - } - - std::vector Anm2::sound_ids_get() - { - std::vector ids{}; - ids.emplace_back(-1); - for (auto& [id, sound] : content.sounds) - ids.emplace_back(id); - return ids; - } - - std::set Anm2::sounds_unused() - { - std::set used; - for (auto& animation : animations.items) - for (auto& trigger : animation.triggers.frames) - for (auto& soundID : trigger.soundIDs) - if (content.sounds.contains(soundID)) used.insert(soundID); - - std::set unused; - for (auto& [id, sound] : content.sounds) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - bool Anm2::sounds_deserialize(const std::string& string, const std::filesystem::path& 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; - } - - 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(content.sounds); - content.sounds[id] = std::move(sound); - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } -} diff --git a/src/anm2/anm2_spritesheets.cpp b/src/anm2/anm2_spritesheets.cpp deleted file mode 100644 index 88dc4d7..0000000 --- a/src/anm2/anm2_spritesheets.cpp +++ /dev/null @@ -1,700 +0,0 @@ -#include "anm2.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include "map_.hpp" -#include "path_.hpp" -#include "working_directory.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::util; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - Spritesheet* Anm2::spritesheet_get(int id) { return map::find(content.spritesheets, id); } - - bool Anm2::spritesheet_add(const std::filesystem::path& directory, const std::filesystem::path& 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; - } - - bool Anm2::spritesheet_pack(int id, int padding) - { - const int packingPadding = std::max(0, padding); - - struct RectI - { - int x{}; - int y{}; - int w{}; - int h{}; - }; - - struct PackItem - { - int regionID{-1}; - int srcX{}; - int srcY{}; - int width{}; - int height{}; - int packWidth{}; - int packHeight{}; - }; - - class MaxRectsPacker - { - int width{}; - int height{}; - std::vector freeRects{}; - - static bool intersects(const RectI& a, const RectI& b) - { - return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y); - } - - static bool contains(const RectI& a, const RectI& b) - { - return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h; - } - - void split_free_rects(const RectI& used) - { - std::vector next{}; - next.reserve(freeRects.size() * 2); - - for (auto& free : freeRects) - { - if (!intersects(free, used)) - { - next.push_back(free); - continue; - } - - if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h}); - if (used.x + used.w < free.x + free.w) - next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h}); - if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y}); - if (used.y + used.h < free.y + free.h) - next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)}); - } - - freeRects = std::move(next); - } - - void prune_free_rects() - { - for (int i = 0; i < (int)freeRects.size(); i++) - { - if (freeRects[i].w <= 0 || freeRects[i].h <= 0) - { - freeRects.erase(freeRects.begin() + i--); - continue; - } - - for (int j = i + 1; j < (int)freeRects.size();) - { - if (contains(freeRects[i], freeRects[j])) - freeRects.erase(freeRects.begin() + j); - else if (contains(freeRects[j], freeRects[i])) - { - freeRects.erase(freeRects.begin() + i); - i--; - break; - } - else - j++; - } - } - } - - public: - MaxRectsPacker(int width, int height) : width(width), height(height), freeRects({{0, 0, width, height}}) {} - - bool insert(int width, int height, RectI& result) - { - int bestShort = std::numeric_limits::max(); - int bestLong = std::numeric_limits::max(); - RectI best{}; - bool found{}; - - for (auto& free : freeRects) - { - if (width > free.w || height > free.h) continue; - - int leftOverW = free.w - width; - int leftOverH = free.h - height; - int shortSide = std::min(leftOverW, leftOverH); - int longSide = std::max(leftOverW, leftOverH); - - if (shortSide < bestShort || (shortSide == bestShort && longSide < bestLong)) - { - bestShort = shortSide; - bestLong = longSide; - best = {free.x, free.y, width, height}; - found = true; - } - } - - if (!found) return false; - - result = best; - split_free_rects(best); - prune_free_rects(); - return true; - } - }; - - auto pack_regions = [&](const std::vector& items, int& packedWidth, int& packedHeight, - std::unordered_map& packedRects) - { - if (items.empty()) return false; - - int maxWidth{}; - int maxHeight{}; - int sumWidth{}; - int sumHeight{}; - int64_t totalArea{}; - for (auto& item : items) - { - maxWidth = std::max(maxWidth, item.packWidth); - maxHeight = std::max(maxHeight, item.packHeight); - sumWidth += item.packWidth; - sumHeight += item.packHeight; - totalArea += (int64_t)item.packWidth * item.packHeight; - } - - if (maxWidth <= 0 || maxHeight <= 0) return false; - - int bestSquareDelta = std::numeric_limits::max(); - int bestArea = std::numeric_limits::max(); - int bestWidth{}; - int bestHeight{}; - std::unordered_map bestRects{}; - - int startWidth = maxWidth; - int endWidth = std::max(startWidth, sumWidth); - int step = std::max(1, (endWidth - startWidth) / 512); - - for (int candidateWidth = startWidth; candidateWidth <= endWidth; candidateWidth += step) - { - int candidateHeightMin = std::max(maxHeight, (int)std::ceil((double)totalArea / candidateWidth)); - bool isValid{}; - int usedWidth{}; - int usedHeight{}; - std::unordered_map candidateRects{}; - - // Grow candidate height until this width can actually fit all rectangles. - for (int candidateHeight = candidateHeightMin; candidateHeight <= sumHeight; candidateHeight++) - { - MaxRectsPacker packer(candidateWidth, candidateHeight); - candidateRects.clear(); - isValid = true; - usedWidth = 0; - usedHeight = 0; - - for (auto& item : items) - { - RectI rect{}; - if (!packer.insert(item.packWidth, item.packHeight, rect)) - { - isValid = false; - break; - } - - candidateRects[item.regionID] = rect; - usedWidth = std::max(usedWidth, rect.x + rect.w); - usedHeight = std::max(usedHeight, rect.y + rect.h); - } - - if (isValid) break; - } - - if (!isValid) continue; - - int area = usedWidth * usedHeight; - int squareDelta = std::abs(usedWidth - usedHeight); - if (squareDelta < bestSquareDelta || (squareDelta == bestSquareDelta && area < bestArea)) - { - bestSquareDelta = squareDelta; - bestArea = area; - bestWidth = usedWidth; - bestHeight = usedHeight; - bestRects = std::move(candidateRects); - if (bestArea == totalArea && bestSquareDelta == 0) break; - } - } - - if (bestArea == std::numeric_limits::max()) return false; - - packedWidth = bestWidth; - packedHeight = bestHeight; - packedRects = std::move(bestRects); - return true; - }; - - if (!content.spritesheets.contains(id)) return false; - auto& spritesheet = content.spritesheets.at(id); - if (!spritesheet.texture.is_valid() || spritesheet.texture.pixels.empty()) return false; - if (spritesheet.regions.empty()) return false; - - std::vector items{}; - items.reserve(spritesheet.regions.size()); - - for (auto& [regionID, region] : spritesheet.regions) - { - auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size)); - auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size)); - auto size = glm::max(maxPoint - minPoint, glm::ivec2(1)); - int packWidth = size.x + packingPadding * 2; - int packHeight = size.y + packingPadding * 2; - items.push_back({regionID, minPoint.x, minPoint.y, size.x, size.y, packWidth, packHeight}); - } - - std::sort(items.begin(), items.end(), [](const PackItem& a, const PackItem& b) - { - int areaA = a.width * a.height; - int areaB = b.width * b.height; - if (areaA != areaB) return areaA > areaB; - return a.regionID < b.regionID; - }); - - int packedWidth{}; - int packedHeight{}; - std::unordered_map packedRects{}; - if (!pack_regions(items, packedWidth, packedHeight, packedRects)) return false; - if (packedWidth <= 0 || packedHeight <= 0) return false; - - auto textureSize = spritesheet.texture.size; - auto& sourcePixels = spritesheet.texture.pixels; - std::vector packedPixels((size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0); - - for (auto& item : items) - { - if (!packedRects.contains(item.regionID)) continue; - auto destinationRect = packedRects.at(item.regionID); - - for (int y = 0; y < item.height; y++) - { - for (int x = 0; x < item.width; x++) - { - int sourceX = item.srcX + x; - int sourceY = item.srcY + y; - int destinationX = destinationRect.x + packingPadding + x; - int destinationY = destinationRect.y + packingPadding + y; - - if (sourceX < 0 || sourceY < 0 || sourceX >= textureSize.x || sourceY >= textureSize.y) continue; - if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight) - continue; - - auto sourceIndex = ((size_t)sourceY * textureSize.x + sourceX) * resource::texture::CHANNELS; - auto destinationIndex = - ((size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS; - std::copy_n(sourcePixels.data() + sourceIndex, resource::texture::CHANNELS, - packedPixels.data() + destinationIndex); - } - } - } - - spritesheet.texture = resource::Texture(packedPixels.data(), {packedWidth, packedHeight}); - - for (auto& [regionID, region] : spritesheet.regions) - if (packedRects.contains(regionID)) - { - auto& rect = packedRects.at(regionID); - region.crop = {rect.x + packingPadding, rect.y + packingPadding}; - } - - return true; - } - - bool Anm2::regions_trim(int spritesheetID, const std::set& ids) - { - auto spritesheet = spritesheet_get(spritesheetID); - if (!spritesheet || !spritesheet->texture.is_valid() || spritesheet->texture.pixels.empty() || ids.empty()) - return false; - - auto& texture = spritesheet->texture; - bool changed{}; - - for (auto id : ids) - { - if (!spritesheet->regions.contains(id)) continue; - auto& region = spritesheet->regions.at(id); - - auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size)); - auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size)); - - int minX = std::max(0, minPoint.x); - int minY = std::max(0, minPoint.y); - int maxX = std::min(texture.size.x, maxPoint.x); - int maxY = std::min(texture.size.y, maxPoint.y); - - if (minX >= maxX || minY >= maxY) continue; - - int contentMinX = std::numeric_limits::max(); - int contentMinY = std::numeric_limits::max(); - int contentMaxX = std::numeric_limits::min(); - int contentMaxY = std::numeric_limits::min(); - - for (int y = minY; y < maxY; y++) - { - for (int x = minX; x < maxX; x++) - { - auto index = ((size_t)y * texture.size.x + x) * resource::texture::CHANNELS; - if (index + resource::texture::CHANNELS > texture.pixels.size()) continue; - - auto r = texture.pixels[index + 0]; - auto g = texture.pixels[index + 1]; - auto b = texture.pixels[index + 2]; - auto a = texture.pixels[index + 3]; - if (r == 0 && g == 0 && b == 0 && a == 0) continue; - - contentMinX = std::min(contentMinX, x); - contentMinY = std::min(contentMinY, y); - contentMaxX = std::max(contentMaxX, x); - contentMaxY = std::max(contentMaxY, y); - } - } - - if (contentMinX == std::numeric_limits::max()) continue; - - auto newCrop = glm::vec2(contentMinX, contentMinY); - auto newSize = glm::vec2(contentMaxX - contentMinX + 1, contentMaxY - contentMinY + 1); - if (region.crop != newCrop || region.size != newSize) - { - auto previousCrop = region.crop; - region.crop = newCrop; - region.size = newSize; - if (region.origin == Spritesheet::Region::TOP_LEFT) - region.pivot = {}; - else if (region.origin == Spritesheet::Region::ORIGIN_CENTER) - region.pivot = {static_cast(region.size.x / 2.0f), static_cast(region.size.y / 2.0f)}; - else - // Preserve the same texture-space pivot location when trimming shifts region crop. - region.pivot -= (region.crop - previousCrop); - changed = true; - } - } - - return changed; - } - - std::set Anm2::spritesheets_unused() - { - std::set used{}; - for (auto& layer : content.layers | std::views::values) - if (layer.is_spritesheet_valid()) used.insert(layer.spritesheetID); - - std::set unused{}; - for (auto& id : content.spritesheets | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - bool Anm2::spritesheets_merge(const std::set& ids, SpritesheetMergeOrigin mergeOrigin, bool isMakeRegions, - bool isMakePrimaryRegion, origin::Type regionOrigin) - { - if (ids.size() < 2) return false; - - auto baseId = *ids.begin(); - if (!content.spritesheets.contains(baseId)) return false; - for (auto id : ids) - if (!content.spritesheets.contains(id)) return false; - - auto& base = content.spritesheets.at(baseId); - if (!base.texture.is_valid()) return false; - - std::unordered_map offsets{}; - offsets[baseId] = {}; - - auto baseTextureSize = base.texture.size; - auto mergedTexture = base.texture; - for (auto id : ids) - { - if (id == baseId) continue; - - auto& spritesheet = content.spritesheets.at(id); - if (!spritesheet.texture.is_valid()) return false; - - offsets[id] = mergeOrigin == APPEND_RIGHT ? glm::ivec2(mergedTexture.size.x, 0) - : glm::ivec2(0, mergedTexture.size.y); - mergedTexture = resource::Texture::merge_append(mergedTexture, spritesheet.texture, - mergeOrigin == APPEND_RIGHT); - } - base.texture = std::move(mergedTexture); - - std::unordered_map> regionIdMap{}; - - if (isMakeRegions) - { - if (base.regionOrder.size() != base.regions.size()) - { - base.regionOrder.clear(); - base.regionOrder.reserve(base.regions.size()); - for (auto id : base.regions | std::views::keys) - base.regionOrder.push_back(id); - } - - if (isMakePrimaryRegion) - { - auto baseLocationRegionID = map::next_id_get(base.regions); - auto baseFilename = path::to_utf8(base.path.stem()); - auto baseLocationRegionName = baseFilename.empty() ? std::format("#{}", baseId) : baseFilename; - auto baseLocationRegionPivot = - regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(baseTextureSize) * 0.5f : glm::vec2(); - base.regions[baseLocationRegionID] = { - .name = baseLocationRegionName, - .crop = {}, - .pivot = glm::ivec2(baseLocationRegionPivot), - .size = baseTextureSize, - .origin = regionOrigin, - }; - base.regionOrder.push_back(baseLocationRegionID); - } - - for (auto id : ids) - { - if (id == baseId) continue; - - auto& source = content.spritesheets.at(id); - auto sheetOffset = offsets.at(id); - - auto locationRegionID = map::next_id_get(base.regions); - auto sourceFilename = path::to_utf8(source.path.stem()); - auto locationRegionName = sourceFilename.empty() ? std::format("#{}", id) : sourceFilename; - auto locationRegionPivot = - regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(source.texture.size) * 0.5f : glm::vec2(); - base.regions[locationRegionID] = { - .name = locationRegionName, - .crop = sheetOffset, - .pivot = glm::ivec2(locationRegionPivot), - .size = source.texture.size, - .origin = regionOrigin, - }; - base.regionOrder.push_back(locationRegionID); - - for (auto& [sourceRegionID, sourceRegion] : source.regions) - { - auto destinationRegionID = map::next_id_get(base.regions); - auto destinationRegion = sourceRegion; - destinationRegion.crop += sheetOffset; - base.regions[destinationRegionID] = destinationRegion; - base.regionOrder.push_back(destinationRegionID); - regionIdMap[id][sourceRegionID] = destinationRegionID; - } - } - } - - std::unordered_map layerSpritesheetBefore{}; - for (auto& [layerID, layer] : content.layers) - { - if (!ids.contains(layer.spritesheetID)) continue; - layerSpritesheetBefore[layerID] = layer.spritesheetID; - layer.spritesheetID = baseId; - } - - for (auto& animation : animations.items) - { - for (auto& [layerID, item] : animation.layerAnimations) - { - if (!layerSpritesheetBefore.contains(layerID)) continue; - auto sourceSpritesheetID = layerSpritesheetBefore.at(layerID); - if (sourceSpritesheetID == baseId) continue; - - for (auto& frame : item.frames) - { - if (frame.regionID == -1) continue; - - if (isMakeRegions && regionIdMap.contains(sourceSpritesheetID) && - regionIdMap.at(sourceSpritesheetID).contains(frame.regionID)) - frame.regionID = regionIdMap.at(sourceSpritesheetID).at(frame.regionID); - else - frame.regionID = -1; - } - } - } - - for (auto id : ids) - if (id != baseId) content.spritesheets.erase(id); - - return true; - } - - std::vector Anm2::spritesheet_labels_get() - { - std::vector labels{}; - for (auto& [id, spritesheet] : content.spritesheets) - { - auto pathString = path::to_utf8(spritesheet.path); - labels.emplace_back(std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathString))); - } - return labels; - } - - std::vector Anm2::spritesheet_ids_get() - { - std::vector ids{}; - for (auto& [id, spritesheet] : content.spritesheets) - ids.emplace_back(id); - return ids; - } - - std::vector Anm2::region_labels_get(Spritesheet& spritesheet) - { - auto rebuild_order = [&]() - { - spritesheet.regionOrder.clear(); - spritesheet.regionOrder.reserve(spritesheet.regions.size()); - for (auto id : spritesheet.regions | std::views::keys) - spritesheet.regionOrder.push_back(id); - }; - if (spritesheet.regionOrder.size() != spritesheet.regions.size()) - rebuild_order(); - else - { - bool isOrderValid = true; - for (auto id : spritesheet.regionOrder) - if (!spritesheet.regions.contains(id)) - { - isOrderValid = false; - break; - } - if (!isOrderValid) rebuild_order(); - } - - std::vector labels{}; - labels.emplace_back(localize.get(BASIC_NONE)); - for (auto id : spritesheet.regionOrder) - labels.emplace_back(spritesheet.regions.at(id).name); - return labels; - } - - std::vector Anm2::region_ids_get(Spritesheet& spritesheet) - { - auto rebuild_order = [&]() - { - spritesheet.regionOrder.clear(); - spritesheet.regionOrder.reserve(spritesheet.regions.size()); - for (auto id : spritesheet.regions | std::views::keys) - spritesheet.regionOrder.push_back(id); - }; - if (spritesheet.regionOrder.size() != spritesheet.regions.size()) - rebuild_order(); - else - { - bool isOrderValid = true; - for (auto id : spritesheet.regionOrder) - if (!spritesheet.regions.contains(id)) - { - isOrderValid = false; - break; - } - if (!isOrderValid) rebuild_order(); - } - - std::vector ids{}; - ids.emplace_back(-1); - for (auto id : spritesheet.regionOrder) - ids.emplace_back(id); - return ids; - } - - std::set Anm2::regions_unused(Spritesheet& spritesheet) - { - std::set used{}; - - for (auto& animation : animations.items) - { - for (auto& layerAnimation : animation.layerAnimations | std::views::values) - { - for (auto& frame : layerAnimation.frames) - if (frame.regionID != -1) used.insert(frame.regionID); - } - } - - std::set unused{}; - for (auto& id : spritesheet.regions | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - void Anm2::scan_and_set_regions() - { - for (auto& animation : animations.items) - { - for (auto& [layerID, item] : animation.layerAnimations) - { - auto layer = map::find(content.layers, layerID); - if (!layer) continue; - - auto spritesheet = spritesheet_get(layer->spritesheetID); - if (!spritesheet || spritesheet->regions.empty()) continue; - - for (auto& frame : item.frames) - { - if (frame.regionID != -1) continue; - - auto frameCrop = glm::ivec2(frame.crop); - auto frameSize = glm::ivec2(frame.size); - auto framePivot = glm::ivec2(frame.pivot); - - for (auto& [regionID, region] : spritesheet->regions) - { - if (glm::ivec2(region.crop) == frameCrop && glm::ivec2(region.size) == frameSize && - glm::ivec2(region.pivot) == framePivot) - { - frame.regionID = regionID; - break; - } - } - } - } - } - } - - bool Anm2::spritesheets_deserialize(const std::string& string, const std::filesystem::path& 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(content.spritesheets); - content.spritesheets[id] = std::move(spritesheet); - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } -} diff --git a/src/anm2/anm2_type.hpp b/src/anm2/anm2_type.hpp deleted file mode 100644 index 104caa1..0000000 --- a/src/anm2/anm2_type.hpp +++ /dev/null @@ -1,121 +0,0 @@ -#pragma once - -#include "icon.hpp" -#include "strings.hpp" - -#include -#include -#include -#include - -namespace anm2ed::anm2 -{ - inline const glm::vec4 ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f); - inline const glm::vec4 ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f); - inline const glm::vec4 ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f); - - inline const glm::vec4 LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f); - inline const glm::vec4 LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f); - inline const glm::vec4 LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f); - - inline const glm::vec4 NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f); - inline const glm::vec4 NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f); - inline const glm::vec4 NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f); - - inline const glm::vec4 TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f); - inline const glm::vec4 TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f); - inline const glm::vec4 TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f); - -#define TYPE_LIST \ - X(NONE, STRING_UNDEFINED, "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \ - X(ROOT, BASIC_ROOT, "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \ - X(LAYER, BASIC_LAYER_ANIMATION, "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, \ - LAYER_COLOR_HOVERED) \ - X(NULL_, BASIC_NULL_ANIMATION, "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, \ - NULL_COLOR_HOVERED) \ - X(TRIGGER, BASIC_TRIGGERS, "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE, \ - TRIGGER_COLOR_HOVERED) - - enum Type - { -#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) symbol, - TYPE_LIST -#undef X - }; - - constexpr StringType TYPE_STRINGS[] = { -#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string, - TYPE_LIST -#undef X - }; - - constexpr const char* TYPE_ITEM_STRINGS[] = { -#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString, - TYPE_LIST -#undef X - }; - - constexpr resource::icon::Type TYPE_ICONS[] = { -#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon, - TYPE_LIST -#undef X - }; - - inline const glm::vec4 TYPE_COLOR[] = { -#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color, - TYPE_LIST -#undef X - }; - - inline const glm::vec4 TYPE_COLOR_ACTIVE[] = { -#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive, - TYPE_LIST -#undef X - }; - - inline const glm::vec4 TYPE_COLOR_HOVERED[] = { -#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered, - TYPE_LIST -#undef X - }; - - enum ChangeType - { - ADJUST, - ADD, - SUBTRACT, - MULTIPLY, - DIVIDE - }; - - enum Compatibility - { - ISAAC, - ANM2ED, - ANM2ED_LIMITED, - COUNT - }; - - enum SpritesheetMergeOrigin - { - APPEND_RIGHT, - APPEND_BOTTOM - }; - - enum Flag - { - NO_SOUNDS = 1 << 0, - NO_REGIONS = 1 << 1, - FRAME_NO_REGION_VALUES = 1 << 2, - INTERPOLATION_BOOL_ONLY = 1 << 3 - }; - - typedef int Flags; - - inline bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; } - - inline const std::unordered_map COMPATIBILITY_FLAGS = { - {ISAAC, NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES | INTERPOLATION_BOOL_ONLY}, - {ANM2ED, 0}, - {ANM2ED_LIMITED, FRAME_NO_REGION_VALUES}}; -} diff --git a/src/anm2/content.cpp b/src/anm2/content.cpp deleted file mode 100644 index cd5f0ac..0000000 --- a/src/anm2/content.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "content.hpp" - -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.emplace(id, Spritesheet(child, id)); - - if (auto layersElement = element->FirstChildElement("Layers")) - for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer")) - layers.emplace(id, Layer(child, id)); - - if (auto nullsElement = element->FirstChildElement("Nulls")) - for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null")) - nulls.emplace(id, Null(child, id)); - - if (auto eventsElement = element->FirstChildElement("Events")) - for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event")) - events.emplace(id, Event(child, id)); - - if (auto soundsElement = element->FirstChildElement("Sounds")) - for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound")) - sounds.emplace(id, Sound(child, id)); - } - - void Content::serialize(XMLDocument& document, XMLElement* parent, Flags flags) - { - auto element = document.NewElement("Content"); - - auto spritesheetsElement = document.NewElement("Spritesheets"); - for (auto& [id, spritesheet] : spritesheets) - spritesheet.serialize(document, spritesheetsElement, id, flags); - 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); - - if (!has_flag(flags, NO_SOUNDS) && !sounds.empty()) - { - auto soundsElement = document.NewElement("Sounds"); - for (auto& [id, sound] : sounds) - sound.serialize(document, soundsElement, id); - element->InsertEndChild(soundsElement); - } - - parent->InsertEndChild(element); - } - -} diff --git a/src/anm2/content.hpp b/src/anm2/content.hpp deleted file mode 100644 index 17094bf..0000000 --- a/src/anm2/content.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -#include "event.hpp" -#include "layer.hpp" -#include "null.hpp" -#include "sound.hpp" -#include "spritesheet.hpp" - -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*, Flags = 0); - Content(tinyxml2::XMLElement*); - }; -} diff --git a/src/anm2/event.cpp b/src/anm2/event.cpp deleted file mode 100644 index cfb9f03..0000000 --- a/src/anm2/event.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "event.hpp" - -#include "xml_.hpp" - -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.hpp b/src/anm2/event.hpp deleted file mode 100644 index 5569c78..0000000 --- a/src/anm2/event.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -namespace anm2ed::anm2 -{ - class Event - { - public: - std::string name{}; - - 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 deleted file mode 100644 index e9f0cc8..0000000 --- a/src/anm2/frame.cpp +++ /dev/null @@ -1,215 +0,0 @@ -#include "frame.hpp" - -#include - -#include "math_.hpp" -#include "xml_.hpp" - -using namespace anm2ed::util; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - namespace - { - Frame::Interpolation interpolation_from_xml(const char* value, bool fallback) - { - if (value) - { - if (std::strcmp(value, "EaseIn") == 0) return Frame::Interpolation::EASE_IN; - if (std::strcmp(value, "EaseOut") == 0) return Frame::Interpolation::EASE_OUT; - if (std::strcmp(value, "EaseInOut") == 0) return Frame::Interpolation::EASE_IN_OUT; - } - return fallback ? Frame::Interpolation::LINEAR : Frame::Interpolation::NONE; - } - - const char* interpolation_to_xml(Frame::Interpolation interpolation) - { - switch (interpolation) - { - case Frame::Interpolation::EASE_IN: - return "EaseIn"; - case Frame::Interpolation::EASE_OUT: - return "EaseOut"; - case Frame::Interpolation::EASE_IN_OUT: - return "EaseInOut"; - default: - return nullptr; - } - } - } - - Frame::Frame(XMLElement* element, Type type) - { - bool isInterpolatedBool{}; - const char* interpolationValue = element->Attribute("Interpolated"); - - switch (type) - { - case ROOT: - case NULL_: - element->QueryFloatAttribute("XPosition", &position.x); - element->QueryFloatAttribute("YPosition", &position.y); - element->QueryFloatAttribute("XScale", &scale.x); - element->QueryFloatAttribute("YScale", &scale.y); - element->QueryIntAttribute("Delay", &duration); - 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", &isInterpolatedBool); - if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool); - break; - case LAYER: - element->QueryIntAttribute("RegionId", ®ionID); - element->QueryFloatAttribute("XPosition", &position.x); - element->QueryFloatAttribute("YPosition", &position.y); - 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", &duration); - 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", &isInterpolatedBool); - if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool); - break; - case TRIGGER: - { - element->QueryIntAttribute("EventId", &eventID); - - int soundID{}; - // Backwards compatibility with old formats - if (element->QueryIntAttribute("SoundId", &soundID) == XML_SUCCESS) soundIDs.push_back(soundID); - - for (auto child = element->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound")) - { - child->QueryIntAttribute("Id", &soundID); - soundIDs.push_back(soundID); - } - - element->QueryIntAttribute("AtFrame", &atFrame); - break; - } - default: - break; - } - } - - XMLElement* Frame::to_element(XMLDocument& document, Type type, Flags flags) - { - 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", duration); - 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); - if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE || - interpolation == Interpolation::LINEAR) - element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR); - else if (const char* interpolationValue = interpolation_to_xml(interpolation)) - element->SetAttribute("Interpolated", interpolationValue); - break; - case LAYER: - { - bool noRegions = has_flag(flags, NO_REGIONS); - bool frameNoRegionValues = has_flag(flags, FRAME_NO_REGION_VALUES); - bool hasValidRegion = !noRegions && regionID != -1; - bool writeRegionValues = !frameNoRegionValues || !hasValidRegion; - - if (hasValidRegion) element->SetAttribute("RegionId", regionID); - element->SetAttribute("XPosition", position.x); - element->SetAttribute("YPosition", position.y); - if (writeRegionValues) - { - 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", duration); - 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); - if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE || - interpolation == Interpolation::LINEAR) - element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR); - else if (const char* interpolationValue = interpolation_to_xml(interpolation)) - element->SetAttribute("Interpolated", interpolationValue); - break; - } - case TRIGGER: - if (eventID != -1) element->SetAttribute("EventId", eventID); - - if (!has_flag(flags, NO_SOUNDS)) - for (auto& id : soundIDs) - { - if (id == -1) continue; - auto soundChild = element->InsertNewChildElement("Sound"); - soundChild->SetAttribute("Id", id); - } - - element->SetAttribute("AtFrame", atFrame); - break; - default: - break; - } - - return element; - } - - void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type, Flags flags) - { - parent->InsertEndChild(to_element(document, type, flags)); - } - - std::string Frame::to_string(Type type, Flags flags) - { - XMLDocument document{}; - document.InsertEndChild(to_element(document, type, flags)); - return xml::document_to_string(document); - } - - void Frame::shorten() { duration = glm::clamp(--duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } - void Frame::extend() { duration = glm::clamp(++duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } -} diff --git a/src/anm2/frame.hpp b/src/anm2/frame.hpp deleted file mode 100644 index 7c41859..0000000 --- a/src/anm2/frame.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "anm2_type.hpp" -#include "types.hpp" - -namespace anm2ed::anm2 -{ - constexpr auto FRAME_DURATION_MIN = 1; - constexpr auto FRAME_DURATION_MAX = 1000000; - - class Frame - { - public: - enum Interpolation - { - NONE, - LINEAR, - EASE_IN, - EASE_OUT, - EASE_IN_OUT - }; - - bool isVisible{true}; - Interpolation interpolation{NONE}; - float rotation{}; - int duration{FRAME_DURATION_MIN}; - int atFrame{-1}; - int eventID{-1}; - int regionID{-1}; - std::vector soundIDs{}; - glm::vec2 pivot{}; - glm::vec2 crop{}; - glm::vec2 position{}; - glm::vec2 size{}; - glm::vec2 scale{100, 100}; - glm::vec3 colorOffset{}; - glm::vec4 tint{types::color::WHITE}; - - Frame() = default; - Frame(tinyxml2::XMLElement*, Type); - tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, Flags = 0); - std::string to_string(Type type, Flags = 0); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, Flags = 0); - void shorten(); - void extend(); - }; - - struct FrameChange - { - std::optional isVisible{}; - std::optional interpolation{}; - std::optional rotation{}; - std::optional duration{}; - std::optional regionID{}; - std::optional pivotX{}; - std::optional pivotY{}; - std::optional cropX{}; - std::optional cropY{}; - std::optional positionX{}; - std::optional positionY{}; - std::optional sizeX{}; - std::optional sizeY{}; - std::optional scaleX{}; - std::optional scaleY{}; - std::optional colorOffsetR{}; - std::optional colorOffsetG{}; - std::optional colorOffsetB{}; - std::optional tintR{}; - std::optional tintG{}; - std::optional tintB{}; - std::optional tintA{}; - std::optional isFlipX{}; - std::optional isFlipY{}; - }; - -} diff --git a/src/anm2/info.cpp b/src/anm2/info.cpp deleted file mode 100644 index d137288..0000000 --- a/src/anm2/info.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "info.hpp" - -#include "xml_.hpp" - -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.hpp b/src/anm2/info.hpp deleted file mode 100644 index c2fd400..0000000 --- a/src/anm2/info.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#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 deleted file mode 100644 index 85ea68d..0000000 --- a/src/anm2/item.cpp +++ /dev/null @@ -1,383 +0,0 @@ -#include "item.hpp" -#include -#include -#include - -#include "vector_.hpp" -#include "xml_.hpp" - -using namespace anm2ed::util; -using namespace tinyxml2; -using namespace glm; - -namespace anm2ed::anm2 -{ - namespace - { - float interpolation_factor(Frame::Interpolation interpolation, float value) - { - value = glm::clamp(value, 0.0f, 1.0f); - - switch (interpolation) - { - case Frame::Interpolation::LINEAR: - return value; - case Frame::Interpolation::EASE_IN: - return value * value; - case Frame::Interpolation::EASE_OUT: - return 1.0f - ((1.0f - value) * (1.0f - value)); - case Frame::Interpolation::EASE_IN_OUT: - return value < 0.5f ? (2.0f * value * value) : (1.0f - std::pow(-2.0f * value + 2.0f, 2.0f) * 0.5f); - case Frame::Interpolation::NONE: - default: - return 0.0f; - } - } - } - - 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, Flags flags) - { - auto element = document.NewElement(TYPE_ITEM_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); - - if (type == TRIGGER) frames_sort_by_at_frame(); - - for (auto& frame : frames) - frame.serialize(document, element, type, flags); - - return element; - } - - void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id, Flags flags) - { - parent->InsertEndChild(to_element(document, type, id, flags)); - } - - std::string Item::to_string(Type type, int id, Flags flags) - { - XMLDocument document{}; - document.InsertEndChild(to_element(document, type, id, flags)); - 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.duration; - - return length; - } - - void Item::frames_sort_by_at_frame() - { - std::sort(frames.begin(), frames.end(), [](const Frame& a, const Frame& b) { return a.atFrame < b.atFrame; }); - } - - Frame Item::frame_generate(float time, Type type) - { - Frame frame{}; - frame.isVisible = false; - - if (frames.empty()) return frame; - - time = time < 0.0f ? 0.0f : time; - - Frame* frameNext = nullptr; - int durationCurrent = 0; - int durationNext = 0; - - for (auto [i, iFrame] : std::views::enumerate(frames)) - { - if (type == TRIGGER) - { - if ((int)time == iFrame.atFrame) - { - frame = iFrame; - break; - } - } - else - { - frame = iFrame; - - durationNext += frame.duration; - - if (time >= durationCurrent && time < durationNext) - { - if (i + 1 < (int)frames.size()) - frameNext = &frames[i + 1]; - else - frameNext = nullptr; - break; - } - - durationCurrent += frame.duration; - } - } - - if (type != TRIGGER && frame.interpolation != Frame::Interpolation::NONE && frameNext && frame.duration > 1) - { - auto interpolation = - interpolation_factor(frame.interpolation, (time - durationCurrent) / (durationNext - durationCurrent)); - - 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(FrameChange change, anm2::Type itemType, ChangeType changeType, std::set& selection) - { - const auto clamp_identity = [](auto value) { return value; }; - const auto clamp01 = [](auto value) { return glm::clamp(value, 0.0f, 1.0f); }; - const auto clamp_duration = [](int value) { return std::max(FRAME_DURATION_MIN, value); }; - - if (selection.empty()) return; - - auto apply_scalar_with_clamp = [&](auto& target, const auto& optionalValue, auto clampFunc) - { - if (!optionalValue) return; - auto value = *optionalValue; - - switch (changeType) - { - case ADJUST: - target = clampFunc(value); - break; - case ADD: - target = clampFunc(target + value); - break; - case SUBTRACT: - target = clampFunc(target - value); - break; - case MULTIPLY: - target = clampFunc(target * value); - break; - case DIVIDE: - if (value == decltype(value){}) return; - target = clampFunc(target / value); - break; - } - }; - - auto apply_scalar = [&](auto& target, const auto& optionalValue) - { apply_scalar_with_clamp(target, optionalValue, clamp_identity); }; - - for (auto i : selection) - { - if (!vector::in_bounds(frames, i)) continue; - Frame& frame = frames[i]; - - if (change.isVisible) frame.isVisible = *change.isVisible; - if (change.interpolation) frame.interpolation = *change.interpolation; - if (change.isFlipX) frame.scale.x = -frame.scale.x; - if (change.isFlipY) frame.scale.y = -frame.scale.y; - - apply_scalar(frame.rotation, change.rotation); - apply_scalar_with_clamp(frame.duration, change.duration, clamp_duration); - - if (itemType == LAYER) - { - apply_scalar(frame.crop.x, change.cropX); - apply_scalar(frame.crop.y, change.cropY); - - apply_scalar(frame.pivot.x, change.pivotX); - apply_scalar(frame.pivot.y, change.pivotY); - - apply_scalar(frame.size.x, change.sizeX); - apply_scalar(frame.size.y, change.sizeY); - - if (change.regionID) frame.regionID = *change.regionID; - } - - apply_scalar(frame.position.x, change.positionX); - apply_scalar(frame.position.y, change.positionY); - - apply_scalar(frame.scale.x, change.scaleX); - apply_scalar(frame.scale.y, change.scaleY); - - apply_scalar_with_clamp(frame.colorOffset.x, change.colorOffsetR, clamp01); - apply_scalar_with_clamp(frame.colorOffset.y, change.colorOffsetG, clamp01); - apply_scalar_with_clamp(frame.colorOffset.z, change.colorOffsetB, clamp01); - - apply_scalar_with_clamp(frame.tint.x, change.tintR, clamp01); - apply_scalar_with_clamp(frame.tint.y, change.tintG, clamp01); - apply_scalar_with_clamp(frame.tint.z, change.tintB, clamp01); - apply_scalar_with_clamp(frame.tint.w, change.tintA, clamp01); - } - } - - 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) - { - int count{}; - if (document.FirstChildElement("Frame") && type != anm2::TRIGGER) - { - start = std::clamp(start, 0, (int)frames.size()); - 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 (document.FirstChildElement("Trigger") && type == anm2::TRIGGER) - { - auto has_conflict = [&](int value) - { - for (auto& trigger : frames) - if (trigger.atFrame == value) return true; - return false; - }; - - for (auto element = document.FirstChildElement("Trigger"); element; - element = element->NextSiblingElement("Trigger")) - { - Frame trigger(element, type); - trigger.atFrame = start + count; - while (has_conflict(trigger.atFrame)) - trigger.atFrame++; - frames.push_back(trigger); - indices.insert(trigger.atFrame); - count++; - } - - frames_sort_by_at_frame(); - return true; - } - else - { - if (errorString) *errorString = type == anm2::TRIGGER ? "No valid trigger(s)." : "No valid frame(s)."; - return false; - } - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - void Item::frames_bake(int index, int interval, bool isRoundScale, bool isRoundRotation) - { - if (!vector::in_bounds(frames, index)) return; - - auto original = frames[index]; - if (original.duration == FRAME_DURATION_MIN) - { - frames[index].interpolation = Frame::Interpolation::NONE; - return; - } - - auto nextFrame = vector::in_bounds(frames, index + 1) ? frames[index + 1] : original; - - int duration{}; - int i = index; - - while (duration < original.duration) - { - Frame baked = original; - float interpolation = interpolation_factor(original.interpolation, (float)duration / original.duration); - baked.duration = std::min(interval, original.duration - duration); - baked.interpolation = Frame::Interpolation::NONE; - baked.rotation = glm::mix(original.rotation, nextFrame.rotation, interpolation); - baked.position = glm::mix(original.position, nextFrame.position, interpolation); - baked.scale = glm::mix(original.scale, nextFrame.scale, interpolation); - baked.colorOffset = glm::mix(original.colorOffset, nextFrame.colorOffset, interpolation); - baked.tint = glm::mix(original.tint, nextFrame.tint, interpolation); - if (isRoundScale) baked.scale = vec2(ivec2(baked.scale)); - if (isRoundRotation) baked.rotation = (int)baked.rotation; - - if (i == index) - frames[i] = baked; - else - frames.insert(frames.begin() + i, baked); - i++; - - duration += baked.duration; - } - } - - void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, - int duration) - { - for (int i = 0; i < count; i++) - { - Frame frame{}; - frame.duration = duration; - frame.pivot = pivot; - frame.size = size; - frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns)); - - frames.emplace_back(frame); - } - } - - int Item::frame_index_from_at_frame_get(int atFrame) - { - for (auto [i, frame] : std::views::enumerate(frames)) - if (frame.atFrame == atFrame) return i; - return -1; - } - - float Item::frame_time_from_index_get(int index) - { - if (!vector::in_bounds(frames, index)) return 0.0f; - - float time{}; - for (auto [i, frame] : std::views::enumerate(frames)) - { - if (i == index) return time; - time += frame.duration; - } - - return time; - } - - int Item::frame_index_from_time_get(float time) - { - if (frames.empty()) return -1; - if (time <= 0.0f) return 0; - - float duration{}; - for (auto [i, frame] : std::views::enumerate(frames)) - { - duration += frame.duration; - if (time < duration) return (int)i; - } - - return (int)frames.size() - 1; - } -} diff --git a/src/anm2/item.hpp b/src/anm2/item.hpp deleted file mode 100644 index 0ca0a12..0000000 --- a/src/anm2/item.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include - -#include "frame.hpp" - -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, Flags = 0); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1, Flags = 0); - std::string to_string(Type, int = -1, Flags = 0); - int length(Type); - Frame frame_generate(float, Type); - void frames_change(FrameChange, anm2::Type, ChangeType, std::set&); - bool frames_deserialize(const std::string&, Type, int, std::set&, std::string*); - void frames_bake(int, int, bool, bool); - void frames_generate_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); - void frames_sort_by_at_frame(); - int frame_index_from_at_frame_get(int); - int frame_index_from_time_get(float); - float frame_time_from_index_get(int); - }; -} diff --git a/src/anm2/layer.cpp b/src/anm2/layer.cpp deleted file mode 100644 index 3c29702..0000000 --- a/src/anm2/layer.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "layer.hpp" - -#include "xml_.hpp" - -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); - } - - bool Layer::is_spritesheet_valid() - { - return spritesheetID > -1; - } -} \ No newline at end of file diff --git a/src/anm2/layer.hpp b/src/anm2/layer.hpp deleted file mode 100644 index 18bf878..0000000 --- a/src/anm2/layer.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -namespace anm2ed::anm2 -{ - class Layer - { - public: - std::string name{}; - 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); - bool is_spritesheet_valid(); - }; -} \ No newline at end of file diff --git a/src/anm2/null.cpp b/src/anm2/null.cpp deleted file mode 100644 index 6873424..0000000 --- a/src/anm2/null.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "null.hpp" - -#include "xml_.hpp" - -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.hpp b/src/anm2/null.hpp deleted file mode 100644 index 7b6dbc5..0000000 --- a/src/anm2/null.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include - -namespace anm2ed::anm2 -{ - class Null - { - public: - std::string name{}; - 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 deleted file mode 100644 index b90ef4f..0000000 --- a/src/anm2/sound.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "sound.hpp" - -#include "path_.hpp" -#include "working_directory.hpp" -#include "xml_.hpp" - -using namespace anm2ed::resource; -using namespace anm2ed::util; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - Sound::Sound(const Sound& other) : path(other.path), audio(other.audio) {} - - Sound& Sound::operator=(const Sound& other) - { - if (this != &other) - { - path = other.path; - audio = other.audio; - } - return *this; - } - - Sound::Sound(const std::filesystem::path& directory, const std::filesystem::path& path) - { - WorkingDirectory workingDirectory(directory); - this->path = !path.empty() ? path::make_relative(path) : this->path; - this->path = path::lower_case_backslash_handle(this->path); - audio = Audio(this->path); - } - - Sound::Sound(XMLElement* element, int& id) - { - if (!element) return; - element->QueryIntAttribute("Id", &id); - xml::query_path_attribute(element, "Path", &path); - path = path::lower_case_backslash_handle(path); - audio = Audio(path); - } - - XMLElement* Sound::to_element(XMLDocument& document, int id) - { - auto element = document.NewElement("Sound"); - element->SetAttribute("Id", id); - auto pathString = path::to_utf8(path); - element->SetAttribute("Path", pathString.c_str()); - return element; - } - - void Sound::serialize(XMLDocument& document, XMLElement* parent, int id) - { - parent->InsertEndChild(to_element(document, id)); - } - - std::string Sound::to_string(int id) - { - XMLDocument document{}; - document.InsertEndChild(to_element(document, id)); - return xml::document_to_string(document); - } - - void Sound::reload(const std::filesystem::path& directory) { *this = Sound(directory, this->path); } - bool Sound::is_valid() { return audio.is_valid(); } - void Sound::play() { audio.play(); } -} diff --git a/src/anm2/sound.hpp b/src/anm2/sound.hpp deleted file mode 100644 index 3271b58..0000000 --- a/src/anm2/sound.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -#include "audio.hpp" - -namespace anm2ed::anm2 -{ - 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::filesystem::path&, const std::filesystem::path&); - tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); - std::string to_string(int); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); - void reload(const std::filesystem::path&); - bool is_valid(); - void play(); - }; -} diff --git a/src/anm2/spritesheet.cpp b/src/anm2/spritesheet.cpp deleted file mode 100644 index bd8f68e..0000000 --- a/src/anm2/spritesheet.cpp +++ /dev/null @@ -1,272 +0,0 @@ -#include "spritesheet.hpp" - -#include -#include -#include -#include -#include - -#include "map_.hpp" -#include "path_.hpp" -#include "working_directory.hpp" -#include "xml_.hpp" - -using namespace anm2ed::resource; -using namespace anm2ed::util; -using namespace anm2ed::types; -using namespace tinyxml2; - -namespace anm2ed::anm2 -{ - namespace - { - const char* origin_to_string(Spritesheet::Region::Origin origin) - { - switch (origin) - { - case Spritesheet::Region::TOP_LEFT: - return "TopLeft"; - case Spritesheet::Region::ORIGIN_CENTER: - return "Center"; - case Spritesheet::Region::CUSTOM: - default: - return nullptr; - } - } - - Spritesheet::Region::Origin origin_from_string(const char* originString) - { - if (!originString) return Spritesheet::Region::CUSTOM; - if (std::string(originString) == "TopLeft") return Spritesheet::Region::TOP_LEFT; - if (std::string(originString) == "Center") return Spritesheet::Region::ORIGIN_CENTER; - return Spritesheet::Region::CUSTOM; - } - } - - 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 - path = path::lower_case_backslash_handle(path); - texture = Texture(path); - - regionOrder.clear(); - for (auto child = element->FirstChildElement("Region"); child; child = child->NextSiblingElement("Region")) - { - Region region{}; - int id{}; - child->QueryIntAttribute("Id", &id); - xml::query_string_attribute(child, "Name", ®ion.name); - child->QueryFloatAttribute("XCrop", ®ion.crop.x); - child->QueryFloatAttribute("YCrop", ®ion.crop.y); - child->QueryFloatAttribute("Width", ®ion.size.x); - child->QueryFloatAttribute("Height", ®ion.size.y); - region.origin = origin_from_string(child->Attribute("Origin")); - if (region.origin == Spritesheet::Region::TOP_LEFT) - region.pivot = {}; - else if (region.origin == Spritesheet::Region::ORIGIN_CENTER) - region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)}; - else - { - child->QueryFloatAttribute("XPivot", ®ion.pivot.x); - child->QueryFloatAttribute("YPivot", ®ion.pivot.y); - } - regions.emplace(id, std::move(region)); - regionOrder.push_back(id); - } - - if (regionOrder.size() != regions.size()) - { - regionOrder.clear(); - regionOrder.reserve(regions.size()); - for (auto id : regions | std::views::keys) - regionOrder.push_back(id); - } - } - - Spritesheet::Spritesheet(const std::filesystem::path& directory, const std::filesystem::path& path) - { - WorkingDirectory workingDirectory(directory); - auto loadPath = !path.empty() ? path::lower_case_backslash_handle(path) : this->path; - this->path = !path.empty() ? path::make_relative(path) : this->path; - this->path = path::lower_case_backslash_handle(this->path); - texture = Texture(!loadPath.empty() ? loadPath : this->path); - } - - XMLElement* Spritesheet::to_element(XMLDocument& document, int id, Flags flags) - { - auto element = document.NewElement("Spritesheet"); - element->SetAttribute("Id", id); - auto pathString = path::to_utf8(path); - element->SetAttribute("Path", pathString.c_str()); - - if (!has_flag(flags, NO_REGIONS)) - { - if (regionOrder.size() != regions.size()) - { - regionOrder.clear(); - regionOrder.reserve(regions.size()); - for (auto id : regions | std::views::keys) - regionOrder.push_back(id); - } - - for (auto id : regionOrder) - { - if (!regions.contains(id)) continue; - auto& region = regions.at(id); - auto regionElement = element->InsertNewChildElement("Region"); - regionElement->SetAttribute("Id", id); - regionElement->SetAttribute("Name", region.name.c_str()); - regionElement->SetAttribute("XCrop", region.crop.x); - regionElement->SetAttribute("YCrop", region.crop.y); - regionElement->SetAttribute("Width", region.size.x); - regionElement->SetAttribute("Height", region.size.y); - if (auto originString = origin_to_string(region.origin); originString) - regionElement->SetAttribute("Origin", originString); - else - { - regionElement->SetAttribute("XPivot", region.pivot.x); - regionElement->SetAttribute("YPivot", region.pivot.y); - } - } - } - - return element; - } - - void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id, Flags flags) - { - parent->InsertEndChild(to_element(document, id, flags)); - } - - std::string Spritesheet::to_string(int id) - { - XMLDocument document{}; - document.InsertEndChild(to_element(document, id)); - return xml::document_to_string(document); - } - - std::string Spritesheet::region_to_string(int id) - { - if (!regions.contains(id)) return {}; - - XMLDocument document{}; - auto element = document.NewElement("Region"); - auto& region = regions.at(id); - element->SetAttribute("Id", id); - element->SetAttribute("Name", region.name.c_str()); - element->SetAttribute("XCrop", region.crop.x); - element->SetAttribute("YCrop", region.crop.y); - element->SetAttribute("Width", region.size.x); - element->SetAttribute("Height", region.size.y); - if (auto originString = origin_to_string(region.origin); originString) - element->SetAttribute("Origin", originString); - else - { - element->SetAttribute("XPivot", region.pivot.x); - element->SetAttribute("YPivot", region.pivot.y); - } - document.InsertEndChild(element); - - return xml::document_to_string(document); - } - - bool Spritesheet::regions_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("Region")) - { - if (errorString) *errorString = "No valid region(s)."; - return false; - } - - for (auto element = document.FirstChildElement("Region"); element; - element = element->NextSiblingElement("Region")) - { - Region region{}; - element->QueryIntAttribute("Id", &id); - xml::query_string_attribute(element, "Name", ®ion.name); - element->QueryFloatAttribute("XCrop", ®ion.crop.x); - element->QueryFloatAttribute("YCrop", ®ion.crop.y); - element->QueryFloatAttribute("Width", ®ion.size.x); - element->QueryFloatAttribute("Height", ®ion.size.y); - region.origin = origin_from_string(element->Attribute("Origin")); - if (region.origin == Spritesheet::Region::TOP_LEFT) - region.pivot = {}; - else if (region.origin == Spritesheet::Region::ORIGIN_CENTER) - region.pivot = glm::ivec2(region.size / 2.0f); - else - { - element->QueryFloatAttribute("XPivot", ®ion.pivot.x); - element->QueryFloatAttribute("YPivot", ®ion.pivot.y); - } - - if (type == merge::APPEND) id = map::next_id_get(regions); - regions[id] = std::move(region); - if (std::find(regionOrder.begin(), regionOrder.end(), id) == regionOrder.end()) regionOrder.push_back(id); - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Spritesheet::save(const std::filesystem::path& directory, const std::filesystem::path& path) - { - WorkingDirectory workingDirectory(directory); - this->path = !path.empty() ? path::make_relative(path) : this->path; - if (this->path.empty()) return false; - path::ensure_directory(this->path.parent_path()); - return texture.write_png(this->path); - } - - void Spritesheet::reload(const std::filesystem::path& directory, const std::filesystem::path& path) - { - WorkingDirectory workingDirectory(directory); - auto loadPath = !path.empty() ? path::lower_case_backslash_handle(path) : this->path; - this->path = !path.empty() ? path::make_relative(path) : this->path; - this->path = path::lower_case_backslash_handle(this->path); - texture = Texture(!loadPath.empty() ? loadPath : this->path); - } - bool Spritesheet::is_valid() { return texture.is_valid(); } - - uint64_t Spritesheet::hash() const - { - auto hash_combine = [](std::size_t& seed, std::size_t value) - { - seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); - }; - - std::size_t seed{}; - hash_combine(seed, std::hash{}(texture.size.x)); - hash_combine(seed, std::hash{}(texture.size.y)); - hash_combine(seed, std::hash{}(texture.channels)); - hash_combine(seed, std::hash{}(texture.filter)); - hash_combine(seed, std::hash{}(path::to_utf8(path))); - - if (!texture.pixels.empty()) - { - std::string_view bytes(reinterpret_cast(texture.pixels.data()), texture.pixels.size()); - hash_combine(seed, std::hash{}(bytes)); - } - else - { - hash_combine(seed, 0); - } - - return static_cast(seed); - } - -} diff --git a/src/anm2/spritesheet.hpp b/src/anm2/spritesheet.hpp deleted file mode 100644 index 4a906f1..0000000 --- a/src/anm2/spritesheet.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "texture.hpp" -#include "anm2_type.hpp" -#include "types.hpp" -#include "origin.hpp" - -namespace anm2ed::anm2 -{ - class Spritesheet - { - public: - struct Region - { - using Origin = origin::Type; - static constexpr Origin TOP_LEFT = origin::TOP_LEFT; - static constexpr Origin ORIGIN_CENTER = origin::ORIGIN_CENTER; - static constexpr Origin CUSTOM = origin::CUSTOM; - - std::string name{}; - glm::vec2 crop{}; - glm::vec2 pivot{}; - glm::vec2 size{}; - Origin origin{CUSTOM}; - }; - - std::filesystem::path path{}; - resource::Texture texture; - - std::map regions{}; - std::vector regionOrder{}; - - Spritesheet() = default; - Spritesheet(tinyxml2::XMLElement*, int&); - Spritesheet(const std::filesystem::path&, const std::filesystem::path& = {}); - tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int, Flags = 0); - std::string to_string(int id); - std::string region_to_string(int id); - bool regions_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); - bool save(const std::filesystem::path&, const std::filesystem::path& = {}); - void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int, Flags = 0); - void reload(const std::filesystem::path&, const std::filesystem::path& = {}); - bool is_valid(); - uint64_t hash() const; - }; -} diff --git a/src/canvas.cpp b/src/canvas.cpp index 5d18447..1956c21 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -6,7 +6,7 @@ #include #include -#include "math_.hpp" +#include "math.hpp" using namespace glm; using namespace anm2ed::resource; diff --git a/src/dialog.cpp b/src/dialog.cpp index 520efca..9f8f4b8 100644 --- a/src/dialog.cpp +++ b/src/dialog.cpp @@ -13,7 +13,7 @@ #include #include -#include "path_.hpp" +#include "path.hpp" using namespace anm2ed::util; @@ -25,13 +25,17 @@ namespace anm2ed if (filelist && filelist[0] && strlen(filelist[0]) > 0) { - self->path = path::from_utf8(filelist[0]); + self->paths.clear(); + for (int i = 0; filelist[i] && strlen(filelist[i]) > 0; ++i) + self->paths.push_back(path::from_utf8(filelist[i])); + self->path = self->paths.empty() ? std::filesystem::path{} : self->paths.front(); self->selectedFilter = filter; } else { self->selectedFilter = -1; self->path.clear(); + self->paths.clear(); } } @@ -41,17 +45,21 @@ namespace anm2ed this->window = window; } - void Dialog::file_open(Type type) + void Dialog::file_open(Type type, bool isMany) { if (type == Dialog::NONE) return; + path.clear(); + paths.clear(); SDL_ShowOpenFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]), - nullptr, false); + nullptr, isMany); this->type = type; } void Dialog::file_save(Type type) { if (type == Dialog::NONE) return; + path.clear(); + paths.clear(); SDL_ShowSaveFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]), nullptr); this->type = type; @@ -60,6 +68,8 @@ namespace anm2ed void Dialog::folder_open(Type type) { if (type == Dialog::NONE) return; + path.clear(); + paths.clear(); SDL_ShowOpenFolderDialog(callback, this, window, nullptr, false); this->type = type; } @@ -80,6 +90,6 @@ namespace anm2ed void Dialog::reset() { *this = Dialog(this->window); } - bool Dialog::is_selected(Type type) const { return this->type == type && !path.empty(); } + bool Dialog::is_selected(Type type) const { return this->type == type && !paths.empty(); } }; diff --git a/src/dialog.hpp b/src/dialog.hpp index 58725c1..7993749 100644 --- a/src/dialog.hpp +++ b/src/dialog.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -42,7 +43,7 @@ namespace anm2ed #define DIALOG_LIST \ X(NONE, NO_FILTER) \ - X(ANM2_NEW, ANM2) \ + X(ANM2_CREATE, ANM2) \ X(ANM2_OPEN, ANM2) \ X(ANM2_SAVE, ANM2) \ X(SOUND_OPEN, SOUND) \ @@ -74,12 +75,13 @@ namespace anm2ed SDL_Window* window{}; std::filesystem::path path{}; + std::vector paths{}; Type type{NONE}; int selectedFilter{-1}; Dialog() = default; Dialog(SDL_Window*); - void file_open(Type type); + void file_open(Type type, bool isMany = false); void file_save(Type type); void folder_open(Type type); bool is_selected(Type type) const; diff --git a/src/document.cpp b/src/document.cpp index 6b9c307..bf5d4a9 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -1,106 +1,259 @@ #include "document.hpp" #include +#include +#include +#include #include +#include +#include +#include +#include #include +#include #include #include "log.hpp" -#include "path_.hpp" +#include "manager.hpp" +#include "path.hpp" #include "strings.hpp" #include "toast.hpp" +#include "working_directory.hpp" -using namespace anm2ed::anm2; using namespace anm2ed::imgui; using namespace anm2ed::types; using namespace anm2ed::util; using namespace glm; +namespace anm2ed::document +{ + Options save_options_get(Compatibility compatibility, bool isBakeSpecialInterpolatedFrames, bool isRoundScale, + bool isRoundRotation) + { + return {.compatibility = compatibility, + .isBakeSpecialInterpolatedFrames = isBakeSpecialInterpolatedFrames, + .isRoundScale = isRoundScale, + .isRoundRotation = isRoundRotation}; + } + + ItemType item_type_get(int type) { return static_cast(type); } + + int animation_count_get(const Anm2& data) + { + int count{}; + if (auto animations = data.element_get(ElementType::ANIMATIONS)) + for (auto& animation : animations->children) + if (animation.type == ElementType::ANIMATION) ++count; + return count; + } + + int frame_count_get(const Element& item) + { + auto frameType = item.type == ElementType::TRIGGERS ? ElementType::TRIGGER : ElementType::FRAME; + int count{}; + for (auto& frame : item.children) + if (frame.type == frameType) ++count; + return count; + } + + std::vector animation_labels_get(const Anm2& data) + { + std::vector labels{"None"}; + if (auto animations = data.element_get(ElementType::ANIMATIONS)) + for (auto& animation : animations->children) + if (animation.type == ElementType::ANIMATION) labels.emplace_back(animation.name); + return labels; + } + + std::vector element_name_labels_get(const Element* container, ElementType type, bool isNone) + { + std::vector labels{}; + if (isNone) labels.emplace_back(localize.get(BASIC_NONE)); + if (!container) return labels; + for (auto& element : container->children) + if (element.type == type) labels.emplace_back(element.name); + return labels; + } + + std::vector element_ids_get(const Element* container, ElementType type, bool isNone) + { + std::vector ids{}; + if (isNone) ids.emplace_back(-1); + if (!container) return ids; + for (auto& element : container->children) + if (element.type == type) ids.emplace_back(element.id); + return ids; + } + + std::vector spritesheet_labels_get(const Anm2& data) + { + std::vector labels{}; + if (auto spritesheets = data.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + if (spritesheet.type == ElementType::SPRITESHEET) + { + auto pathString = path::to_utf8(spritesheet.path); + labels.emplace_back( + std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(spritesheet.id, pathString))); + } + return labels; + } + + std::vector spritesheet_ids_get(const Anm2& data) + { + std::vector ids{}; + if (auto spritesheets = data.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + if (spritesheet.type == ElementType::SPRITESHEET) ids.emplace_back(spritesheet.id); + return ids; + } + + std::vector sound_labels_get(const Anm2& data) + { + std::vector labels{localize.get(BASIC_NONE)}; + if (auto sounds = data.element_get(ElementType::SOUNDS)) + for (auto& sound : sounds->children) + if (sound.type == ElementType::SOUND_ELEMENT) + { + auto pathString = path::to_utf8(sound.path); + labels.emplace_back(std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(sound.id, pathString))); + } + return labels; + } + + std::vector sound_ids_get(const Anm2& data) + { + std::vector ids{-1}; + if (auto sounds = data.element_get(ElementType::SOUNDS)) + for (auto& sound : sounds->children) + if (sound.type == ElementType::SOUND_ELEMENT) ids.emplace_back(sound.id); + return ids; + } + + uint64_t spritesheet_hash_get(const Element& spritesheet, const resource::Texture* texture) + { + auto hash_combine = [](std::size_t& seed, std::size_t value) + { seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); }; + + std::size_t seed{}; + hash_combine(seed, std::hash{}(texture ? texture->size.x : 0)); + hash_combine(seed, std::hash{}(texture ? texture->size.y : 0)); + hash_combine(seed, std::hash{}(texture ? texture->channels : 0)); + hash_combine(seed, std::hash{}(texture ? texture->filter : 0)); + hash_combine(seed, std::hash{}(path::to_utf8(spritesheet.path))); + + if (texture && !texture->pixels.empty()) + { + std::string_view bytes(reinterpret_cast(texture->pixels.data()), texture->pixels.size()); + hash_combine(seed, std::hash{}(bytes)); + } + else + hash_combine(seed, 0); + + return static_cast(seed); + } + + void restored_snapshot_sanitize(Document& document) + { + auto& reference = document.reference; + auto& selection = document.frames.selection; + auto& frameReferences = document.frames.references; + auto& itemReferences = document.items.references; + + auto animationCount = animation_count_get(document.anm2); + if (animationCount <= 0) + { + reference = {}; + selection.clear(); + frameReferences.clear(); + itemReferences.clear(); + document.frameTime = 0.0f; + return; + } + + if (reference.animationIndex < 0 || reference.animationIndex >= animationCount) + reference.animationIndex = std::clamp(reference.animationIndex, 0, animationCount - 1); + + auto item = + document.anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID); + if (!item) + { + reference.itemType = (int)ItemType::ROOT; + reference.itemID = -1; + item = document.anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID); + } + + if (!item) + { + reference.frameIndex = -1; + selection.clear(); + frameReferences.clear(); + document.frameTime = 0.0f; + return; + } + + for (auto it = itemReferences.begin(); it != itemReferences.end();) + { + auto item = document.anm2.element_get(it->animationIndex, item_type_get(it->itemType), it->itemID); + if (!item) + it = itemReferences.erase(it); + else + ++it; + } + + for (auto it = frameReferences.begin(); it != frameReferences.end();) + { + auto item = document.anm2.element_get(it->animationIndex, item_type_get(it->itemType), it->itemID); + auto frameCount = item ? frame_count_get(*item) : 0; + if (!item || it->frameIndex < 0 || it->frameIndex >= frameCount) + it = frameReferences.erase(it); + else + ++it; + } + + auto frameCount = frame_count_get(*item); + for (auto it = selection.begin(); it != selection.end();) + { + if (*it < 0 || *it >= frameCount) + it = selection.erase(it); + else + ++it; + } + + if (frameCount <= 0) + { + reference.frameIndex = -1; + frameReferences.clear(); + document.frameTime = 0.0f; + return; + } + + if (reference.frameIndex < 0 || reference.frameIndex >= frameCount) + reference.frameIndex = + selection.empty() ? std::clamp(reference.frameIndex, 0, frameCount - 1) : *selection.begin(); + + if (frameReferences.empty()) + for (auto frameIndex : selection) + frameReferences.insert({reference.animationIndex, reference.itemType, reference.itemID, frameIndex}); + + selection.clear(); + for (const auto& frameReference : frameReferences) + if (frameReference.animationIndex == reference.animationIndex && frameReference.itemType == reference.itemType && + frameReference.itemID == reference.itemID) + selection.insert(frameReference.frameIndex); + + document.frameTime = frame_time_from_index_get(*item, reference.frameIndex); + } +} + namespace anm2ed { - namespace - { - anm2::Flags serialize_flags_get(anm2::Compatibility compatibility) - { - if (auto it = anm2::COMPATIBILITY_FLAGS.find(compatibility); it != anm2::COMPATIBILITY_FLAGS.end()) - return it->second; - return 0; - } - - void restored_snapshot_sanitize(Document& document) - { - auto& reference = document.reference; - auto& documentAnm2 = document.anm2; - auto& selection = document.frames.selection; - - auto animationCount = (int)documentAnm2.animations.items.size(); - if (animationCount <= 0) - { - reference = {}; - selection.clear(); - document.frameTime = 0.0f; - return; - } - - if (reference.animationIndex < 0 || reference.animationIndex >= animationCount) - reference.animationIndex = std::clamp(reference.animationIndex, 0, animationCount - 1); - - auto item = documentAnm2.item_get(reference.animationIndex, reference.itemType, reference.itemID); - if (!item) - { - reference.itemType = ROOT; - reference.itemID = -1; - item = documentAnm2.item_get(reference.animationIndex, reference.itemType, reference.itemID); - } - - if (!item) - { - reference.frameIndex = -1; - selection.clear(); - document.frameTime = 0.0f; - return; - } - - auto frameCount = (int)item->frames.size(); - for (auto it = selection.begin(); it != selection.end();) - { - if (*it < 0 || *it >= frameCount) - it = selection.erase(it); - else - ++it; - } - - if (frameCount <= 0) - { - reference.frameIndex = -1; - document.frameTime = 0.0f; - return; - } - - if (reference.frameIndex < 0 || reference.frameIndex >= frameCount) - reference.frameIndex = selection.empty() ? std::clamp(reference.frameIndex, 0, frameCount - 1) - : *selection.begin(); - - document.frameTime = item->frame_time_from_index_get(reference.frameIndex); - } - } - - Document::Document(Anm2& anm2, const std::filesystem::path& path) - { - this->anm2 = std::move(anm2); - this->path = path; - isValid = this->anm2.isValid; - if (!isValid) return; - clean(); - change(Document::ALL); - } - Document::Document(const std::filesystem::path& path, bool isNew, std::string* errorString) { if (isNew) { - anm2 = anm2::Anm2(); + anm2 = Anm2(); if (!save(path, errorString)) { isValid = false; @@ -129,37 +282,37 @@ namespace anm2ed : path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current), playback(current.playback), animation(current.animation), event(current.event), frames(current.frames), items(current.items), layer(current.layer), merge(current.merge), null(current.null), region(current.region), - sound(current.sound), spritesheet(current.spritesheet), anm2(current.anm2), reference(current.reference), - frameTime(current.frameTime), message(current.message), regionBySpritesheet(std::move(other.regionBySpritesheet)), - changeAllFramePropertiesRegionId(other.changeAllFramePropertiesRegionId), - previewZoom(other.previewZoom), previewPan(other.previewPan), editorPan(other.editorPan), - editorZoom(other.editorZoom), overlayIndex(other.overlayIndex), hash(other.hash), saveHash(other.saveHash), - autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isValid(other.isValid), - isOpen(other.isOpen), isForceDirty(other.isForceDirty), - spritesheetHashes(std::move(other.spritesheetHashes)), - spritesheetSaveHashes(std::move(other.spritesheetSaveHashes)), - isAnimationPreviewSet(other.isAnimationPreviewSet), isSpritesheetEditorSet(other.isSpritesheetEditorSet) + sound(current.sound), spritesheet(current.spritesheet), textures(current.textures), sounds(current.sounds), + anm2(current.anm2), reference(current.reference), frameTime(current.frameTime), message(current.message), + regionBySpritesheet(std::move(other.regionBySpritesheet)), + changeAllFramePropertiesRegionId(other.changeAllFramePropertiesRegionId), previewZoom(other.previewZoom), + previewPan(other.previewPan), editorPan(other.editorPan), editorZoom(other.editorZoom), + overlayIndex(other.overlayIndex), hash(other.hash), saveHash(other.saveHash), autosaveHash(other.autosaveHash), + lastAutosaveTime(other.lastAutosaveTime), isValid(other.isValid), isOpen(other.isOpen), + isForceDirty(other.isForceDirty), spritesheetHashes(std::move(other.spritesheetHashes)), + spritesheetSaveHashes(std::move(other.spritesheetSaveHashes)), texturePaths(std::move(other.texturePaths)), + soundPaths(std::move(other.soundPaths)), isAnimationPreviewSet(other.isAnimationPreviewSet), + isSpritesheetEditorSet(other.isSpritesheetEditorSet) { } Document& Document::operator=(Document&& other) noexcept { - if (this != &other) - this->~Document(); + if (this != &other) this->~Document(); new (this) Document(std::move(other)); return *this; } - bool Document::save(const std::filesystem::path& path, std::string* errorString, anm2::Compatibility compatibility, + bool Document::save(const std::filesystem::path& path, std::string* errorString, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) { - this->path = !path.empty() ? path : this->path; - - auto absolutePath = this->path; + auto absolutePath = !path.empty() ? path : this->path; auto absolutePathUtf8 = path::to_utf8(absolutePath); - if (anm2.serialize(absolutePath, errorString, serialize_flags_get(compatibility), - isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation)) + if (anm2.save(absolutePath, errorString, + document::save_options_get(compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, + isRoundRotation))) { + this->path = absolutePath; toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePathUtf8))); logger.info( std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), std::make_format_args(absolutePathUtf8))); @@ -189,21 +342,21 @@ namespace anm2ed auto fileName = path::to_utf8(path.filename()); if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin()); constexpr std::string_view autosaveExtension = ".autosave"; - if (fileName.ends_with(autosaveExtension)) - fileName.erase(fileName.size() - autosaveExtension.size()); + if (fileName.ends_with(autosaveExtension)) fileName.erase(fileName.size() - autosaveExtension.size()); auto restorePath = path.parent_path() / std::filesystem::path(std::u8string(fileName.begin(), fileName.end())); return restorePath; } - bool Document::autosave(std::string* errorString, anm2::Compatibility compatibility, + bool Document::autosave(std::string* errorString, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) { auto autosavePath = autosave_path_get(); auto autosavePathUtf8 = path::to_utf8(autosavePath); - if (anm2.serialize(autosavePath, errorString, serialize_flags_get(compatibility), - isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation)) + if (anm2.save(autosavePath, errorString, + document::save_options_get(compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, + isRoundRotation))) { autosaveHash = hash; lastAutosaveTime = 0.0f; @@ -223,10 +376,931 @@ namespace anm2ed return false; } + void Document::anm2_change(ChangeType type) { change(type); } + + void Document::texture_change(int id) + { + auto texture = texture_get(id); + if (!texture || !anm2.element_get(ElementType::SPRITESHEET, id)) return; + change(SPRITESHEETS); + } + + void Document::assets_sync(ChangeType type) + { + if (type == ALL || type == SPRITESHEETS || type == TEXTURES) + { + std::set validIds{}; + util::WorkingDirectory workingDirectory(directory_get()); + if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + { + if (spritesheet.type != ElementType::SPRITESHEET) continue; + validIds.insert(spritesheet.id); + auto isReload = !textures.contains(spritesheet.id) || !texturePaths.contains(spritesheet.id) || + texturePaths.at(spritesheet.id) != spritesheet.path; + if (isReload) + { + textures[spritesheet.id] = resource::Texture(spritesheet.path); + texturePaths[spritesheet.id] = spritesheet.path; + } + } + + for (auto it = textures.begin(); it != textures.end();) + { + if (!validIds.contains(it->first)) + { + texturePaths.erase(it->first); + it = textures.erase(it); + } + else + ++it; + } + } + + if (type == ALL || type == SOUNDS) + { + std::set validIds{}; + util::WorkingDirectory workingDirectory(directory_get()); + if (auto soundItems = anm2.element_get(ElementType::SOUNDS)) + for (auto& sound : soundItems->children) + { + if (sound.type != ElementType::SOUND_ELEMENT) continue; + validIds.insert(sound.id); + auto isReload = + !sounds.contains(sound.id) || !soundPaths.contains(sound.id) || soundPaths.at(sound.id) != sound.path; + if (isReload) + { + sounds[sound.id] = resource::Audio(sound.path); + soundPaths[sound.id] = sound.path; + } + } + + for (auto it = sounds.begin(); it != sounds.end();) + { + if (!validIds.contains(it->first)) + { + soundPaths.erase(it->first); + it = sounds.erase(it); + } + else + ++it; + } + } + } + + resource::Texture* Document::texture_get(int id) + { + auto it = textures.find(id); + return it == textures.end() ? nullptr : &it->second; + } + + const resource::Texture* Document::texture_get(int id) const + { + auto it = textures.find(id); + return it == textures.end() ? nullptr : &it->second; + } + + resource::Audio* Document::sound_get(int id) + { + auto it = sounds.find(id); + return it == sounds.end() ? nullptr : &it->second; + } + + const resource::Audio* Document::sound_get(int id) const + { + auto it = sounds.find(id); + return it == sounds.end() ? nullptr : &it->second; + } + + bool Document::regions_trim(int spritesheetId, const std::set& ids) + { + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, spritesheetId); + auto texture = texture_get(spritesheetId); + if (!spritesheet || !texture || !texture->is_valid() || texture->pixels.empty() || ids.empty()) return false; + + bool isChanged{}; + for (auto id : ids) + { + auto region = element_child_id_get(*spritesheet, ElementType::REGION, id); + if (!region) continue; + + auto minPoint = glm::ivec2(glm::min(region->crop, region->crop + region->size)); + auto maxPoint = glm::ivec2(glm::max(region->crop, region->crop + region->size)); + int minX = std::max(0, minPoint.x); + int minY = std::max(0, minPoint.y); + int maxX = std::min(texture->size.x, maxPoint.x); + int maxY = std::min(texture->size.y, maxPoint.y); + if (minX >= maxX || minY >= maxY) continue; + + int contentMinX = std::numeric_limits::max(); + int contentMinY = std::numeric_limits::max(); + int contentMaxX = std::numeric_limits::min(); + int contentMaxY = std::numeric_limits::min(); + + for (int y = minY; y < maxY; ++y) + { + for (int x = minX; x < maxX; ++x) + { + auto index = ((std::size_t)y * texture->size.x + x) * resource::texture::CHANNELS; + if (index + resource::texture::CHANNELS > texture->pixels.size()) continue; + auto r = texture->pixels[index + 0]; + auto g = texture->pixels[index + 1]; + auto b = texture->pixels[index + 2]; + auto a = texture->pixels[index + 3]; + if (r == 0 && g == 0 && b == 0 && a == 0) continue; + contentMinX = std::min(contentMinX, x); + contentMinY = std::min(contentMinY, y); + contentMaxX = std::max(contentMaxX, x); + contentMaxY = std::max(contentMaxY, y); + } + } + + if (contentMinX == std::numeric_limits::max()) continue; + + auto newCrop = glm::vec2(contentMinX, contentMinY); + auto newSize = glm::vec2(contentMaxX - contentMinX + 1, contentMaxY - contentMinY + 1); + if (region->crop == newCrop && region->size == newSize) continue; + + auto previousCrop = region->crop; + region->crop = newCrop; + region->size = newSize; + if (region->origin == Origin::TOP_LEFT) + region->pivot = {}; + else if (region->origin == Origin::CENTER) + region->pivot = {static_cast(region->size.x / 2.0f), static_cast(region->size.y / 2.0f)}; + else + region->pivot -= region->crop - previousCrop; + isChanged = true; + } + + return isChanged; + } + + bool Document::spritesheet_pack(int id, int padding) + { + struct RectI + { + int x{}; + int y{}; + int w{}; + int h{}; + }; + + struct PackItem + { + int regionId{-1}; + int srcX{}; + int srcY{}; + int width{}; + int height{}; + int packWidth{}; + int packHeight{}; + }; + + class MaxRectsPacker + { + int width{}; + int height{}; + std::vector freeRects{}; + + static bool intersects(const RectI& a, const RectI& b) + { + return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y); + } + + static bool contains(const RectI& a, const RectI& b) + { + return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h; + } + + void split_free_rects(const RectI& used) + { + std::vector next{}; + next.reserve(freeRects.size() * 2); + for (auto& free : freeRects) + { + if (!intersects(free, used)) + { + next.push_back(free); + continue; + } + if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h}); + if (used.x + used.w < free.x + free.w) + next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h}); + if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y}); + if (used.y + used.h < free.y + free.h) + next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)}); + } + freeRects = std::move(next); + } + + void prune_free_rects() + { + for (int i = 0; i < (int)freeRects.size(); ++i) + { + if (freeRects[i].w <= 0 || freeRects[i].h <= 0) + { + freeRects.erase(freeRects.begin() + i--); + continue; + } + for (int j = i + 1; j < (int)freeRects.size();) + { + if (contains(freeRects[i], freeRects[j])) + freeRects.erase(freeRects.begin() + j); + else if (contains(freeRects[j], freeRects[i])) + { + freeRects.erase(freeRects.begin() + i--); + break; + } + else + ++j; + } + } + } + + public: + MaxRectsPacker(int width, int height) : width(width), height(height), freeRects({{0, 0, width, height}}) {} + + bool insert(int width, int height, RectI& result) + { + int bestShort = std::numeric_limits::max(); + int bestLong = std::numeric_limits::max(); + RectI best{}; + bool isFound{}; + for (auto& free : freeRects) + { + if (width > free.w || height > free.h) continue; + int leftOverW = free.w - width; + int leftOverH = free.h - height; + int shortSide = std::min(leftOverW, leftOverH); + int longSide = std::max(leftOverW, leftOverH); + if (shortSide < bestShort || (shortSide == bestShort && longSide < bestLong)) + { + bestShort = shortSide; + bestLong = longSide; + best = {free.x, free.y, width, height}; + isFound = true; + } + } + if (!isFound) return false; + result = best; + split_free_rects(best); + prune_free_rects(); + return true; + } + }; + + auto pack_regions = [&](const std::vector& items, int& packedWidth, int& packedHeight, + std::unordered_map& packedRects) + { + if (items.empty()) return false; + + int maxWidth{}; + int maxHeight{}; + int sumWidth{}; + int sumHeight{}; + std::int64_t totalArea{}; + for (auto& item : items) + { + maxWidth = std::max(maxWidth, item.packWidth); + maxHeight = std::max(maxHeight, item.packHeight); + sumWidth += item.packWidth; + sumHeight += item.packHeight; + totalArea += (std::int64_t)item.packWidth * item.packHeight; + } + if (maxWidth <= 0 || maxHeight <= 0) return false; + + int bestSquareDelta = std::numeric_limits::max(); + int bestArea = std::numeric_limits::max(); + std::unordered_map bestRects{}; + int bestWidth{}; + int bestHeight{}; + int startWidth = maxWidth; + int endWidth = std::max(startWidth, sumWidth); + int step = std::max(1, (endWidth - startWidth) / 512); + + for (int candidateWidth = startWidth; candidateWidth <= endWidth; candidateWidth += step) + { + int candidateHeightMin = std::max(maxHeight, (int)std::ceil((double)totalArea / candidateWidth)); + bool isValid{}; + int usedWidth{}; + int usedHeight{}; + std::unordered_map candidateRects{}; + for (int candidateHeight = candidateHeightMin; candidateHeight <= sumHeight; ++candidateHeight) + { + MaxRectsPacker packer(candidateWidth, candidateHeight); + candidateRects.clear(); + isValid = true; + usedWidth = 0; + usedHeight = 0; + for (auto& item : items) + { + RectI rect{}; + if (!packer.insert(item.packWidth, item.packHeight, rect)) + { + isValid = false; + break; + } + candidateRects[item.regionId] = rect; + usedWidth = std::max(usedWidth, rect.x + rect.w); + usedHeight = std::max(usedHeight, rect.y + rect.h); + } + if (isValid) break; + } + if (!isValid) continue; + + int area = usedWidth * usedHeight; + int squareDelta = std::abs(usedWidth - usedHeight); + if (squareDelta < bestSquareDelta || (squareDelta == bestSquareDelta && area < bestArea)) + { + bestSquareDelta = squareDelta; + bestArea = area; + bestWidth = usedWidth; + bestHeight = usedHeight; + bestRects = std::move(candidateRects); + if (bestArea == totalArea && bestSquareDelta == 0) break; + } + } + + if (bestArea == std::numeric_limits::max()) return false; + packedWidth = bestWidth; + packedHeight = bestHeight; + packedRects = std::move(bestRects); + return true; + }; + + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); + auto texture = texture_get(id); + if (!spritesheet || !texture || !texture->is_valid() || texture->pixels.empty()) return false; + + auto packingPadding = std::max(0, padding); + std::vector items{}; + for (auto& region : spritesheet->children) + { + if (region.type != ElementType::REGION) continue; + auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size)); + auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size)); + auto size = glm::max(maxPoint - minPoint, glm::ivec2(1)); + items.push_back({region.id, minPoint.x, minPoint.y, size.x, size.y, size.x + packingPadding * 2, + size.y + packingPadding * 2}); + } + if (items.empty()) return false; + + std::sort(items.begin(), items.end(), + [](const PackItem& a, const PackItem& b) + { + int areaA = a.width * a.height; + int areaB = b.width * b.height; + if (areaA != areaB) return areaA > areaB; + return a.regionId < b.regionId; + }); + + int packedWidth{}; + int packedHeight{}; + std::unordered_map packedRects{}; + if (!pack_regions(items, packedWidth, packedHeight, packedRects)) return false; + if (packedWidth <= 0 || packedHeight <= 0) return false; + + std::vector packedPixels((std::size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0); + for (auto& item : items) + { + if (!packedRects.contains(item.regionId)) continue; + auto destinationRect = packedRects.at(item.regionId); + for (int y = 0; y < item.height; ++y) + for (int x = 0; x < item.width; ++x) + { + int sourceX = item.srcX + x; + int sourceY = item.srcY + y; + int destinationX = destinationRect.x + packingPadding + x; + int destinationY = destinationRect.y + packingPadding + y; + if (sourceX < 0 || sourceY < 0 || sourceX >= texture->size.x || sourceY >= texture->size.y) continue; + if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight) + continue; + auto sourceIndex = ((std::size_t)sourceY * texture->size.x + sourceX) * resource::texture::CHANNELS; + auto destinationIndex = + ((std::size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS; + std::copy_n(texture->pixels.data() + sourceIndex, resource::texture::CHANNELS, + packedPixels.data() + destinationIndex); + } + } + + textures[id] = resource::Texture(packedPixels.data(), {packedWidth, packedHeight}); + for (auto& region : spritesheet->children) + if (region.type == ElementType::REGION && packedRects.contains(region.id)) + { + auto& rect = packedRects.at(region.id); + region.crop = {rect.x + packingPadding, rect.y + packingPadding}; + } + + assets_sync(SPRITESHEETS); + return true; + } + + bool Document::spritesheets_merge(const std::set& ids, bool isAppendRight, bool isMakeRegions, + bool isMakePrimaryRegion, origin::Type regionOrigin) + { + if (ids.size() < 2) return false; + auto baseId = *ids.begin(); + auto base = anm2.element_get(ElementType::SPRITESHEET, baseId); + auto baseTexture = texture_get(baseId); + if (!base || !baseTexture || !baseTexture->is_valid()) return false; + for (auto id : ids) + if (!anm2.element_get(ElementType::SPRITESHEET, id) || !texture_get(id) || !texture_get(id)->is_valid()) + return false; + + auto origin = regionOrigin == origin::ORIGIN_CENTER ? Origin::CENTER : Origin::TOP_LEFT; + auto baseTextureSize = baseTexture->size; + auto mergedTexture = *baseTexture; + std::unordered_map offsets{{baseId, {}}}; + + for (auto id : ids) + { + if (id == baseId) continue; + auto texture = texture_get(id); + offsets[id] = isAppendRight ? glm::ivec2(mergedTexture.size.x, 0) : glm::ivec2(0, mergedTexture.size.y); + mergedTexture = resource::Texture::merge_append(mergedTexture, *texture, isAppendRight); + } + textures[baseId] = std::move(mergedTexture); + + std::unordered_map> regionIdMap{}; + if (isMakeRegions) + { + auto add_location_region = [&](int sourceId, glm::ivec2 crop, glm::ivec2 size) + { + auto source = anm2.element_get(ElementType::SPRITESHEET, sourceId); + if (!source) return; + auto region = element_make(ElementType::REGION); + region.id = element_child_next_id_get(*base, ElementType::REGION); + auto stem = path::to_utf8(source->path.stem()); + region.name = stem.empty() ? std::format("#{}", sourceId) : stem; + region.crop = crop; + region.size = size; + region.pivot = origin == Origin::CENTER ? glm::vec2(size) * 0.5f : glm::vec2(); + region.origin = origin; + base->children.push_back(region); + }; + + if (isMakePrimaryRegion) add_location_region(baseId, {}, baseTextureSize); + for (auto id : ids) + { + if (id == baseId) continue; + auto source = anm2.element_get(ElementType::SPRITESHEET, id); + auto sourceTexture = texture_get(id); + auto sheetOffset = offsets.at(id); + add_location_region(id, sheetOffset, sourceTexture->size); + for (auto& sourceRegion : source->children) + { + if (sourceRegion.type != ElementType::REGION) continue; + auto destinationRegion = sourceRegion; + destinationRegion.id = element_child_next_id_get(*base, ElementType::REGION); + destinationRegion.crop += sheetOffset; + base->children.push_back(destinationRegion); + regionIdMap[id][sourceRegion.id] = destinationRegion.id; + } + } + } + + std::unordered_map layerSpritesheetBefore{}; + if (auto layers = anm2.element_get(ElementType::LAYERS)) + for (auto& layer : layers->children) + if (layer.type == ElementType::LAYER_ELEMENT && ids.contains(layer.spritesheetId)) + { + layerSpritesheetBefore[layer.id] = layer.spritesheetId; + layer.spritesheetId = baseId; + } + + if (auto animations = anm2.element_get(ElementType::ANIMATIONS)) + for (auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + auto layerAnimations = element_child_first_get(animation, ElementType::LAYER_ANIMATIONS); + if (!layerAnimations) continue; + auto layer_animation_update = [&](auto&& self, Element& layerAnimation) -> void + { + if (layerAnimation.type == ElementType::GROUP) + { + for (auto& child : layerAnimation.children) + self(self, child); + return; + } + if (layerAnimation.type != ElementType::LAYER_ANIMATION || + !layerSpritesheetBefore.contains(layerAnimation.layerId)) + return; + auto sourceSpritesheetId = layerSpritesheetBefore.at(layerAnimation.layerId); + if (sourceSpritesheetId == baseId) return; + for (auto& frame : layerAnimation.children) + { + if (frame.type != ElementType::FRAME || frame.regionId == -1) continue; + if (isMakeRegions && regionIdMap.contains(sourceSpritesheetId) && + regionIdMap.at(sourceSpritesheetId).contains(frame.regionId)) + frame.regionId = regionIdMap.at(sourceSpritesheetId).at(frame.regionId); + else + frame.regionId = -1; + } + }; + for (auto& layerAnimation : layerAnimations->children) + layer_animation_update(layer_animation_update, layerAnimation); + } + + auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS); + if (!spritesheets) return false; + for (auto id : ids) + if (id != baseId) + { + element_child_id_erase(*spritesheets, ElementType::SPRITESHEET, id); + textures.erase(id); + } + + assets_sync(ALL); + return true; + } + + void Document::scan_and_set_regions() + { + auto animations = anm2.element_get(ElementType::ANIMATIONS); + if (animations) + { + for (auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + auto layerAnimations = element_child_first_get(animation, ElementType::LAYER_ANIMATIONS); + if (!layerAnimations) continue; + auto layer_animation_update = [&](auto&& self, Element& layerAnimation) -> void + { + if (layerAnimation.type == ElementType::GROUP) + { + for (auto& child : layerAnimation.children) + self(self, child); + return; + } + if (layerAnimation.type != ElementType::LAYER_ANIMATION) return; + auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, layerAnimation.layerId); + auto spritesheet = layer ? anm2.element_get(ElementType::SPRITESHEET, layer->spritesheetId) : nullptr; + if (!spritesheet) return; + for (auto& frame : layerAnimation.children) + { + if (frame.type != ElementType::FRAME || frame.regionId != -1) continue; + auto frameCrop = glm::ivec2(frame.crop); + auto frameSize = glm::ivec2(frame.size); + auto framePivot = glm::ivec2(frame.pivot); + for (const auto& region : spritesheet->children) + if (region.type == ElementType::REGION && glm::ivec2(region.crop) == frameCrop && + glm::ivec2(region.size) == frameSize && glm::ivec2(region.pivot) == framePivot) + { + frame.regionId = region.id; + break; + } + } + }; + for (auto& layerAnimation : layerAnimations->children) + layer_animation_update(layer_animation_update, layerAnimation); + } + } + } + + bool Document::file_merge(const std::filesystem::path& path) + { + Anm2 source(path); + if (!source.isValid) return false; + + auto remap_path = [&](const std::filesystem::path& original) -> std::filesystem::path + { + if (directory_get().empty()) return original; + std::error_code ec{}; + std::filesystem::path absolute{}; + bool isAbsolute{}; + + if (!original.empty()) + { + if (original.is_absolute()) + { + absolute = original; + isAbsolute = true; + } + else + { + absolute = std::filesystem::weakly_canonical(path.parent_path() / original, ec); + if (ec) + { + ec.clear(); + absolute = path.parent_path() / original; + } + isAbsolute = true; + } + } + + if (!isAbsolute) return original; + auto relative = std::filesystem::relative(absolute, directory_get(), ec); + if (!ec) return relative; + return original.empty() ? absolute : original; + }; + + auto remap_id = [](const std::unordered_map& remap, int id) + { + auto it = remap.find(id); + return it == remap.end() ? -1 : it->second; + }; + + auto find_by_name = [](Element& container, ElementType type, const std::string& name) + { + for (auto& child : container.children) + if (child.type == type && child.name == name) return &child; + return (Element*)nullptr; + }; + + auto child_set = [](Element& container, Element child, ElementType type) + { + if (auto existing = element_child_first_get(container, type)) + *existing = std::move(child); + else + container.children.push_back(std::move(child)); + }; + + auto track_find = [](Element& container, const Element& source) + { + auto find = [](auto&& self, Element& parent, const Element& source) -> Element* + { + for (auto& track : parent.children) + { + if (track.type == ElementType::GROUP) + if (auto result = self(self, track, source)) return result; + if (track.type != source.type) continue; + if (track.type == ElementType::LAYER_ANIMATION && track.layerId == source.layerId) return &track; + if (track.type == ElementType::NULL_ANIMATION && track.nullId == source.nullId) return &track; + } + return (Element*)nullptr; + }; + return find(find, container, source); + }; + + auto track_container_get = [](Element& animation, ElementType type) + { + if (auto container = element_child_first_get(animation, type)) return container; + animation.children.push_back(element_make(type)); + return &animation.children.back(); + }; + + std::unordered_map spritesheetRemap{}; + std::unordered_map layerRemap{}; + std::unordered_map nullRemap{}; + std::unordered_map eventRemap{}; + std::unordered_map soundRemap{}; + + if (auto sourceSpritesheets = source.element_get(ElementType::SPRITESHEETS)) + if (auto destinationSpritesheets = anm2.element_get(ElementType::SPRITESHEETS)) + for (auto spritesheet : sourceSpritesheets->children) + { + if (spritesheet.type != ElementType::SPRITESHEET) continue; + auto sourceId = spritesheet.id; + spritesheet.id = element_child_next_id_get(*destinationSpritesheets, ElementType::SPRITESHEET); + spritesheet.path = remap_path(spritesheet.path); + destinationSpritesheets->children.push_back(spritesheet); + spritesheetRemap[sourceId] = spritesheet.id; + } + + if (auto sourceSounds = source.element_get(ElementType::SOUNDS)) + if (auto destinationSounds = anm2.element_get(ElementType::SOUNDS)) + for (auto sound : sourceSounds->children) + { + if (sound.type != ElementType::SOUND_ELEMENT) continue; + auto sourceId = sound.id; + sound.path = remap_path(sound.path); + int destinationId{-1}; + for (auto& existing : destinationSounds->children) + if (existing.type == ElementType::SOUND_ELEMENT && existing.path == sound.path) + { + destinationId = existing.id; + sound.id = destinationId; + existing = sound; + break; + } + if (destinationId == -1) + { + destinationId = element_child_next_id_get(*destinationSounds, ElementType::SOUND_ELEMENT); + sound.id = destinationId; + destinationSounds->children.push_back(sound); + } + soundRemap[sourceId] = destinationId; + } + + if (auto sourceLayers = source.element_get(ElementType::LAYERS)) + if (auto destinationLayers = anm2.element_get(ElementType::LAYERS)) + for (auto layer : sourceLayers->children) + { + if (layer.type != ElementType::LAYER_ELEMENT) continue; + auto sourceId = layer.id; + layer.spritesheetId = remap_id(spritesheetRemap, layer.spritesheetId); + if (auto existing = find_by_name(*destinationLayers, ElementType::LAYER_ELEMENT, layer.name)) + { + layer.id = existing->id; + *existing = layer; + } + else + { + layer.id = element_child_next_id_get(*destinationLayers, ElementType::LAYER_ELEMENT); + destinationLayers->children.push_back(layer); + } + layerRemap[sourceId] = layer.id; + } + + if (auto sourceNulls = source.element_get(ElementType::NULLS)) + if (auto destinationNulls = anm2.element_get(ElementType::NULLS)) + for (auto null : sourceNulls->children) + { + if (null.type != ElementType::NULL_ELEMENT) continue; + auto sourceId = null.id; + if (auto existing = find_by_name(*destinationNulls, ElementType::NULL_ELEMENT, null.name)) + { + null.id = existing->id; + *existing = null; + } + else + { + null.id = element_child_next_id_get(*destinationNulls, ElementType::NULL_ELEMENT); + destinationNulls->children.push_back(null); + } + nullRemap[sourceId] = null.id; + } + + if (auto sourceEvents = source.element_get(ElementType::EVENTS)) + if (auto destinationEvents = anm2.element_get(ElementType::EVENTS)) + for (auto event : sourceEvents->children) + { + if (event.type != ElementType::EVENT_ELEMENT) continue; + auto sourceId = event.id; + if (auto existing = find_by_name(*destinationEvents, ElementType::EVENT_ELEMENT, event.name)) + { + event.id = existing->id; + *existing = event; + } + else + { + event.id = element_child_next_id_get(*destinationEvents, ElementType::EVENT_ELEMENT); + destinationEvents->children.push_back(event); + } + eventRemap[sourceId] = event.id; + } + + auto item_remap = [&](Element& item) + { + for (auto& frame : item.children) + { + if (frame.type != ElementType::FRAME && frame.type != ElementType::TRIGGER) continue; + for (auto& soundId : frame.soundIds) + soundId = remap_id(soundRemap, soundId); + frame.eventId = remap_id(eventRemap, frame.eventId); + } + }; + + auto track_tree_remap = [&](auto&& self, Element item, int itemType, Element& container, int parentGroupId = -1) -> void + { + auto trackType = itemType == LAYER ? ElementType::LAYER_ANIMATION : ElementType::NULL_ANIMATION; + if (item.type == ElementType::GROUP) + { + auto group = element_make(ElementType::GROUP); + group.id = item.id == -1 ? element_child_next_id_get(container, ElementType::GROUP) : item.id; + group.name = item.name; + group.isExpanded = item.isExpanded; + container.children.push_back(group); + for (auto child : item.children) + self(self, child, itemType, container, group.id); + return; + } + + if (item.type != trackType) return; + if (itemType == LAYER) + item.layerId = remap_id(layerRemap, item.layerId); + else + item.nullId = remap_id(nullRemap, item.nullId); + if ((itemType == LAYER && item.layerId < 0) || (itemType == NULL_ && item.nullId < 0)) return; + if (parentGroupId != -1) item.groupId = parentGroupId; + item_remap(item); + container.children.push_back(item); + }; + + auto animation_build = [&](const Element& incoming) + { + auto animation = element_make(ElementType::ANIMATION); + animation.name = incoming.name; + animation.frameNum = incoming.frameNum; + animation.isLoop = incoming.isLoop; + + if (auto root = element_child_first_get(incoming, ElementType::ROOT_ANIMATION)) + { + auto item = *root; + item_remap(item); + animation.children.push_back(item); + } + + if (auto layerAnimations = element_child_first_get(incoming, ElementType::LAYER_ANIMATIONS)) + { + auto container = element_make(ElementType::LAYER_ANIMATIONS); + for (auto item : layerAnimations->children) + track_tree_remap(track_tree_remap, item, LAYER, container); + animation.children.push_back(container); + } + + if (auto nullAnimations = element_child_first_get(incoming, ElementType::NULL_ANIMATIONS)) + { + auto container = element_make(ElementType::NULL_ANIMATIONS); + for (auto item : nullAnimations->children) + track_tree_remap(track_tree_remap, item, NULL_, container); + animation.children.push_back(container); + } + + if (auto triggers = element_child_first_get(incoming, ElementType::TRIGGERS)) + { + auto item = *triggers; + item_remap(item); + animation.children.push_back(item); + } + + return animation; + }; + + auto track_merge = [&](Element& destinationContainer, const Element& sourceTrack) + { + if (auto destinationTrack = track_find(destinationContainer, sourceTrack)) + { + if (!sourceTrack.children.empty()) *destinationTrack = sourceTrack; + } + else + destinationContainer.children.push_back(sourceTrack); + }; + + auto track_container_merge = [&](Element& destinationContainer, const Element& sourceContainer, ElementType trackType) + { + std::unordered_map groupRemap{}; + for (auto item : sourceContainer.children) + { + if (item.type != ElementType::GROUP) continue; + auto sourceGroupId = item.id; + item.id = element_child_next_id_get(destinationContainer, ElementType::GROUP); + item.children.clear(); + destinationContainer.children.push_back(item); + groupRemap[sourceGroupId] = item.id; + } + + for (auto item : sourceContainer.children) + { + if (item.type != trackType) continue; + if (item.groupId != -1) + item.groupId = groupRemap.contains(item.groupId) ? groupRemap.at(item.groupId) : -1; + track_merge(destinationContainer, item); + } + }; + + if (auto sourceAnimations = source.element_get(ElementType::ANIMATIONS)) + if (auto destinationAnimations = anm2.element_get(ElementType::ANIMATIONS)) + { + for (const auto& incoming : sourceAnimations->children) + { + if (incoming.type != ElementType::ANIMATION) continue; + auto processed = animation_build(incoming); + auto destination = find_by_name(*destinationAnimations, ElementType::ANIMATION, processed.name); + if (!destination) + { + destinationAnimations->children.push_back(processed); + continue; + } + + destination->frameNum = std::max(destination->frameNum, processed.frameNum); + destination->isLoop = processed.isLoop; + if (auto root = element_child_first_get(processed, ElementType::ROOT_ANIMATION); + root && !root->children.empty()) + child_set(*destination, *root, ElementType::ROOT_ANIMATION); + if (auto triggers = element_child_first_get(processed, ElementType::TRIGGERS); + triggers && !triggers->children.empty()) + child_set(*destination, *triggers, ElementType::TRIGGERS); + + if (auto layerAnimations = element_child_first_get(processed, ElementType::LAYER_ANIMATIONS)) + { + auto destinationLayerAnimations = track_container_get(*destination, ElementType::LAYER_ANIMATIONS); + track_container_merge(*destinationLayerAnimations, *layerAnimations, ElementType::LAYER_ANIMATION); + } + + if (auto nullAnimations = element_child_first_get(processed, ElementType::NULL_ANIMATIONS)) + { + auto destinationNullAnimations = track_container_get(*destination, ElementType::NULL_ANIMATIONS); + track_container_merge(*destinationNullAnimations, *nullAnimations, ElementType::NULL_ANIMATION); + } + + destination->frameNum = std::max(destination->frameNum, animation_length_get(*destination)); + } + + if (destinationAnimations->defaultAnimation.empty() && !sourceAnimations->defaultAnimation.empty()) + destinationAnimations->defaultAnimation = sourceAnimations->defaultAnimation; + } + + assets_sync(ALL); + return true; + } + void Document::hash_set() { hash = anm2.hash(); } void Document::clean() { + assets_sync(); saveHash = anm2.hash(); hash = saveHash; lastAutosaveTime = 0.0f; @@ -237,19 +1311,26 @@ namespace anm2ed { spritesheetHashes.clear(); spritesheetSaveHashes.clear(); - for (auto& [id, spritesheet] : anm2.content.spritesheets) - { - auto currentHash = spritesheet.hash(); - spritesheetHashes[id] = currentHash; - spritesheetSaveHashes[id] = currentHash; - } + if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + if (spritesheet.type == ElementType::SPRITESHEET) + { + auto currentHash = document::spritesheet_hash_get(spritesheet, texture_get(spritesheet.id)); + spritesheetHashes[spritesheet.id] = currentHash; + spritesheetSaveHashes[spritesheet.id] = currentHash; + } } void Document::spritesheet_hashes_sync() { + std::set validIds{}; + if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + if (spritesheet.type == ElementType::SPRITESHEET) validIds.insert(spritesheet.id); + for (auto it = spritesheetHashes.begin(); it != spritesheetHashes.end();) { - if (!anm2.content.spritesheets.contains(it->first)) + if (!validIds.contains(it->first)) it = spritesheetHashes.erase(it); else ++it; @@ -257,45 +1338,56 @@ namespace anm2ed for (auto it = spritesheetSaveHashes.begin(); it != spritesheetSaveHashes.end();) { - if (!anm2.content.spritesheets.contains(it->first)) + if (!validIds.contains(it->first)) it = spritesheetSaveHashes.erase(it); else ++it; } - for (auto& [id, spritesheet] : anm2.content.spritesheets) - { - auto currentHash = spritesheet.hash(); - spritesheetHashes[id] = currentHash; - if (!spritesheetSaveHashes.contains(id)) spritesheetSaveHashes[id] = currentHash; - } + if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + if (spritesheet.type == ElementType::SPRITESHEET) + { + auto currentHash = document::spritesheet_hash_get(spritesheet, texture_get(spritesheet.id)); + spritesheetHashes[spritesheet.id] = currentHash; + if (!spritesheetSaveHashes.contains(spritesheet.id)) spritesheetSaveHashes[spritesheet.id] = currentHash; + } } void Document::change(ChangeType type) { hash_set(); + assets_sync(type); - auto events_set = [&]() { event.labels_set(anm2.event_labels_get(), anm2.event_ids_get()); }; + auto events_set = [&]() + { + auto events = anm2.element_get(ElementType::EVENTS); + event.labels_set(document::element_name_labels_get(events, ElementType::EVENT_ELEMENT, true), + document::element_ids_get(events, ElementType::EVENT_ELEMENT, true)); + }; - auto animations_set = [&]() { animation.labels_set(anm2.animation_labels_get()); }; + auto animations_set = [&]() { animation.labels_set(document::animation_labels_get(anm2)); }; auto spritesheets_set = [&]() { - spritesheet.labels_set(anm2.spritesheet_labels_get(), anm2.spritesheet_ids_get()); + spritesheet.labels_set(document::spritesheet_labels_get(anm2), document::spritesheet_ids_get(anm2)); spritesheet_hashes_sync(); }; - auto sounds_set = [&]() { sound.labels_set(anm2.sound_labels_get(), anm2.sound_ids_get()); }; + auto sounds_set = [&]() { sound.labels_set(document::sound_labels_get(anm2), document::sound_ids_get(anm2)); }; auto regions_set = [&]() { regionBySpritesheet.clear(); - for (auto& [id, spritesheet] : anm2.content.spritesheets) - { - Storage storage{}; - storage.labels_set(anm2.region_labels_get(spritesheet), anm2.region_ids_get(spritesheet)); - regionBySpritesheet.emplace(id, std::move(storage)); - } + if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + if (spritesheet.type == ElementType::SPRITESHEET) + { + Storage storage{}; + storage.labels_set(document::element_name_labels_get(&spritesheet, ElementType::REGION, true), + document::element_ids_get(&spritesheet, ElementType::REGION, true)); + regionBySpritesheet.emplace(spritesheet.id, std::move(storage)); + } }; switch (type) @@ -338,21 +1430,25 @@ namespace anm2ed bool Document::is_autosave_dirty() const { return hash != autosaveHash; } void Document::spritesheet_hash_update(int id) { - if (!anm2.content.spritesheets.contains(id)) return; - spritesheetHashes[id] = anm2.content.spritesheets.at(id).hash(); + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) return; + assets_sync(TEXTURES); + spritesheetHashes[id] = document::spritesheet_hash_get(*spritesheet, texture_get(id)); } void Document::spritesheet_hash_set_saved(int id) { - if (!anm2.content.spritesheets.contains(id)) return; - auto currentHash = anm2.content.spritesheets.at(id).hash(); + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) return; + assets_sync(TEXTURES); + auto currentHash = document::spritesheet_hash_get(*spritesheet, texture_get(id)); spritesheetHashes[id] = currentHash; spritesheetSaveHashes[id] = currentHash; } bool Document::spritesheet_is_dirty(int id) { - if (!anm2.content.spritesheets.contains(id)) return false; + if (!anm2.element_get(ElementType::SPRITESHEET, id)) return false; if (!spritesheetHashes.contains(id)) spritesheet_hash_update(id); auto saveIt = spritesheetSaveHashes.find(id); if (saveIt == spritesheetSaveHashes.end()) return false; @@ -361,83 +1457,132 @@ namespace anm2ed bool Document::spritesheet_any_dirty() { - for (auto& [id, spritesheet] : anm2.content.spritesheets) - { - if (spritesheet_is_dirty(id)) return true; - } + if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) + for (auto& spritesheet : spritesheets->children) + if (spritesheet.type == ElementType::SPRITESHEET && spritesheet_is_dirty(spritesheet.id)) return true; return false; } std::filesystem::path Document::directory_get() const { return path.parent_path(); } std::filesystem::path Document::filename_get() const { return path.filename(); } bool Document::is_valid() const { return isValid && !path.empty(); } - anm2::Frame* Document::frame_get() + void Document::command_run(Manager& manager, Command& command) { - return anm2.frame_get(reference.animationIndex, reference.itemType, reference.frameIndex, reference.itemID); + if (command.run) command.run(manager, *this); } - anm2::Item* Document::item_get() + Element* Document::frame_get() { - return anm2.item_get(reference.animationIndex, reference.itemType, reference.itemID); + return anm2.element_get(reference.animationIndex, document::item_type_get(reference.itemType), reference.frameIndex, + reference.itemID); } - anm2::Animation* Document::animation_get() { return anm2.animation_get(reference.animationIndex); } - anm2::Spritesheet* Document::spritesheet_get() { return anm2.spritesheet_get(spritesheet.reference); } + + Element* Document::item_get() + { + return anm2.element_get(reference.animationIndex, document::item_type_get(reference.itemType), reference.itemID); + } + Element* Document::animation_get() { return anm2.element_get(ElementType::ANIMATION, reference.animationIndex); } + Element* Document::spritesheet_get() { return anm2.element_get(ElementType::SPRITESHEET, spritesheet.reference); } void Document::spritesheet_add(const std::filesystem::path& path) { - auto add = [&]() + spritesheets_add({path}); + } + + void Document::spritesheets_add(const std::vector& paths) + { + struct LoadedSpritesheet + { + std::filesystem::path path{}; + resource::Texture texture{}; + }; + + auto items = anm2.element_get(ElementType::SPRITESHEETS); + if (!items) return; + + std::vector loaded{}; + for (auto& path : paths) { - int id{}; auto pathCopy = path; - if (anm2.spritesheet_add(directory_get(), path, id)) - { - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - auto pathString = path::to_utf8(spritesheet.path); - this->spritesheet.selection = {id}; - this->spritesheet.reference = id; - spritesheet_hash_set_saved(id); - toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, pathString))); - logger.info(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - } - else + WorkingDirectory workingDirectory(directory_get()); + auto loadPath = path::lower_case_backslash_handle(path); + auto texture = resource::Texture(loadPath); + if (!texture.is_valid()) { auto pathUtf8 = path::to_utf8(pathCopy); toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED), std::make_format_args(pathUtf8))); logger.error(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED, anm2ed::ENGLISH), std::make_format_args(pathUtf8))); + continue; } - }; - DOCUMENT_EDIT_PTR(this, localize.get(EDIT_ADD_SPRITESHEET), Document::SPRITESHEETS, add()); + loaded.push_back({.path = pathCopy, .texture = std::move(texture)}); + } + if (loaded.empty()) return; + + snapshot(localize.get(EDIT_ADD_SPRITESHEET)); + + std::set added{}; + for (auto& loadedSpritesheet : loaded) + { + auto id = element_child_next_id_get(*items, ElementType::SPRITESHEET); + auto spritesheet = element_make(ElementType::SPRITESHEET); + spritesheet.id = id; + spritesheet.path = path::lower_case_backslash_handle(path::make_relative(loadedSpritesheet.path)); + auto pathString = path::to_utf8(spritesheet.path); + items->children.push_back(spritesheet); + textures[id] = std::move(loadedSpritesheet.texture); + texturePaths[id] = spritesheet.path; + added.insert(id); + this->spritesheet.reference = id; + spritesheet_hash_set_saved(id); + toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, pathString))); + logger.info(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + } + this->spritesheet.selection = added; + change(Document::SPRITESHEETS); } void Document::sound_add(const std::filesystem::path& path) { - auto add = [&]() - { - int id{}; - auto pathCopy = path; - if (anm2.sound_add(directory_get(), path, id)) - { - auto& soundInfo = anm2.content.sounds[id]; - auto soundPath = path::to_utf8(soundInfo.path); - sound.selection = {id}; - sound.reference = id; - toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, soundPath))); - logger.info( - std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH), std::make_format_args(id, soundPath))); - } - else - { - auto pathUtf8 = path::to_utf8(pathCopy); - toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathUtf8))); - logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH), - std::make_format_args(pathUtf8))); - } - }; + sounds_add({path}); + } - DOCUMENT_EDIT_PTR(this, localize.get(EDIT_ADD_SOUND), Document::SOUNDS, add()); + void Document::sounds_add(const std::vector& paths) + { + auto items = anm2.element_get(ElementType::SOUNDS); + if (!items) return; + + std::vector validPaths{}; + for (auto& path : paths) + if (!path.empty()) validPaths.push_back(path); + if (validPaths.empty()) return; + + snapshot(localize.get(EDIT_ADD_SOUND)); + + std::set added{}; + for (auto& path : validPaths) + { + auto id = element_child_next_id_get(*items, ElementType::SOUND_ELEMENT); + auto soundElement = element_make(ElementType::SOUND_ELEMENT); + soundElement.id = id; + { + WorkingDirectory workingDirectory(directory_get()); + soundElement.path = path::lower_case_backslash_handle(path::make_relative(path)); + sounds[id] = resource::Audio(soundElement.path); + soundPaths[id] = soundElement.path; + } + auto soundPath = path::to_utf8(soundElement.path); + items->children.push_back(soundElement); + added.insert(id); + sound.reference = id; + toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, soundPath))); + logger.info( + std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH), std::make_format_args(id, soundPath))); + } + sound.selection = added; + change(Document::SOUNDS); } void Document::snapshot(const std::string& message) @@ -449,7 +1594,7 @@ namespace anm2ed void Document::undo() { if (!snapshots.undo()) return; - restored_snapshot_sanitize(*this); + document::restored_snapshot_sanitize(*this); toasts.push(std::vformat(localize.get(TOAST_UNDO), std::make_format_args(message))); logger.info(std::vformat(localize.get(TOAST_UNDO, anm2ed::ENGLISH), std::make_format_args(message))); change(Document::ALL); @@ -458,7 +1603,7 @@ namespace anm2ed void Document::redo() { if (!snapshots.redo()) return; - restored_snapshot_sanitize(*this); + document::restored_snapshot_sanitize(*this); toasts.push(std::vformat(localize.get(TOAST_REDO), std::make_format_args(message))); logger.info(std::vformat(localize.get(TOAST_REDO, anm2ed::ENGLISH), std::make_format_args(message))); change(Document::ALL); diff --git a/src/document.hpp b/src/document.hpp index aacdf1b..7ba6c38 100644 --- a/src/document.hpp +++ b/src/document.hpp @@ -2,14 +2,21 @@ #include #include +#include #include +#include #include "snapshots.hpp" #include +#include "origin.hpp" +#include "types.hpp" + namespace anm2ed { + class Manager; + struct Command; class Document { @@ -46,8 +53,10 @@ namespace anm2ed Storage& region = current.region; Storage& sound = current.sound; Storage& spritesheet = current.spritesheet; - anm2::Anm2& anm2 = current.anm2; - anm2::Reference& reference = current.reference; + std::map& textures = current.textures; + std::map& sounds = current.sounds; + Anm2& anm2 = current.anm2; + Reference& reference = current.reference; float& frameTime = current.frameTime; std::string& message = current.message; std::map regionBySpritesheet{}; @@ -68,17 +77,30 @@ namespace anm2ed bool isForceDirty{false}; std::unordered_map spritesheetHashes{}; std::unordered_map spritesheetSaveHashes{}; + std::unordered_map texturePaths{}; + std::unordered_map soundPaths{}; bool isAnimationPreviewSet{false}; bool isSpritesheetEditorSet{false}; - Document(anm2::Anm2& anm2, const std::filesystem::path&); Document(const std::filesystem::path&, bool = false, std::string* = nullptr); Document(const Document&) = delete; Document& operator=(const Document&) = delete; Document(Document&&) noexcept; Document& operator=(Document&&) noexcept; - bool save(const std::filesystem::path& = {}, std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED, + bool save(const std::filesystem::path& = {}, std::string* = nullptr, Compatibility = Compatibility::ANM2ED, bool = false, bool = true, bool = true); + void anm2_change(ChangeType); + void assets_sync(ChangeType = ALL); + void texture_change(int); + resource::Texture* texture_get(int); + const resource::Texture* texture_get(int) const; + resource::Audio* sound_get(int); + const resource::Audio* sound_get(int) const; + bool regions_trim(int, const std::set&); + bool spritesheet_pack(int, int); + bool spritesheets_merge(const std::set&, bool, bool, bool, origin::Type); + void scan_and_set_regions(); + bool file_merge(const std::filesystem::path&); void hash_set(); void clean(); void change(ChangeType); @@ -87,6 +109,7 @@ namespace anm2ed std::filesystem::path directory_get() const; std::filesystem::path filename_get() const; bool is_valid() const; + void command_run(Manager&, Command&); void spritesheet_hash_update(int); void spritesheet_hash_set_saved(int); bool spritesheet_is_dirty(int); @@ -94,15 +117,17 @@ namespace anm2ed void spritesheet_hashes_reset(); void spritesheet_hashes_sync(); - anm2::Frame* frame_get(); - anm2::Item* item_get(); - anm2::Spritesheet* spritesheet_get(); - anm2::Animation* animation_get(); + Element* frame_get(); + Element* item_get(); + Element* spritesheet_get(); + Element* animation_get(); void spritesheet_add(const std::filesystem::path&); + void spritesheets_add(const std::vector&); void sound_add(const std::filesystem::path&); + void sounds_add(const std::vector&); - bool autosave(std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true, + bool autosave(std::string* = nullptr, Compatibility = Compatibility::ANM2ED, bool = false, bool = true, bool = true); std::filesystem::path autosave_path_get(); std::filesystem::path path_from_autosave_get(const std::filesystem::path&); diff --git a/src/imgui/actions.cpp b/src/imgui/actions.cpp new file mode 100644 index 0000000..f26078f --- /dev/null +++ b/src/imgui/actions.cpp @@ -0,0 +1,122 @@ +#include "actions.hpp" + +#include + +namespace anm2ed::imgui +{ + bool is_action_shortcut_valid(int shortcutType) { return shortcutType >= 0 && shortcutType < SHORTCUT_COUNT; } + + std::string action_shortcut_text_get(const Action& action, Settings& settings) + { + if (!is_action_shortcut_valid(action.shortcut)) return {}; + return settings.*SHORTCUT_MEMBERS[action.shortcut]; + } + + ImGuiKeyChord action_chord_get(const Action& action, Manager& manager) + { + if (!is_action_shortcut_valid(action.shortcut)) return ImGuiKey_None; + return manager.chords[action.shortcut]; + } + + bool is_action_enabled(const Action& action) { return !action.isEnabled || action.isEnabled(); } + + Action action_make(ActionType type, std::function isEnabled, std::function run, StringType tooltip, + int shortcut) + { + auto info = ACTION_INFOS[type]; + return {.type = type, + .label = info.label, + .tooltip = tooltip, + .shortcut = shortcut == ACTION_COUNT ? info.shortcut : shortcut, + .isEnabled = std::move(isEnabled), + .run = std::move(run)}; + } + + void Actions::add(Action action) { items.push_back(std::move(action)); } + + void Actions::add(ActionType type, std::function isEnabled, std::function run, StringType tooltip, + int shortcut) + { + add(action_make(type, std::move(isEnabled), std::move(run), tooltip, shortcut)); + } + + void Actions::separator() { items.push_back({.isSeparator = true}); } + + void actions_undo_redo_add(Actions& actions, Manager& manager, Document& document) + { + actions.add(ACTION_UNDO, [&document]() { return document.is_able_to_undo(); }, + [&manager]() + { manager.command_push({manager.selected, [](Manager&, Document& document) { document.undo(); }}); }); + actions.add(ACTION_REDO, [&document]() { return document.is_able_to_redo(); }, + [&manager]() + { manager.command_push({manager.selected, [](Manager&, Document& document) { document.redo(); }}); }); + } + + void actions_menu_draw(Actions& actions, Settings& settings) + { + bool isPreviousSeparator{}; + for (auto& action : actions.items) + { + if (action.isSeparator) + { + if (!isPreviousSeparator) ImGui::Separator(); + isPreviousSeparator = true; + continue; + } + + auto shortcutText = action_shortcut_text_get(action, settings); + auto shortcutLabel = shortcutText.empty() ? nullptr : shortcutText.c_str(); + if (ImGui::MenuItem(localize.get(action.label), shortcutLabel, false, is_action_enabled(action)) && action.run) + action.run(); + if (action.tooltip != STRING_UNDEFINED && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetItemTooltip("%s", localize.get(action.tooltip)); + isPreviousSeparator = false; + } + } + + bool actions_context_window_draw(const char* label, Actions& actions, Settings& settings, ImGuiPopupFlags flags) + { + if (!ImGui::BeginPopupContextWindow(label, flags)) return false; + actions_menu_draw(actions, settings); + ImGui::EndPopup(); + return true; + } + + bool actions_popup_draw(const char* label, Actions& actions, Settings& settings) + { + if (!ImGui::BeginPopup(label)) return false; + actions_menu_draw(actions, settings); + ImGui::EndPopup(); + return true; + } + + void actions_shortcuts_update(Actions& actions, Manager& manager, types::shortcut::Type shortcutType) + { + for (auto& action : actions.items) + { + if (action.isSeparator || !action.run || !is_action_enabled(action)) continue; + auto chord = action_chord_get(action, manager); + if (!shortcut(chord, shortcutType)) continue; + action.run(); + break; + } + } + + void action_button_draw(Action& action, Manager& manager, Settings& settings, ImVec2 widgetSize, bool& isSameLine) + { + if (isSameLine) ImGui::SameLine(); + ImGui::BeginDisabled(!is_action_enabled(action)); + shortcut(action_chord_get(action, manager)); + if (ImGui::Button(localize.get(action.label), widgetSize) && action.run) action.run(); + ImGui::EndDisabled(); + if (action.tooltip != STRING_UNDEFINED) + { + auto shortcutText = action_shortcut_text_get(action, settings); + if (shortcutText.empty()) + ImGui::SetItemTooltip("%s", localize.get(action.tooltip)); + else + set_item_tooltip_shortcut(localize.get(action.tooltip), shortcutText); + } + isSameLine = true; + } +} diff --git a/src/imgui/actions.hpp b/src/imgui/actions.hpp new file mode 100644 index 0000000..17a2dca --- /dev/null +++ b/src/imgui/actions.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +#include "util/imgui/imgui.hpp" +#include "manager.hpp" +#include "settings.hpp" + +namespace anm2ed::imgui +{ +#define ACTIONS(X) \ + X(ACTION_UNDO, SHORTCUT_STRING_UNDO, SHORTCUT_UNDO) \ + X(ACTION_REDO, SHORTCUT_STRING_REDO, SHORTCUT_REDO) \ + X(ACTION_ADD, BASIC_ADD, SHORTCUT_ADD) \ + X(ACTION_REMOVE, BASIC_REMOVE, SHORTCUT_REMOVE) \ + X(ACTION_REMOVE_UNUSED, BASIC_REMOVE_UNUSED, SHORTCUT_REMOVE) \ + X(ACTION_DUPLICATE, BASIC_DUPLICATE, SHORTCUT_DUPLICATE) \ + X(ACTION_MERGE, BASIC_MERGE, SHORTCUT_MERGE) \ + X(ACTION_GROUP, BASIC_GROUP, SHORTCUT_GROUP) \ + X(ACTION_DEFAULT, BASIC_DEFAULT, SHORTCUT_DEFAULT) \ + X(ACTION_RENAME, BASIC_RENAME, SHORTCUT_RENAME) \ + X(ACTION_PROPERTIES, BASIC_PROPERTIES, -1) \ + X(ACTION_CUT, BASIC_CUT, SHORTCUT_CUT) \ + X(ACTION_COPY, BASIC_COPY, SHORTCUT_COPY) \ + X(ACTION_PASTE, BASIC_PASTE, SHORTCUT_PASTE) \ + X(ACTION_RELOAD, BASIC_RELOAD, -1) \ + X(ACTION_REPLACE, BASIC_REPLACE, -1) \ + X(ACTION_SAVE, BASIC_SAVE, -1) \ + X(ACTION_OPEN_DIRECTORY, BASIC_OPEN_DIRECTORY, -1) \ + X(ACTION_SET_FILE_PATH, BASIC_SET_FILE_PATH, -1) \ + X(ACTION_PACK, BASIC_PACK, -1) \ + X(ACTION_TRIM, BASIC_TRIM, -1) \ + X(ACTION_PLAY, LABEL_PLAY, SHORTCUT_PLAY_PAUSE) \ + X(ACTION_CENTER_VIEW, LABEL_CENTER_VIEW, SHORTCUT_CENTER_VIEW) \ + X(ACTION_FIT_VIEW, LABEL_FIT, SHORTCUT_FIT) \ + X(ACTION_ZOOM_IN, SHORTCUT_STRING_ZOOM_IN, SHORTCUT_ZOOM_IN) \ + X(ACTION_ZOOM_OUT, SHORTCUT_STRING_ZOOM_OUT, SHORTCUT_ZOOM_OUT) + + enum ActionType + { +#define X(name, label, shortcut) name, + ACTIONS(X) +#undef X + ACTION_COUNT + }; + + struct ActionInfo + { + StringType label{}; + int shortcut{-1}; + }; + + constexpr ActionInfo ACTION_INFOS[] = { +#define X(name, label, shortcut) {label, shortcut}, + ACTIONS(X) +#undef X + }; + + struct Action + { + ActionType type{}; + StringType label{STRING_UNDEFINED}; + StringType tooltip{STRING_UNDEFINED}; + int shortcut{-1}; + bool isSeparator{}; + std::function isEnabled{}; + std::function run{}; + }; + + struct Actions + { + std::vector items{}; + + void add(Action); + void add(ActionType, std::function, std::function, StringType = STRING_UNDEFINED, + int = ACTION_COUNT); + void separator(); + }; + + bool is_action_enabled(const Action&); + Action action_make(ActionType, std::function, std::function, StringType = STRING_UNDEFINED, + int = ACTION_COUNT); + void actions_undo_redo_add(Actions&, Manager&, Document&); + void actions_menu_draw(Actions&, Settings&); + bool actions_context_window_draw(const char*, Actions&, Settings&, ImGuiPopupFlags = ImGuiPopupFlags_MouseButtonRight); + bool actions_popup_draw(const char*, Actions&, Settings&); + void actions_shortcuts_update(Actions&, Manager&, types::shortcut::Type = types::shortcut::FOCUSED); + void action_button_draw(Action&, Manager&, Settings&, ImVec2, bool&); +} diff --git a/src/imgui/dockspace.cpp b/src/imgui/dockspace.cpp index 8c73106..99fdf80 100644 --- a/src/imgui/dockspace.cpp +++ b/src/imgui/dockspace.cpp @@ -2,6 +2,8 @@ namespace anm2ed::imgui { + bool Dockspace::is_canvas_focused_get() const { return isCanvasFocused; } + void Dockspace::tick(Manager& manager, Settings& settings, float deltaSeconds) { if (auto document = manager.get(); document) @@ -11,6 +13,7 @@ namespace anm2ed::imgui void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, Clipboard& clipboard) { + isCanvasFocused = false; auto viewport = ImGui::GetMainViewport(); auto windowHeight = viewport->Size.y - taskbar.height - documents.height; @@ -30,18 +33,21 @@ namespace anm2ed::imgui if (ImGui::DockSpace(ImGui::GetID("##DockSpace"), ImVec2(), ImGuiDockNodeFlags_PassthruCentralNode)) { if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources); - if (settings.windowIsAnimations) animations.update(manager, settings, resources, clipboard); - if (settings.windowIsRegions) regions.update(manager, settings, resources, clipboard); - if (settings.windowIsEvents) events.update(manager, settings, resources, clipboard); + if (settings.windowIsAnimations) window_update(animations, manager, settings, resources, dialog, clipboard); + if (settings.windowIsRegions) window_update(regions, manager, settings, resources, dialog, clipboard); + if (settings.windowIsEvents) window_update(events, manager, settings, resources, dialog, clipboard); if (settings.windowIsFrameProperties) frameProperties.update(manager, settings); - if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard); - if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard); + if (settings.windowIsLayers) window_update(layers, manager, settings, resources, dialog, clipboard); + if (settings.windowIsNulls) window_update(nulls, manager, settings, resources, dialog, clipboard); if (settings.windowIsOnionskin) onionskin.update(manager, settings); - if (settings.windowIsSounds) sounds.update(manager, settings, resources, dialog, clipboard); + if (settings.windowIsSounds) window_update(sounds, 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.windowIsSpritesheets) + window_update(spritesheets, manager, settings, resources, dialog, clipboard); if (settings.windowIsTimeline) timeline.update(manager, settings, resources, clipboard); if (settings.windowIsTools) tools.update(manager, settings, resources); + isCanvasFocused = (settings.windowIsAnimationPreview && animationPreview.is_focused_get()) || + (settings.windowIsSpritesheetEditor && spritesheetEditor.is_focused_get()); } } else diff --git a/src/imgui/dockspace.hpp b/src/imgui/dockspace.hpp index eb1bbf1..bab15a5 100644 --- a/src/imgui/dockspace.hpp +++ b/src/imgui/dockspace.hpp @@ -1,44 +1,40 @@ #pragma once -#include "autosave_restore.hpp" +#include "popup/autosave_restore.hpp" #include "documents.hpp" #include "taskbar.hpp" #include "window/animation_preview.hpp" -#include "window/animations.hpp" -#include "window/regions.hpp" -#include "window/events.hpp" #include "window/frame_properties.hpp" -#include "window/layers.hpp" -#include "window/nulls.hpp" #include "window/onionskin.hpp" -#include "window/sounds.hpp" #include "window/spritesheet_editor.hpp" -#include "window/spritesheets.hpp" #include "window/timeline.hpp" #include "window/tools.hpp" #include "window/welcome.hpp" +#include "window/window.hpp" namespace anm2ed::imgui { class Dockspace { AnimationPreview animationPreview; - Animations animations; - Regions regions; - Events events; + bool isCanvasFocused{}; + Window animations{animations_window_register()}; + Window regions{regions_window_register()}; + Window events{events_window_register()}; FrameProperties frameProperties; - Layers layers; - Nulls nulls; + Window layers{layers_window_register()}; + Window nulls{nulls_window_register()}; Onionskin onionskin; SpritesheetEditor spritesheetEditor; - Spritesheets spritesheets; - Sounds sounds; + Window spritesheets{spritesheets_window_register()}; + Window sounds{sounds_window_register()}; Timeline timeline; Tools tools; Welcome welcome; AutosaveRestore autosaveRestore; public: + bool is_canvas_focused_get() const; void tick(Manager&, Settings&, float); void update(Taskbar&, Documents&, Manager&, Settings&, Resources&, Dialog&, Clipboard&); }; diff --git a/src/imgui/documents.cpp b/src/imgui/documents.cpp index 66a7bbb..4494a42 100644 --- a/src/imgui/documents.cpp +++ b/src/imgui/documents.cpp @@ -3,11 +3,11 @@ #include #include -#include "path_.hpp" -#include "strings.hpp" -#include "time_.hpp" -#include "toast.hpp" #include "log.hpp" +#include "path.hpp" +#include "strings.hpp" +#include "time.hpp" +#include "toast.hpp" using namespace anm2ed::resource; using namespace anm2ed::types; @@ -31,16 +31,24 @@ namespace anm2ed::imgui pushedStyle = true; } - for (auto& document : manager.documents) + for (auto i = 0; i < (int)manager.documents.size(); ++i) { + auto& document = manager.documents[i]; auto isDirty = document.is_dirty() && document.is_autosave_dirty(); if (isDirty) { document.lastAutosaveTime += ImGui::GetIO().DeltaTime; if (document.lastAutosaveTime > time::SECOND_M) - manager.autosave(document, (anm2::Compatibility)settings.fileCompatibility, - settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale, - settings.bakeIsRoundRotation); + { + auto compatibility = (Compatibility)settings.fileCompatibility; + auto bakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave; + auto isRoundScale = settings.bakeIsRoundScale; + auto isRoundRotation = settings.bakeIsRoundRotation; + manager.command_push({i, + [compatibility, bakeFrames, isRoundScale, isRoundRotation](Manager& manager, + Document& document) + { manager.autosave(document, compatibility, bakeFrames, isRoundScale, isRoundRotation); }}); + } } } @@ -95,8 +103,8 @@ namespace anm2ed::imgui auto isRequested = i == manager.pendingSelected; auto font = isDocumentDirty ? font::ITALICS : font::REGULAR; auto filename = path::to_utf8(document.filename_get()); - auto string = - isDocumentDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) : filename; + auto string = isDocumentDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) + : filename; auto label = std::format("{}###Document{}", string, i); auto flags = isDocumentDirty ? ImGuiTabItemFlags_UnsavedDocument : 0; @@ -120,7 +128,8 @@ namespace anm2ed::imgui for (auto it = closeIndices.rbegin(); it != closeIndices.rend(); ++it) { if (closePopup.is_open() && closeDocumentIndex > *it) --closeDocumentIndex; - manager.close(*it); + auto index = *it; + manager.command_push({.runManager = [index](Manager& manager) { manager.close(index); }}); } ImGui::EndTabBar(); @@ -137,10 +146,10 @@ namespace anm2ed::imgui auto filename = path::to_utf8(closeDocument.filename_get()); auto isDocumentDirty = closeDocument.is_dirty() || closeDocument.isForceDirty; auto isSpritesheetDirty = closeDocument.spritesheet_any_dirty(); - auto promptLabel = isDocumentDirty && isSpritesheetDirty - ? LABEL_DOCUMENT_AND_SPRITESHEETS_MODIFIED_PROMPT - : (isDocumentDirty ? LABEL_DOCUMENT_MODIFIED_PROMPT - : LABEL_SPRITESHEETS_MODIFIED_PROMPT); + auto promptLabel = + isDocumentDirty && isSpritesheetDirty + ? LABEL_DOCUMENT_AND_SPRITESHEETS_MODIFIED_PROMPT + : (isDocumentDirty ? LABEL_DOCUMENT_MODIFIED_PROMPT : LABEL_SPRITESHEETS_MODIFIED_PROMPT); auto prompt = std::vformat(localize.get(promptLabel), std::make_format_args(filename)); ImGui::TextUnformatted(prompt.c_str()); @@ -156,8 +165,7 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(BASIC_YES), widgetSize)) { bool isSaved = true; - if (isDocumentDirty) - isSaved = taskbar.save_manual(manager, settings, closeDocumentIndex); + if (isDocumentDirty) isSaved = taskbar.save_manual(manager, settings, closeDocumentIndex); if (!isSaved) { @@ -167,27 +175,38 @@ namespace anm2ed::imgui if (isSpritesheetDirty) { - for (auto& [id, spritesheet] : closeDocument.anm2.content.spritesheets) + auto spritesheets = closeDocument.anm2.element_get(ElementType::SPRITESHEETS); + if (spritesheets) { - if (!closeDocument.spritesheet_is_dirty(id)) continue; - auto pathString = path::to_utf8(spritesheet.path); - if (spritesheet.save(closeDocument.directory_get())) + for (auto& spritesheet : spritesheets->children) { - closeDocument.spritesheet_hash_set_saved(id); - toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString))); - logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - } - else - { - toasts.push( - std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString))); - logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); + if (spritesheet.type != ElementType::SPRITESHEET) continue; + auto id = spritesheet.id; + auto texture = closeDocument.texture_get(id); + if (!texture || !closeDocument.spritesheet_is_dirty(id)) continue; + auto pathString = path::to_utf8(spritesheet.path); + auto savePath = closeDocument.directory_get() / spritesheet.path; + path::ensure_directory(savePath.parent_path()); + if (texture->write_png(savePath)) + { + closeDocument.spritesheet_hash_set_saved(id); + toasts.push( + std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString))); + logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + } + else + { + toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), + std::make_format_args(id, pathString))); + logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + } } } } - manager.close(closeDocumentIndex); + auto index = closeDocumentIndex; + manager.command_push({.runManager = [index](Manager& manager) { manager.close(index); }}); close(); } @@ -195,7 +214,8 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(BASIC_NO), widgetSize)) { - manager.close(closeDocumentIndex); + auto index = closeDocumentIndex; + manager.command_push({.runManager = [index](Manager& manager) { manager.close(index); }}); close(); } @@ -244,8 +264,12 @@ namespace anm2ed::imgui if (ImGui::MenuItem(manager.anm2DragDropPaths.size() > 1 ? localize.get(LABEL_DOCUMENTS_OPEN_MANY) : localize.get(LABEL_DOCUMENTS_OPEN_NEW))) { - for (auto& path : manager.anm2DragDropPaths) - manager.open(path); + auto paths = manager.anm2DragDropPaths; + manager.command_push({.runManager = + [paths](Manager& manager) + { + for (auto& path : paths) manager.open(path); + }}); drag_drop_reset(); } @@ -254,16 +278,14 @@ namespace anm2ed::imgui { if (document) { - auto merge_anm2s = [&]() - { - for (auto& path : manager.anm2DragDropPaths) - { - anm2::Anm2 source(path); - document->anm2.merge(source, document->directory_get(), path.parent_path()); - } - }; - - DOCUMENT_EDIT_PTR(document, localize.get(EDIT_MERGE_ANM2), Document::ALL, merge_anm2s()); + auto paths = manager.anm2DragDropPaths; + manager.command_push({manager.selected, + [paths](Manager&, Document& document) + { + document.snapshot(localize.get(EDIT_MERGE_ANM2)); + for (auto& path : paths) document.file_merge(path); + document.change(Document::ALL); + }}); drag_drop_reset(); } } diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp deleted file mode 100644 index 66334af..0000000 --- a/src/imgui/imgui_.cpp +++ /dev/null @@ -1,695 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include "imgui_.hpp" -#include "path_.hpp" -#include "strings.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::util; -using namespace glm; - -namespace anm2ed::imgui -{ - static auto isRenaming = false; - - namespace - { - const std::vector> CANONICAL_KEY_NAMES = { - {ImGuiKey_A, "A"}, - {ImGuiKey_B, "B"}, - {ImGuiKey_C, "C"}, - {ImGuiKey_D, "D"}, - {ImGuiKey_E, "E"}, - {ImGuiKey_F, "F"}, - {ImGuiKey_G, "G"}, - {ImGuiKey_H, "H"}, - {ImGuiKey_I, "I"}, - {ImGuiKey_J, "J"}, - {ImGuiKey_K, "K"}, - {ImGuiKey_L, "L"}, - {ImGuiKey_M, "M"}, - {ImGuiKey_N, "N"}, - {ImGuiKey_O, "O"}, - {ImGuiKey_P, "P"}, - {ImGuiKey_Q, "Q"}, - {ImGuiKey_R, "R"}, - {ImGuiKey_S, "S"}, - {ImGuiKey_T, "T"}, - {ImGuiKey_U, "U"}, - {ImGuiKey_V, "V"}, - {ImGuiKey_W, "W"}, - {ImGuiKey_X, "X"}, - {ImGuiKey_Y, "Y"}, - {ImGuiKey_Z, "Z"}, - {ImGuiKey_0, "0"}, - {ImGuiKey_1, "1"}, - {ImGuiKey_2, "2"}, - {ImGuiKey_3, "3"}, - {ImGuiKey_4, "4"}, - {ImGuiKey_5, "5"}, - {ImGuiKey_6, "6"}, - {ImGuiKey_7, "7"}, - {ImGuiKey_8, "8"}, - {ImGuiKey_9, "9"}, - {ImGuiKey_Keypad0, "Num0"}, - {ImGuiKey_Keypad1, "Num1"}, - {ImGuiKey_Keypad2, "Num2"}, - {ImGuiKey_Keypad3, "Num3"}, - {ImGuiKey_Keypad4, "Num4"}, - {ImGuiKey_Keypad5, "Num5"}, - {ImGuiKey_Keypad6, "Num6"}, - {ImGuiKey_Keypad7, "Num7"}, - {ImGuiKey_Keypad8, "Num8"}, - {ImGuiKey_Keypad9, "Num9"}, - {ImGuiKey_KeypadAdd, "NumAdd"}, - {ImGuiKey_KeypadSubtract, "NumSubtract"}, - {ImGuiKey_KeypadMultiply, "NumMultiply"}, - {ImGuiKey_KeypadDivide, "NumDivide"}, - {ImGuiKey_KeypadEnter, "NumEnter"}, - {ImGuiKey_KeypadDecimal, "NumDecimal"}, - {ImGuiKey_KeypadEqual, "NumEqual"}, - {ImGuiKey_F1, "F1"}, - {ImGuiKey_F2, "F2"}, - {ImGuiKey_F3, "F3"}, - {ImGuiKey_F4, "F4"}, - {ImGuiKey_F5, "F5"}, - {ImGuiKey_F6, "F6"}, - {ImGuiKey_F7, "F7"}, - {ImGuiKey_F8, "F8"}, - {ImGuiKey_F9, "F9"}, - {ImGuiKey_F10, "F10"}, - {ImGuiKey_F11, "F11"}, - {ImGuiKey_F12, "F12"}, - {ImGuiKey_UpArrow, "Up"}, - {ImGuiKey_DownArrow, "Down"}, - {ImGuiKey_LeftArrow, "Left"}, - {ImGuiKey_RightArrow, "Right"}, - {ImGuiKey_Space, "Space"}, - {ImGuiKey_Enter, "Enter"}, - {ImGuiKey_Escape, "Escape"}, - {ImGuiKey_Tab, "Tab"}, - {ImGuiKey_Backspace, "Backspace"}, - {ImGuiKey_Delete, "Delete"}, - {ImGuiKey_Insert, "Insert"}, - {ImGuiKey_Home, "Home"}, - {ImGuiKey_End, "End"}, - {ImGuiKey_PageUp, "PageUp"}, - {ImGuiKey_PageDown, "PageDown"}, - {ImGuiKey_Minus, "Minus"}, - {ImGuiKey_Equal, "Equal"}, - {ImGuiKey_LeftBracket, "LeftBracket"}, - {ImGuiKey_RightBracket, "RightBracket"}, - {ImGuiKey_Semicolon, "Semicolon"}, - {ImGuiKey_Apostrophe, "Apostrophe"}, - {ImGuiKey_Comma, "Comma"}, - {ImGuiKey_Period, "Period"}, - {ImGuiKey_Slash, "Slash"}, - {ImGuiKey_Backslash, "Backslash"}, - {ImGuiKey_GraveAccent, "GraveAccent"}, - }; - - const char* canonical_key_name(ImGuiKey key) - { - for (const auto& [mappedKey, name] : CANONICAL_KEY_NAMES) - if (mappedKey == key) return name; - return nullptr; - } - } - - constexpr ImVec4 COLOR_LIGHT_BUTTON{0.98f, 0.98f, 0.98f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TITLE_BG{0.78f, 0.78f, 0.78f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TITLE_BG_ACTIVE{0.64f, 0.64f, 0.64f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TITLE_BG_COLLAPSED{0.74f, 0.74f, 0.74f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TABLE_HEADER{0.78f, 0.78f, 0.78f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TAB{0.74f, 0.74f, 0.74f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TAB_HOVERED{0.82f, 0.82f, 0.82f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TAB_SELECTED{0.92f, 0.92f, 0.92f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED{0.70f, 0.70f, 0.70f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_SELECTED{0.86f, 0.86f, 0.86f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TAB_OVERLINE{0.55f, 0.55f, 0.55f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_OVERLINE{0.50f, 0.50f, 0.50f, 1.0f}; - constexpr ImVec4 COLOR_LIGHT_CHECK_MARK{0.0f, 0.0f, 0.0f, 1.0f}; - constexpr auto FRAME_BORDER_SIZE = 1.0f; - - void theme_set(theme::Type theme) - { - switch (theme) - { - case theme::LIGHT: - ImGui::StyleColorsLight(); - break; - case theme::DARK: - default: - ImGui::StyleColorsDark(); - break; - case theme::CLASSIC: - ImGui::StyleColorsClassic(); - break; - } - auto& style = ImGui::GetStyle(); - style.FrameBorderSize = FRAME_BORDER_SIZE; - - if (theme == theme::LIGHT) - { - auto& colors = style.Colors; - colors[ImGuiCol_Button] = COLOR_LIGHT_BUTTON; - colors[ImGuiCol_TitleBg] = COLOR_LIGHT_TITLE_BG; - colors[ImGuiCol_TitleBgActive] = COLOR_LIGHT_TITLE_BG_ACTIVE; - colors[ImGuiCol_TitleBgCollapsed] = COLOR_LIGHT_TITLE_BG_COLLAPSED; - colors[ImGuiCol_TableHeaderBg] = COLOR_LIGHT_TABLE_HEADER; - colors[ImGuiCol_Tab] = COLOR_LIGHT_TAB; - colors[ImGuiCol_TabHovered] = COLOR_LIGHT_TAB_HOVERED; - colors[ImGuiCol_TabSelected] = COLOR_LIGHT_TAB_SELECTED; - colors[ImGuiCol_TabSelectedOverline] = COLOR_LIGHT_TAB_OVERLINE; - colors[ImGuiCol_TabDimmed] = COLOR_LIGHT_TAB_DIMMED; - colors[ImGuiCol_TabDimmedSelected] = COLOR_LIGHT_TAB_DIMMED_SELECTED; - colors[ImGuiCol_TabDimmedSelectedOverline] = COLOR_LIGHT_TAB_DIMMED_OVERLINE; - colors[ImGuiCol_CheckMark] = COLOR_LIGHT_CHECK_MARK; - } - } - - 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 input_text_path(const char* label, std::filesystem::path* path, ImGuiInputTextFlags flags) - { - if (!path) return false; - - auto pathUtf8 = path::to_utf8(*path); - auto edited = input_text_string(label, &pathUtf8, flags); - if (edited) *path = path::from_utf8(pathUtf8); - - return edited; - } - - bool combo_negative_one_indexed(const std::string& label, int* index, std::vector& strings) - { - *index += 1; - bool isActivated = ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size()); - *index -= 1; - - return isActivated; - } - - bool combo_id_mapped(const std::string& label, int* id, const std::vector& ids, std::vector& labels) - { - if (!id) return false; - - int index = -1; - if (!ids.empty()) - { - auto it = std::find(ids.begin(), ids.end(), *id); - if (it != ids.end()) index = (int)std::distance(ids.begin(), it); - } - - bool isActivated = ImGui::Combo(label.c_str(), &index, labels.data(), (int)labels.size()); - if (isActivated) - { - if (index >= 0 && index < (int)ids.size()) - *id = ids[index]; - else - *id = -1; - } - - return isActivated; - } - - edit::Type drag_int_persistent(const char* label, int* value, float speed, int min, int max, const char* format, - ImGuiSliderFlags flags) - { - static bool isEditing{}; - static int start{INT_MAX}; - auto persistent = value ? *value : 0; - - ImGui::DragInt(label, &persistent, speed, min, max, format, flags); - if (!value) return edit::NONE; - if (ImGui::IsItemActivated() && persistent != start) - { - isEditing = true; - start = *value; - return edit::START; - } - else if (ImGui::IsItemDeactivatedAfterEdit()) - { - isEditing = false; - *value = persistent; - start = INT_MAX; - return edit::END; - } - else if (isEditing) - { - *value = persistent; - return edit::DURING; - } - - return edit::NONE; - } - - edit::Type drag_float_persistent(const char* label, float* value, float speed, float min, float max, - const char* format, ImGuiSliderFlags flags) - { - static bool isEditing{}; - static float start{NAN}; - auto persistent = value ? *value : 0; - - ImGui::DragFloat(label, &persistent, speed, min, max, format, flags); - if (!value) return edit::NONE; - if (ImGui::IsItemActivated() && persistent != start) - { - isEditing = true; - start = *value; - return edit::START; - } - else if (ImGui::IsItemDeactivatedAfterEdit()) - { - isEditing = false; - *value = persistent; - start = NAN; - return edit::END; - } - else if (isEditing) - { - *value = persistent; - return edit::DURING; - } - - return edit::NONE; - } - - edit::Type drag_float2_persistent(const char* label, vec2* value, float speed, float min, float max, - const char* format, ImGuiSliderFlags flags) - { - static bool isEditing{}; - static vec2 start{NAN}; - auto persistent = value ? *value : vec2(); - - ImGui::DragFloat2(label, value_ptr(persistent), speed, min, max, format, flags); - if (!value) return edit::NONE; - if (ImGui::IsItemActivated() && persistent != start) - { - isEditing = true; - start = *value; - return edit::START; - } - else if (ImGui::IsItemDeactivatedAfterEdit()) - { - isEditing = false; - *value = persistent; - start = vec2{NAN}; - return edit::END; - } - else if (isEditing) - { - *value = persistent; - return edit::DURING; - } - - return edit::NONE; - } - - edit::Type color_edit3_persistent(const char* label, vec3* value, ImGuiColorEditFlags flags) - { - static bool isEditing{}; - static vec3 start{NAN}; - auto persistent = value ? *value : vec4(); - - ImGui::ColorEdit3(label, value_ptr(persistent), flags); - if (!value) return edit::NONE; - if (ImGui::IsItemActivated() && persistent != start) - { - isEditing = true; - start = *value; - return edit::START; - } - else if (ImGui::IsItemDeactivatedAfterEdit()) - { - isEditing = false; - *value = persistent; - start = vec4{NAN}; - return edit::END; - } - else if (isEditing) - { - *value = persistent; - return edit::DURING; - } - - return edit::NONE; - } - - edit::Type color_edit4_persistent(const char* label, vec4* value, ImGuiColorEditFlags flags) - { - static bool isEditing{}; - static vec4 start{NAN}; - auto persistent = value ? *value : vec4(); - - ImGui::ColorEdit4(label, value_ptr(persistent), flags); - if (!value) return edit::NONE; - if (ImGui::IsItemActivated() && persistent != start) - { - isEditing = true; - start = *value; - return edit::START; - } - else if (ImGui::IsItemDeactivatedAfterEdit()) - { - isEditing = false; - *value = persistent; - start = vec4{NAN}; - return edit::END; - } - else if (isEditing) - { - *value = persistent; - return edit::DURING; - } - - return edit::NONE; - } - - 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 input_int2_range(const char* label, ivec2& value, ivec2 min, ivec2 max, ImGuiInputTextFlags flags) - { - auto isActivated = ImGui::InputInt2(label, value_ptr(value), flags); - value = glm::clamp(value, min, max); - return isActivated; - } - - bool input_float_range(const char* label, float& value, float min, float max, float step, float stepFast, - const char* format, ImGuiInputTextFlags flags) - { - auto isActivated = ImGui::InputFloat(label, &value, step, stepFast, format, flags); - value = glm::clamp(value, min, max); - return isActivated; - } - - std::string& selectable_input_text_id() - { - static std::string editID{}; - return editID; - } - - bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected, - ImGuiSelectableFlags flags, RenameState& state) - { - auto& editID = selectable_input_text_id(); - auto isRename = editID == id; - bool isActivated{}; - - if (isRename) - { - auto finish = [&]() - { - editID.clear(); - isActivated = true; - state = RENAME_FINISHED; - isRenaming = false; - }; - - if (state == RENAME_BEGIN) - { - ImGui::SetKeyboardFocusHere(); - state = RENAME_EDITING; - } - - ImGui::SetNextItemWidth(-FLT_MIN); - if (input_text_string("##Edit", &text, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) - finish(); - if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) finish(); - } - else - { - if (ImGui::Selectable(label.c_str(), isSelected, flags)) isActivated = true; - - if (state == RENAME_FORCE_EDIT || (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))) - { - state = RENAME_BEGIN; - editID = id; - isActivated = true; - isRenaming = true; - } - } - - return isActivated; - } - - void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut) - { - ImGui::SetItemTooltip( - "%s", std::vformat(localize.get(FORMAT_TOOLTIP_SHORTCUT), std::make_format_args(tooltip, shortcut)).c_str()); - } - - namespace - { - struct CheckerStart - { - float position{}; - long long index{}; - }; - - CheckerStart checker_start(float minCoord, float offset, float step) - { - float world = minCoord + offset; - long long idx = static_cast(std::floor(world / step)); - float first = minCoord - (world - static_cast(idx) * step); - return {first, idx}; - } - } - - void render_checker_background(ImDrawList* drawList, ImVec2 min, ImVec2 max, vec2 offset, float step) - { - if (!drawList || step <= 0.0f) return; - - const ImU32 colorLight = IM_COL32(204, 204, 204, 255); - const ImU32 colorDark = IM_COL32(128, 128, 128, 255); - - auto [startY, rowIndex] = checker_start(min.y, offset.y, step); - for (float y = startY; y < max.y; y += step, ++rowIndex) - { - float y1 = glm::max(y, min.y); - float y2 = glm::min(y + step, max.y); - if (y2 <= y1) continue; - - auto [startX, columnIndex] = checker_start(min.x, offset.x, step); - for (float x = startX; x < max.x; x += step, ++columnIndex) - { - float x1 = glm::max(x, min.x); - float x2 = glm::min(x + step, max.x); - if (x2 <= x1) continue; - - bool isDark = ((rowIndex + columnIndex) & 1LL) != 0; - drawList->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), isDark ? colorDark : colorLight); - } - } - } - - void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected) - { - auto* storage = static_cast(self->UserData); - auto value = storage ? storage->resolve_index(id) : id; - if (isSelected) - storage->insert(value); - else - storage->erase(value); - }; - - std::string chord_to_string(ImGuiKeyChord chord) - { - std::string result; - - if (chord & ImGuiMod_Ctrl) result += "Ctrl+"; - if (chord & ImGuiMod_Shift) result += "Shift+"; - if (chord & ImGuiMod_Alt) result += "Alt+"; - if (chord & ImGuiMod_Super) result += "Super+"; - - if (auto key = (ImGuiKey)(chord & ~ImGuiMod_Mask_); key != ImGuiKey_None) - { - if (const char* name = canonical_key_name(key); name && *name) - result += name; - else - result += ImGui::GetKeyName(key); - } - - if (!result.empty() && result.back() == '+') result.pop_back(); - - return result; - } - - ImGuiKeyChord string_to_chord(const std::string& string) - { - ImGuiKeyChord chord = 0; - ImGuiKey baseKey = ImGuiKey_None; - - std::stringstream ss(string); - std::string token; - while (std::getline(ss, token, '+')) - { - token.erase(0, token.find_first_not_of(" \t\r\n")); - token.erase(token.find_last_not_of(" \t\r\n") + 1); - - if (token.empty()) continue; - - if (auto it = MOD_MAP.find(token); it != MOD_MAP.end()) - chord |= it->second; - else if (baseKey == ImGuiKey_None) - if (auto it2 = KEY_MAP.find(token); it2 != KEY_MAP.end()) baseKey = it2->second; - } - - if (baseKey != ImGuiKey_None) chord |= baseKey; - - return chord; - } - - float row_widget_width_get(int count, float width) - { - return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count; - } - - ImVec2 widget_size_with_row_get(int count, float width) { return ImVec2(row_widget_width_get(count, width), 0); } - - float footer_height_get(int itemCount) - { - return ImGui::GetTextLineHeightWithSpacing() * itemCount + ImGui::GetStyle().WindowPadding.y + - ImGui::GetStyle().ItemSpacing.y * (itemCount); - } - - ImVec2 footer_size_get(int itemCount) - { - return ImVec2(ImGui::GetContentRegionAvail().x, footer_height_get(itemCount)); - } - - ImVec2 size_without_footer_get(int rowCount) - { - return ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - footer_height_get(rowCount)); - } - - ImVec2 child_size_get(int rowCount) - { - return ImVec2(ImGui::GetContentRegionAvail().x, - (ImGui::GetFrameHeightWithSpacing() * rowCount) + (ImGui::GetStyle().WindowPadding.y * 2.0f)); - } - - ImVec2 icon_size_get() - { - return ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing()); - } - - bool shortcut(ImGuiKeyChord chord, shortcut::Type type) - { - if (chord == ImGuiKey_None) return false; - - if (ImGui::GetTopMostPopupModal() != nullptr && - (type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET)) - return false; - - int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal - : ImGuiInputFlags_RouteFocused; - flags |= ImGuiInputFlags_Repeat; - - if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET) - { - ImGui::SetNextItemShortcut(chord, flags); - return false; - } - - return ImGui::Shortcut(chord, flags); - } - - MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; } - - void MultiSelectStorage::start(size_t size, ImGuiMultiSelectFlags flags) - { - internal.UserData = this; - - io = ImGui::BeginMultiSelect(flags, this->size(), size); - apply(); - } - - void MultiSelectStorage::apply() { internal.ApplyRequests(io); } - - void MultiSelectStorage::finish() - { - io = ImGui::EndMultiSelect(); - apply(); - } - - void MultiSelectStorage::set_index_map(std::vector* map) { indexMap = map; } - - int MultiSelectStorage::resolve_index(int index) const - { - if (!indexMap) return index; - if (index < 0 || index >= (int)indexMap->size()) return index; - return (*indexMap)[index]; - } - - PopupHelper::PopupHelper(StringType labelId, PopupType type, PopupPosition position) - { - this->labelId = labelId; - this->type = type; - this->position = position; - } - - void PopupHelper::open() - { - isOpen = true; - isTriggered = true; - isJustOpened = true; - } - - bool PopupHelper::is_open() { return isOpen; } - - void PopupHelper::trigger() - { - if (isTriggered) ImGui::OpenPopup(localize.get(labelId)); - isTriggered = false; - - auto viewport = ImGui::GetMainViewport(); - - switch (position) - { - case POPUP_CENTER: - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f))); - 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)); - break; - case POPUP_BY_ITEM: - ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None); - case POPUP_BY_CURSOR: - default: - break; - } - } - - void PopupHelper::end() { isJustOpened = false; } - - void PopupHelper::close() { isOpen = false; } -} diff --git a/src/imgui/imgui_.hpp b/src/imgui/imgui_.hpp deleted file mode 100644 index c53d097..0000000 --- a/src/imgui/imgui_.hpp +++ /dev/null @@ -1,271 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "strings.hpp" -#include "types.hpp" - -namespace anm2ed::imgui -{ - constexpr auto DRAG_SPEED = 0.25f; - constexpr auto DRAG_SPEED_FAST = 1.00f; - 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, - POPUP_BY_CURSOR - }; - - enum RenameState - { - RENAME_SELECTABLE, - RENAME_BEGIN, - RENAME_EDITING, - RENAME_FINISHED, - RENAME_FORCE_EDIT - }; - - 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}, - {"C", ImGuiKey_C}, - {"D", ImGuiKey_D}, - {"E", ImGuiKey_E}, - {"F", ImGuiKey_F}, - {"G", ImGuiKey_G}, - {"H", ImGuiKey_H}, - {"I", ImGuiKey_I}, - {"J", ImGuiKey_J}, - {"K", ImGuiKey_K}, - {"L", ImGuiKey_L}, - {"M", ImGuiKey_M}, - {"N", ImGuiKey_N}, - {"O", ImGuiKey_O}, - {"P", ImGuiKey_P}, - {"Q", ImGuiKey_Q}, - {"R", ImGuiKey_R}, - {"S", ImGuiKey_S}, - {"T", ImGuiKey_T}, - {"U", ImGuiKey_U}, - {"V", ImGuiKey_V}, - {"W", ImGuiKey_W}, - {"X", ImGuiKey_X}, - {"Y", ImGuiKey_Y}, - {"Z", ImGuiKey_Z}, - - {"0", ImGuiKey_0}, - {"1", ImGuiKey_1}, - {"2", ImGuiKey_2}, - {"3", ImGuiKey_3}, - {"4", ImGuiKey_4}, - {"5", ImGuiKey_5}, - {"6", ImGuiKey_6}, - {"7", ImGuiKey_7}, - {"8", ImGuiKey_8}, - {"9", ImGuiKey_9}, - - {"Num0", ImGuiKey_Keypad0}, - {"Num1", ImGuiKey_Keypad1}, - {"Num2", ImGuiKey_Keypad2}, - {"Num3", ImGuiKey_Keypad3}, - {"Num4", ImGuiKey_Keypad4}, - {"Num5", ImGuiKey_Keypad5}, - {"Num6", ImGuiKey_Keypad6}, - {"Num7", ImGuiKey_Keypad7}, - {"Num8", ImGuiKey_Keypad8}, - {"Num9", ImGuiKey_Keypad9}, - {"NumAdd", ImGuiKey_KeypadAdd}, - {"NumSubtract", ImGuiKey_KeypadSubtract}, - {"NumMultiply", ImGuiKey_KeypadMultiply}, - {"NumDivide", ImGuiKey_KeypadDivide}, - {"NumEnter", ImGuiKey_KeypadEnter}, - {"NumDecimal", ImGuiKey_KeypadDecimal}, - {"NumEqual", ImGuiKey_KeypadEqual}, - - {"F1", ImGuiKey_F1}, - {"F2", ImGuiKey_F2}, - {"F3", ImGuiKey_F3}, - {"F4", ImGuiKey_F4}, - {"F5", ImGuiKey_F5}, - {"F6", ImGuiKey_F6}, - {"F7", ImGuiKey_F7}, - {"F8", ImGuiKey_F8}, - {"F9", ImGuiKey_F9}, - {"F10", ImGuiKey_F10}, - {"F11", ImGuiKey_F11}, - {"F12", ImGuiKey_F12}, - - {"Up", ImGuiKey_UpArrow}, - {"Down", ImGuiKey_DownArrow}, - {"Left", ImGuiKey_LeftArrow}, - {"Right", ImGuiKey_RightArrow}, - - {"Space", ImGuiKey_Space}, - {"Enter", ImGuiKey_Enter}, - {"Escape", ImGuiKey_Escape}, - {"Tab", ImGuiKey_Tab}, - {"Backspace", ImGuiKey_Backspace}, - {"Delete", ImGuiKey_Delete}, - {"Insert", ImGuiKey_Insert}, - {"Home", ImGuiKey_Home}, - {"End", ImGuiKey_End}, - {"PageUp", ImGuiKey_PageUp}, - {"PageDown", ImGuiKey_PageDown}, - - {"Minus", ImGuiKey_Minus}, - {"Equal", ImGuiKey_Equal}, - {"LeftBracket", ImGuiKey_LeftBracket}, - {"RightBracket", ImGuiKey_RightBracket}, - {"Semicolon", ImGuiKey_Semicolon}, - {"Apostrophe", ImGuiKey_Apostrophe}, - {"Comma", ImGuiKey_Comma}, - {"Period", ImGuiKey_Period}, - {"Slash", ImGuiKey_Slash}, - {"Backslash", ImGuiKey_Backslash}, - {"GraveAccent", ImGuiKey_GraveAccent}, - - // Legacy aliases for older saved shortcut strings. - {"Keypad0", ImGuiKey_Keypad0}, - {"Keypad1", ImGuiKey_Keypad1}, - {"Keypad2", ImGuiKey_Keypad2}, - {"Keypad3", ImGuiKey_Keypad3}, - {"Keypad4", ImGuiKey_Keypad4}, - {"Keypad5", ImGuiKey_Keypad5}, - {"Keypad6", ImGuiKey_Keypad6}, - {"Keypad7", ImGuiKey_Keypad7}, - {"Keypad8", ImGuiKey_Keypad8}, - {"Keypad9", ImGuiKey_Keypad9}, - {"KeypadAdd", ImGuiKey_KeypadAdd}, - {"KeypadSubtract", ImGuiKey_KeypadSubtract}, - {"KeypadMultiply", ImGuiKey_KeypadMultiply}, - {"KeypadDivide", ImGuiKey_KeypadDivide}, - {"KeypadEnter", ImGuiKey_KeypadEnter}, - {"KeypadDecimal", ImGuiKey_KeypadDecimal}, - {"KeypadEqual", ImGuiKey_KeypadEqual}, - {"UpArrow", ImGuiKey_UpArrow}, - {"DownArrow", ImGuiKey_DownArrow}, - {"LeftArrow", ImGuiKey_LeftArrow}, - {"RightArrow", ImGuiKey_RightArrow}, - }; - - const std::unordered_map MOD_MAP = { - {"Ctrl", ImGuiMod_Ctrl}, - {"Shift", ImGuiMod_Shift}, - {"Alt", ImGuiMod_Alt}, - {"Super", ImGuiMod_Super}, - }; - - void theme_set(types::theme::Type theme); - std::string chord_to_string(ImGuiKeyChord); - ImGuiKeyChord string_to_chord(const std::string&); - float row_widget_width_get(int, float = ImGui::GetContentRegionAvail().x); - ImVec2 widget_size_with_row_get(int, float = ImGui::GetContentRegionAvail().x); - float footer_height_get(int = 1); - ImVec2 footer_size_get(int = 1); - ImVec2 size_without_footer_get(int = 1); - ImVec2 child_size_get(int = 1); - int input_text_callback(ImGuiInputTextCallbackData*); - bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0); - bool input_text_path(const char*, std::filesystem::path*, ImGuiInputTextFlags = 0); - bool input_int_range(const char*, int&, int, int, int = STEP, int = STEP_FAST, ImGuiInputTextFlags = 0); - bool input_int2_range(const char*, glm::ivec2&, glm::ivec2, glm::ivec2, ImGuiInputTextFlags = 0); - bool input_float_range(const char*, float&, float, float, float = STEP, float = STEP_FAST, const char* = "%.3f", - ImGuiInputTextFlags = 0); - types::edit::Type drag_int_persistent(const char*, int*, float = DRAG_SPEED, int = {}, int = {}, const char* = "%d", - ImGuiSliderFlags = 0); - types::edit::Type drag_float_persistent(const char*, float*, float = DRAG_SPEED, float = {}, float = {}, - const char* = "%.3f", ImGuiSliderFlags = 0); - types::edit::Type drag_float2_persistent(const char*, glm::vec2*, float = DRAG_SPEED, float = {}, float = {}, - const char* = "%.3f", ImGuiSliderFlags = 0); - types::edit::Type color_edit3_persistent(const char*, glm::vec3*, ImGuiColorEditFlags = 0); - types::edit::Type color_edit4_persistent(const char*, glm::vec4*, ImGuiColorEditFlags = 0); - bool combo_negative_one_indexed(const std::string&, int*, std::vector&); - bool combo_id_mapped(const std::string&, int*, const std::vector&, std::vector&); - std::string& selectable_input_text_id(); - bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected, - ImGuiSelectableFlags flags, RenameState& state); - void set_item_tooltip_shortcut(const char*, const std::string& = {}); - void external_storage_set(ImGuiSelectionExternalStorage*, int, bool); - void render_checker_background(ImDrawList*, ImVec2, ImVec2, glm::vec2, float); - ImVec2 icon_size_get(); - bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET); - - class MultiSelectStorage : public std::set - { - public: - ImGuiSelectionExternalStorage internal{}; - ImGuiMultiSelectIO* io{}; - std::vector* indexMap{}; - - using std::set::set; - using std::set::operator=; - using std::set::begin; - using std::set::rbegin; - using std::set::end; - using std::set::size; - using std::set::insert; - using std::set::erase; - - MultiSelectStorage(); - void start(size_t, - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ScopeWindow); - void apply(); - void finish(); - void set_index_map(std::vector*); - int resolve_index(int) const; - }; - - class PopupHelper - { - public: - StringType labelId{}; - PopupType type{}; - PopupPosition position{}; - bool isOpen{}; - bool isTriggered{}; - bool isJustOpened{}; - - PopupHelper(StringType, PopupType = POPUP_NORMAL, PopupPosition = POPUP_CENTER); - const char* label() const { return localize.get(labelId); } - bool is_open(); - void open(); - void trigger(); - void end(); - void close(); - }; -} diff --git a/src/imgui/autosave_restore.cpp b/src/imgui/popup/autosave_restore.cpp similarity index 96% rename from src/imgui/autosave_restore.cpp rename to src/imgui/popup/autosave_restore.cpp index 972da3e..0c44a08 100644 --- a/src/imgui/autosave_restore.cpp +++ b/src/imgui/popup/autosave_restore.cpp @@ -2,7 +2,8 @@ #include -#include "path_.hpp" +#include "path.hpp" +#include "util/imgui/imgui.hpp" using namespace anm2ed::resource; using namespace anm2ed::util; diff --git a/src/imgui/autosave_restore.hpp b/src/imgui/popup/autosave_restore.hpp similarity index 100% rename from src/imgui/autosave_restore.hpp rename to src/imgui/popup/autosave_restore.hpp diff --git a/src/imgui/popup/item_properties.cpp b/src/imgui/popup/item_properties.cpp index 6b7b74a..aa9575f 100644 --- a/src/imgui/popup/item_properties.cpp +++ b/src/imgui/popup/item_properties.cpp @@ -2,7 +2,7 @@ #include -#include "imgui_.hpp" +#include "util/imgui/imgui.hpp" using namespace anm2ed::resource; using namespace anm2ed::types; @@ -16,11 +16,11 @@ namespace anm2ed::imgui::popup addItemSpritesheetID = {}; } - std::set ItemProperties::unused_items_get(anm2::Anm2& anm2, anm2::Animation* animation, anm2::Type type) + std::set ItemProperties::unused_items_get(Anm2& anm2, const Element* animation, int type) { if (!animation) return {}; - if (type == anm2::LAYER) return anm2.layers_unused(*animation); - if (type == anm2::NULL_) return anm2.nulls_unused(*animation); + if (type == LAYER) return anm2.element_unused(ElementType::LAYER_ELEMENT, *animation); + if (type == NULL_) return anm2.element_unused(ElementType::NULL_ELEMENT, *animation); return {}; } @@ -30,11 +30,11 @@ namespace anm2ed::imgui::popup popup.open(); } - void ItemProperties::update(Manager& manager, Settings& settings, Document& document, anm2::Animation* animation, - anm2::Reference& reference, - const std::function& referenceSetItem) + void ItemProperties::update(Manager& manager, Settings& settings, Document& document, Reference& reference, + const std::function& referenceSetItem) { auto& anm2 = document.anm2; + auto animation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex); popup.trigger(); @@ -70,18 +70,18 @@ namespace anm2ed::imgui::popup spaced_pair( [&]() { - ImGui::RadioButton(localize.get(LABEL_LAYER), &type, anm2::LAYER); + ImGui::RadioButton(localize.get(LABEL_LAYER), &type, LAYER); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_TYPE)); }, [&]() { - ImGui::RadioButton(localize.get(LABEL_NULL), &type, anm2::NULL_); + ImGui::RadioButton(localize.get(LABEL_NULL), &type, NULL_); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_TYPE)); }); ImGui::SeparatorText(localize.get(LABEL_SOURCE)); - auto isUnusedItems = animation && !unused_items_get(anm2, animation, (anm2::Type)type).empty(); + auto isUnusedItems = animation && !unused_items_get(anm2, animation, (int)type).empty(); spaced_pair( [&]() { @@ -117,13 +117,13 @@ namespace anm2ed::imgui::popup input_text_string(localize.get(BASIC_NAME), &addItemName); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); - if (type == anm2::LAYER) + if (type == LAYER) { combo_id_mapped(localize.get(LABEL_SPRITESHEET), &addItemSpritesheetID, document.spritesheet.ids, document.spritesheet.labels); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET)); } - else if (type == anm2::NULL_) + else if (type == NULL_) { ImGui::Checkbox(localize.get(LABEL_RECT), &addItemIsShowRect); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_RECT)); @@ -131,11 +131,11 @@ namespace anm2ed::imgui::popup } else if (animation) { - ImGui::SeparatorText(localize.get(type == anm2::LAYER ? LABEL_LAYER : LABEL_NULL)); + ImGui::SeparatorText(localize.get(type == LAYER ? LABEL_LAYER : LABEL_NULL)); if (ImGui::BeginChild("##Existing Items", ImVec2(0, 0))) { - auto unusedItems = unused_items_get(anm2, animation, (anm2::Type)type); + auto unusedItems = unused_items_get(anm2, animation, (int)type); if (addItemID != -1 && !unusedItems.contains(addItemID)) addItemID = -1; for (auto id : unusedItems) @@ -144,30 +144,30 @@ namespace anm2ed::imgui::popup ImGui::PushID(id); - if (type == anm2::LAYER) + if (type == LAYER) { - if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end()) + if (auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, id)) { - auto& layer = it->second; auto label = std::vformat(localize.get(FORMAT_LAYER), - std::make_format_args(id, layer.name, layer.spritesheetID)); + std::make_format_args(id, layer->name, layer->spritesheetId)); if (ImGui::Selectable(label.c_str(), isSelected)) { addItemID = id; - addItemSpritesheetID = layer.spritesheetID; + addItemSpritesheetID = layer->spritesheetId; } } } - else if (type == anm2::NULL_) + else if (type == NULL_) { - if (auto it = anm2.content.nulls.find(id); it != anm2.content.nulls.end()) + auto nulls = anm2.element_get(ElementType::NULLS); + auto null = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr; + if (null) { - auto& null = it->second; - auto label = std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name)); + auto label = std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null->name)); if (ImGui::Selectable(label.c_str(), isSelected)) { addItemID = id; - addItemIsShowRect = null.isShowRect; + addItemIsShowRect = null->isShowRect; } } } @@ -186,20 +186,35 @@ namespace anm2ed::imgui::popup shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) { - anm2::Reference addReference{}; - int insertBeforeID = reference.itemType == anm2::LAYER ? reference.itemID : -1; + auto queuedType = type; + auto queuedDestination = destination; + auto queuedAnimationIndex = reference.animationIndex; + auto queuedInsertBeforeID = reference.itemType == LAYER ? reference.itemID : -1; + auto queuedAddItemID = addItemID; + auto queuedAddItemName = addItemName; + auto queuedAddItemSpritesheetID = addItemSpritesheetID; + auto queuedAddItemIsShowRect = addItemIsShowRect; + auto queuedReferenceSetItem = referenceSetItem; - document.snapshot(localize.get(EDIT_ADD_ITEM)); - if (type == anm2::LAYER) - addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, insertBeforeID, - addItemName, addItemSpritesheetID, (destination::Type)destination); - else if (type == anm2::NULL_) - addReference = anm2.null_animation_add({reference.animationIndex, anm2::NULL_, addItemID}, addItemName, - addItemIsShowRect, (destination::Type)destination); + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + int addId{-1}; - document.change(Document::ITEMS); + document.snapshot(localize.get(EDIT_ADD_ITEM)); + if (queuedType == LAYER) + addId = document.anm2.layer_animation_add( + queuedAnimationIndex, queuedAddItemID, queuedInsertBeforeID, queuedAddItemName, + queuedAddItemSpritesheetID, (destination::Type)queuedDestination); + else if (queuedType == NULL_) + addId = document.anm2.null_animation_add(queuedAnimationIndex, queuedAddItemID, + queuedAddItemName, queuedAddItemIsShowRect, + (destination::Type)queuedDestination); - referenceSetItem(addReference.itemType, addReference.itemID); + document.anm2_change(Document::ITEMS); + + if (addId != -1) queuedReferenceSetItem((int)queuedType, addId); + }}); close(); } diff --git a/src/imgui/popup/item_properties.hpp b/src/imgui/popup/item_properties.hpp index 4391e4a..50a1c1d 100644 --- a/src/imgui/popup/item_properties.hpp +++ b/src/imgui/popup/item_properties.hpp @@ -20,11 +20,10 @@ namespace anm2ed::imgui::popup int addItemSpritesheetID{-1}; void reset(); - std::set unused_items_get(anm2::Anm2&, anm2::Animation*, anm2::Type); + std::set unused_items_get(Anm2&, const Element*, int); public: void open(); - void update(Manager&, Settings&, Document&, anm2::Animation*, anm2::Reference&, - const std::function&); + void update(Manager&, Settings&, Document&, Reference&, const std::function&); }; } diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index 4e91ab7..3d2045d 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -9,7 +9,7 @@ #include "document.hpp" #include "log.hpp" -#include "path_.hpp" +#include "path.hpp" #include "strings.hpp" #include "toast.hpp" #include "types.hpp" @@ -25,35 +25,54 @@ namespace anm2ed::imgui { auto* document = manager.get(index); return document && settings.fileIsSpecialInterpolatedFramesOnSaveReminder && - document->anm2.has_special_interpolated_frames(); + document->anm2.is_special_interpolated_frames(); } - void Taskbar::save_execute(Manager& manager, Settings& settings, const PendingSave& request, bool bakeFrames) + bool Taskbar::save_execute(Manager& manager, Settings& settings, const PendingSave& request, bool bakeFrames) { - manager.save(request.index, request.path, (anm2::Compatibility)settings.fileCompatibility, bakeFrames, - settings.bakeIsRoundScale, settings.bakeIsRoundRotation); + return manager.save(request.index, request.path, (Compatibility)settings.fileCompatibility, bakeFrames, + settings.bakeIsRoundScale, settings.bakeIsRoundRotation); } - bool Taskbar::save_request(Manager& manager, Settings& settings, int index, const std::filesystem::path& path) + void Taskbar::save_enqueue(Manager& manager, Settings& settings, const PendingSave& request, bool bakeFrames) + { + auto index = request.index; + auto path = request.path; + auto compatibility = (Compatibility)settings.fileCompatibility; + auto isRoundScale = settings.bakeIsRoundScale; + auto isRoundRotation = settings.bakeIsRoundRotation; + + manager.command_push({.runManager = + [=](Manager& manager) + { manager.save(index, path, compatibility, bakeFrames, isRoundScale, isRoundRotation); }}); + } + + bool Taskbar::save_request(Manager& manager, Settings& settings, int index, const std::filesystem::path& path, + bool isQueued) { auto* document = manager.get(index); if (!document) return false; - if (settings.fileIsSpecialInterpolatedFramesOnSaveReminder && document->anm2.has_special_interpolated_frames()) + if (settings.fileIsSpecialInterpolatedFramesOnSaveReminder && document->anm2.is_special_interpolated_frames()) { pendingSave = {.index = index, .path = path, .isOpen = true, .disableReminder = false, - .autoBakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave}; + .autoBakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave, + .isQueued = isQueued}; specialInterpolatedFramesReminderPopup.open(); return false; } PendingSave request{.index = index, .path = path}; auto bakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave; - save_execute(manager, settings, request, bakeFrames); - return true; + if (isQueued) + { + save_enqueue(manager, settings, request, bakeFrames); + return true; + } + return save_execute(manager, settings, request, bakeFrames); } bool Taskbar::save_manual(Manager& manager, Settings& settings, int index, const std::filesystem::path& path) @@ -64,19 +83,25 @@ namespace anm2ed::imgui void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) { auto document = manager.get(); - auto animation = document ? document->animation_get() : nullptr; - auto item = document ? document->item_get() : nullptr; + auto itemType = document ? (ItemType)document->reference.itemType : ItemType::NONE; + auto animation = + document ? document->anm2.element_get(ElementType::ANIMATION, document->reference.animationIndex) : nullptr; + auto item = + document ? document->anm2.element_get(document->reference.animationIndex, itemType, document->reference.itemID) + : nullptr; auto frames = document ? &document->frames : nullptr; bool hasRegions = false; if (document) { - for (auto& spritesheet : document->anm2.content.spritesheets | std::views::values) + if (auto spritesheets = document->anm2.element_get(ElementType::SPRITESHEETS)) { - if (!spritesheet.regions.empty()) - { - hasRegions = true; - break; - } + for (auto& spritesheet : spritesheets->children) + for (auto& child : spritesheet.children) + if (spritesheet.type == ElementType::SPRITESHEET && child.type == ElementType::REGION) + { + hasRegions = true; + break; + } } } @@ -86,9 +111,10 @@ namespace anm2ed::imgui if (ImGui::BeginMenu(localize.get(LABEL_FILE_MENU))) { - if (ImGui::MenuItem(localize.get(BASIC_NEW), settings.shortcutNew.c_str())) dialog.file_save(Dialog::ANM2_NEW); + if (ImGui::MenuItem(localize.get(BASIC_NEW), settings.shortcutNew.c_str())) + dialog.file_save(Dialog::ANM2_CREATE); if (ImGui::MenuItem(localize.get(BASIC_OPEN), settings.shortcutOpen.c_str())) - dialog.file_open(Dialog::ANM2_OPEN); + dialog.file_open(Dialog::ANM2_OPEN, true); auto recentFiles = manager.recent_files_ordered(); if (ImGui::BeginMenu(localize.get(LABEL_OPEN_RECENT), !recentFiles.empty())) @@ -99,7 +125,8 @@ namespace anm2ed::imgui auto fileNameUtf8 = path::to_utf8(file.filename()); auto filePathUtf8 = path::to_utf8(file); auto label = std::format(FILE_LABEL_FORMAT, fileNameUtf8, filePathUtf8); - if (ImGui::MenuItem(label.c_str())) manager.open(file); + if (ImGui::MenuItem(label.c_str())) + manager.command_push({.runManager = [file](Manager& manager) { manager.open(file); }}); ImGui::PopID(); } @@ -112,11 +139,11 @@ namespace anm2ed::imgui if (ImGui::MenuItem(localize.get(BASIC_SAVE), settings.shortcutSave.c_str(), false, document)) { if (save_requires_special_prompt(manager, settings, manager.selected)) - save_request(manager, settings, manager.selected, document->path); + save_request(manager, settings, manager.selected, document->path, true); else if (settings.fileIsWarnOverwrite) overwritePopup.open(); else - save_request(manager, settings, manager.selected, document->path); + save_request(manager, settings, manager.selected, document->path, true); } if (ImGui::MenuItem(localize.get(LABEL_SAVE_AS), settings.shortcutSaveAs.c_str(), false, document)) @@ -128,33 +155,38 @@ namespace anm2ed::imgui if (ImGui::MenuItem(localize.get(LABEL_EXIT), settings.shortcutExit.c_str())) isQuitting = true; ImGui::EndMenu(); } - if (dialog.is_selected(Dialog::ANM2_NEW)) + if (dialog.is_selected(Dialog::ANM2_CREATE)) { - manager.new_(dialog.path); + auto path = dialog.path; + manager.command_push({.runManager = [path](Manager& manager) { manager.new_(path); }}); dialog.reset(); } if (dialog.is_selected(Dialog::ANM2_OPEN)) { - manager.open(dialog.path); + auto paths = dialog.paths; + manager.command_push({.runManager = + [paths](Manager& manager) + { + for (auto& path : paths) manager.open(path); + }}); dialog.reset(); } if (dialog.is_selected(Dialog::ANM2_SAVE)) { - save_request(manager, settings, manager.selected, dialog.path); + save_request(manager, settings, manager.selected, dialog.path, true); dialog.reset(); } if (ImGui::BeginMenu(localize.get(LABEL_WIZARD_MENU))) { if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID), nullptr, false, - item && document->reference.itemType == anm2::LAYER)) + item && itemType == ItemType::LAYER)) generatePopup.open(); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_WIZARD_GENERATE_ANIMATION_FROM_GRID)); - bool isChangeAllFramesAvailable = - frames && !frames->selection.empty() && document->reference.itemType != anm2::TRIGGER; + bool isChangeAllFramesAvailable = frames && !frames->selection.empty() && itemType != ItemType::TRIGGER; bool isChangeAllAnimationsAvailable = document && !document->animation.selection.empty(); if (ImGui::MenuItem(localize.get(LABEL_CHANGE_ALL_FRAME_PROPERTIES), nullptr, false, isChangeAllFramesAvailable || isChangeAllAnimationsAvailable)) @@ -163,8 +195,13 @@ namespace anm2ed::imgui if (ImGui::MenuItem(localize.get(LABEL_SCAN_AND_SET_REGIONS), nullptr, false, document && hasRegions)) { - DOCUMENT_EDIT_PTR(document, localize.get(EDIT_SCAN_AND_SET_REGIONS), Document::FRAMES, - document->anm2.scan_and_set_regions()); + manager.command_push({manager.selected, + [](Manager&, Document& document) + { + document.snapshot(localize.get(EDIT_SCAN_AND_SET_REGIONS)); + document.scan_and_set_regions(); + document.change(Document::FRAMES); + }}); toasts.push(localize.get(TOAST_SCAN_AND_SET_REGIONS)); logger.info(localize.get(TOAST_SCAN_AND_SET_REGIONS, anm2ed::ENGLISH)); } @@ -223,7 +260,7 @@ namespace anm2ed::imgui { if (document) { - generateAnimationFromGrid.update(*document, resources, settings); + generateAnimationFromGrid.update(manager, *document, resources, settings); if (generateAnimationFromGrid.isEnd) generatePopup.close(); } ImGui::EndPopup(); @@ -235,7 +272,7 @@ namespace anm2ed::imgui { if (document) { - changeAllFrameProperties.update(*document, settings, true); + changeAllFrameProperties.update(manager, *document, settings, true); if (changeAllFrameProperties.isChanged) changePopup.close(); } ImGui::EndPopup(); @@ -285,7 +322,7 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(BASIC_YES), widgetSize)) { - save_request(manager, settings); + save_request(manager, settings, manager.selected, {}, true); overwritePopup.close(); } @@ -312,7 +349,10 @@ namespace anm2ed::imgui { if (pendingSave.disableReminder) settings.fileIsSpecialInterpolatedFramesOnSaveReminder = false; settings.fileBakeSpecialInterpolatedFramesOnSave = pendingSave.autoBakeFrames; - save_execute(manager, settings, pendingSave, true); + if (pendingSave.isQueued) + save_enqueue(manager, settings, pendingSave, true); + else + save_execute(manager, settings, pendingSave, true); pendingSave = {}; specialInterpolatedFramesReminderPopup.close(); } @@ -322,7 +362,10 @@ namespace anm2ed::imgui { if (pendingSave.disableReminder) settings.fileIsSpecialInterpolatedFramesOnSaveReminder = false; settings.fileBakeSpecialInterpolatedFramesOnSave = pendingSave.autoBakeFrames; - save_execute(manager, settings, pendingSave, false); + if (pendingSave.isQueued) + save_enqueue(manager, settings, pendingSave, false); + else + save_execute(manager, settings, pendingSave, false); pendingSave = {}; specialInterpolatedFramesReminderPopup.close(); } @@ -340,16 +383,16 @@ namespace anm2ed::imgui aboutPopup.end(); - if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_NEW); - if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(Dialog::ANM2_OPEN); + if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_CREATE); + if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(Dialog::ANM2_OPEN, true); if (shortcut(manager.chords[SHORTCUT_SAVE], shortcut::GLOBAL)) { if (save_requires_special_prompt(manager, settings)) - save_request(manager, settings); + save_request(manager, settings, manager.selected, {}, true); else if (settings.fileIsWarnOverwrite) overwritePopup.open(); else - save_request(manager, settings); + save_request(manager, settings, manager.selected, {}, true); } if (shortcut(manager.chords[SHORTCUT_SAVE_AS], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_SAVE); if (shortcut(manager.chords[SHORTCUT_EXIT], shortcut::GLOBAL)) isQuitting = true; diff --git a/src/imgui/taskbar.hpp b/src/imgui/taskbar.hpp index 3570103..722b948 100644 --- a/src/imgui/taskbar.hpp +++ b/src/imgui/taskbar.hpp @@ -4,7 +4,7 @@ #include "canvas.hpp" #include "dialog.hpp" -#include "imgui_.hpp" +#include "util/imgui/imgui.hpp" #include "manager.hpp" #include "resources.hpp" #include "settings.hpp" @@ -27,6 +27,7 @@ namespace anm2ed::imgui bool isOpen{}; bool disableReminder{}; bool autoBakeFrames{}; + bool isQueued{}; }; wizard::ChangeAllFrameProperties changeAllFrameProperties{}; @@ -49,8 +50,9 @@ namespace anm2ed::imgui PendingSave pendingSave{}; bool save_requires_special_prompt(Manager&, Settings&, int = -1) const; - void save_execute(Manager&, Settings&, const PendingSave&, bool); - bool save_request(Manager&, Settings&, int = -1, const std::filesystem::path& = {}); + bool save_execute(Manager&, Settings&, const PendingSave&, bool); + void save_enqueue(Manager&, Settings&, const PendingSave&, bool); + bool save_request(Manager&, Settings&, int = -1, const std::filesystem::path& = {}, bool = false); public: float height{}; diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index 0e713ff..0a37064 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -7,15 +7,15 @@ #include #include #include -#include #include #include -#include "imgui_.hpp" +#include "actions.hpp" +#include "util/imgui/imgui.hpp" #include "log.hpp" -#include "math_.hpp" -#include "path_.hpp" +#include "math.hpp" +#include "path.hpp" #include "strings.hpp" #include "toast.hpp" #include "tool.hpp" @@ -103,7 +103,7 @@ namespace anm2ed::imgui pixels[index + 2] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 2] / alphaUnit), 0.0f, 255.0f); } } - bool render_audio_stream_generate(AudioStream& audioStream, std::map& sounds, + bool render_audio_stream_generate(AudioStream& audioStream, std::map& sounds, const std::vector& frameSoundIDs, int fps) { audioStream.stream.clear(); @@ -122,7 +122,7 @@ namespace anm2ed::imgui for (auto soundID : frameSoundIDs) { - if (soundID != -1 && sounds.contains(soundID)) sounds.at(soundID).audio.play(false, mixer); + if (soundID != -1 && sounds.contains(soundID)) sounds.at(soundID).play(false, mixer); sampleFrameAccumulator += framesPerStep; auto sampleFramesToGenerate = (int)std::floor(sampleFrameAccumulator); @@ -133,7 +133,7 @@ namespace anm2ed::imgui if (!MIX_Generate(mixer, frameBuffer.data(), (int)(frameBuffer.size() * sizeof(float)))) { for (auto& [_, sound] : sounds) - sound.audio.track_detach(mixer); + sound.track_detach(mixer); MIX_DestroyMixer(mixer); audioStream.stream.clear(); return false; @@ -143,7 +143,7 @@ namespace anm2ed::imgui } for (auto& [_, sound] : sounds) - sound.audio.track_detach(mixer); + sound.track_detach(mixer); MIX_DestroyMixer(mixer); return true; } @@ -151,6 +151,8 @@ namespace anm2ed::imgui AnimationPreview::AnimationPreview() : Canvas(vec2()) {} + bool AnimationPreview::is_focused_get() const { return isFocused; } + void AnimationPreview::tick(Manager& manager, Settings& settings, float deltaSeconds) { auto& document = *manager.get(); @@ -164,8 +166,24 @@ namespace anm2ed::imgui auto stop_all_sounds = [&]() { - for (auto& sound : anm2.content.sounds | std::views::values) - sound.audio.stop(mixer); + for (auto& [_, sound] : document.sounds) + sound.stop(mixer); + }; + + auto trigger_sound_id_get = [&](Element* animation, float time) + { + if (!animation) return -1; + auto triggers = animation_item_get(*animation, ItemType::TRIGGER); + if (!triggers || !triggers->isVisible) return -1; + + auto trigger = frame_generate(*triggers, time); + if (!trigger.isVisible || trigger.soundIds.empty()) return -1; + + auto soundIndex = + trigger.soundIds.size() > 1 ? (size_t)math::random_in_range(0.0f, (float)trigger.soundIds.size()) : (size_t)0; + soundIndex = std::min(soundIndex, trigger.soundIds.size() - 1); + auto soundID = trigger.soundIds[soundIndex]; + return document.sound_get(soundID) ? soundID : -1; }; if (manager.isRecording) @@ -261,7 +279,7 @@ namespace anm2ed::imgui { if (settings.timelineIsSound && type != render::GIF) { - if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, renderFrameRate)) + if (!render_audio_stream_generate(audioStream, document.sounds, renderFrameSoundIDs, renderFrameRate)) { toasts.push(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED)); logger.error("Failed to generate deterministic render audio stream; exporting without audio."); @@ -322,26 +340,13 @@ namespace anm2ed::imgui { if (settings.timelineIsSound && renderTempFrames.empty()) audioStream.capture_begin(mixer); auto frameSoundID = -1; - if (settings.timelineIsSound && !anm2.content.sounds.empty()) + if (settings.timelineIsSound && !document.sounds.empty()) { auto soundTime = (int)std::floor(playback.time); - if (auto animation = document.animation_get(); soundTime != renderFrameSoundTimePrev && animation && - animation->triggers.isVisible && - (!settings.timelineIsOnlyShowLayers || manager.isRecording)) - { - if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible) - { - if (!trigger.soundIDs.empty()) - { - auto soundIndex = trigger.soundIDs.size() > 1 - ? (size_t)math::random_in_range(0.0f, (float)trigger.soundIDs.size()) - : (size_t)0; - soundIndex = std::min(soundIndex, trigger.soundIDs.size() - 1); - auto soundID = trigger.soundIDs[soundIndex]; - if (anm2.content.sounds.contains(soundID)) frameSoundID = soundID; - } - } - } + auto animation = anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex); + if (soundTime != renderFrameSoundTimePrev && animation && + (!settings.timelineIsOnlyShowLayers || manager.isRecording)) + frameSoundID = trigger_sound_id_get(animation, playback.time); renderFrameSoundTimePrev = soundTime; } renderFrameSoundIDs.push_back(frameSoundID); @@ -376,7 +381,7 @@ namespace anm2ed::imgui if (playback.isPlaying) { - auto animation = document.animation_get(); + auto animation = anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex); auto& isSound = settings.timelineIsSound; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; @@ -388,27 +393,15 @@ namespace anm2ed::imgui } else { - if (!manager.isRecording && !anm2.content.sounds.empty() && isSound) + if (!manager.isRecording && !document.sounds.empty() && isSound) { - if (animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording)) - { - if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible) - { - if (!trigger.soundIDs.empty()) - { - auto soundIndex = trigger.soundIDs.size() > 1 - ? (size_t)math::random_in_range(0.0f, (float)trigger.soundIDs.size()) - : (size_t)0; - soundIndex = std::min(soundIndex, trigger.soundIDs.size() - 1); - auto soundID = trigger.soundIDs[soundIndex]; - - if (anm2.content.sounds.contains(soundID)) anm2.content.sounds[soundID].audio.play(false, mixer); - } - } - } + if (!isOnlyShowLayers || manager.isRecording) + if (auto soundID = trigger_sound_id_get(animation, playback.time); soundID != -1) + if (auto sound = document.sound_get(soundID)) sound->play(false, mixer); } - auto fps = std::max(anm2.info.fps, 1); + auto info = element_first_get(anm2.root, ElementType::INFO); + auto fps = std::max(info ? info->fps : 30, 1); playback.tick(fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording, deltaSeconds); @@ -422,11 +415,13 @@ namespace anm2ed::imgui void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources) { + isFocused = false; + auto& document = *manager.get(); auto& anm2 = document.anm2; auto& playback = document.playback; auto& reference = document.reference; - auto animation = document.animation_get(); + auto animation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex); auto& pan = document.previewPan; auto& zoom = document.previewZoom; auto& backgroundColor = settings.previewBackgroundColor; @@ -502,6 +497,7 @@ namespace anm2ed::imgui if (ImGui::Begin(localize.get(LABEL_ANIMATION_PREVIEW_WINDOW), &settings.windowIsAnimationPreview)) { + isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); manager.isAbleToRecord = true; auto childSize = ImVec2(row_widget_width_get(4), @@ -636,11 +632,12 @@ namespace anm2ed::imgui savedZoom = zoom; savedPan = pan; - if (auto rect = anm2.animation_rect(*document.animation_get(), isRootTransform); rect != vec4(-1.0f)) - { - size_set(vec2(rect.z, rect.w) * settings.renderScale); - set_to_rect(zoom, pan, rect); - } + if (animation) + if (auto rect = anm2.animation_rect(*animation, isRootTransform); rect != vec4(-1.0f)) + { + size_set(vec2(rect.z, rect.w) * settings.renderScale); + set_to_rect(zoom, pan, rect); + } isSizeTrySet = false; } @@ -738,43 +735,56 @@ namespace anm2ed::imgui add_samples(settings.onionskinAfterCount, 1, settings.onionskinAfterColor); } - auto render = [&](anm2::Animation* animation, float time, vec3 colorOffset = {}, float alphaOffset = {}, + auto referenceItemType = static_cast(reference.itemType); + + auto render = [&](Element* animation, float time, vec3 colorOffset = {}, float alphaOffset = {}, const std::vector* layeredOnions = nullptr, bool isIndexMode = false) { - auto sample_time_for_item = [&](anm2::Item& item, const OnionskinSample& sample) -> std::optional + auto sample_time_for_item = [&](Element& item, const OnionskinSample& sample) -> std::optional { if (!isIndexMode) { if (sample.time < 0.0f || sample.time > animation->frameNum) return std::nullopt; return sample.time; } - if (item.frames.empty()) return std::nullopt; - int baseIndex = item.frame_index_from_time_get(frameTime); + if (item.children.empty()) return std::nullopt; + int baseIndex = frame_index_from_time_get(item, frameTime); if (baseIndex < 0) return std::nullopt; int sampleIndex = baseIndex + sample.indexOffset; - if (sampleIndex < 0 || sampleIndex >= (int)item.frames.size()) return std::nullopt; - return item.frame_time_from_index_get(sampleIndex); + if (!track_frame_get(item, sampleIndex)) return std::nullopt; + return frame_time_from_index_get(item, sampleIndex); }; - auto transform_for_time = [&](anm2::Animation* anim, float t) + auto root = animation_item_get(*animation, ItemType::ROOT); + + auto transform_for_time = [&](float t) { auto sampleTransform = baseTransform; - if (isRootTransform) + if (isRootTransform && root) { - auto rootFrame = anim->rootAnimation.frame_generate(t, anm2::ROOT); + auto rootFrame = frame_generate(*root, t); sampleTransform *= math::quad_model_parent_get(rootFrame.position, {}, math::percent_to_unit(rootFrame.scale), rootFrame.rotation); } return sampleTransform; }; - auto transform = transform_for_time(animation, time); + auto transform = transform_for_time(time); + + auto is_track_group_visible = [](const Element& container, const Element& track) + { + if (track.groupId == -1) return true; + for (const auto& child : container.children) + if (child.type == ElementType::GROUP && child.id == track.groupId) return child.isVisible; + return true; + }; auto draw_root = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { - auto rootFrame = animation->rootAnimation.frame_generate(sampleTime, anm2::ROOT); - if (isOnlyShowLayers || !rootFrame.isVisible || !animation->rootAnimation.isVisible) return; + if (!root) return; + auto rootFrame = frame_generate(*root, sampleTime); + if (isOnlyShowLayers || !rootFrame.isVisible || !root->isVisible) return; auto rootModel = isRootTransform ? math::quad_model_get(TARGET_SIZE, {}, TARGET_SIZE * 0.5f) @@ -788,34 +798,45 @@ namespace anm2ed::imgui texture_render(shaderTexture, resources.icons[icon].id, rootTransform, color); }; - if (layeredOnions) + if (layeredOnions && root) for (auto& sample : *layeredOnions) - if (auto sampleTime = sample_time_for_item(animation->rootAnimation, sample)) + if (auto sampleTime = sample_time_for_item(*root, sample)) { - auto sampleTransform = transform_for_time(animation, *sampleTime); + auto sampleTransform = transform_for_time(*sampleTime); draw_root(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); } draw_root(time, transform, {}, 0.0f, false); - for (auto& id : animation->layerOrder) + if (auto layerAnimations = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS)) { - if (!animation->layerAnimations.contains(id)) continue; - auto& layerAnimation = animation->layerAnimations[id]; - if (!layerAnimation.isVisible) continue; - - if (!anm2.content.layers.contains(id)) continue; - auto& layer = anm2.content.layers.at(id); - - auto spritesheet = anm2.spritesheet_get(layer.spritesheetID); - if (!spritesheet || !spritesheet->is_valid()) continue; - - auto draw_layer = - [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) + auto layer_animation_draw = [&](auto&& self, Element& layerAnimation, bool isParentVisible = true) -> void { - if (auto frame = layerAnimation.frame_generate(sampleTime, anm2::LAYER); frame.isVisible) + if (layerAnimation.type == ElementType::GROUP) { - auto& texture = spritesheet->texture; + for (auto& child : layerAnimation.children) + self(self, child, isParentVisible && layerAnimation.isVisible); + return; + } + if (layerAnimation.type != ElementType::LAYER_ANIMATION || !isParentVisible || + !layerAnimation.isVisible || !is_track_group_visible(*layerAnimations, layerAnimation)) + return; + + auto id = layerAnimation.layerId; + auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, id); + if (!layer) return; + + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, layer->spritesheetId); + auto textureInfo = document.texture_get(layer->spritesheetId); + if (!spritesheet || !textureInfo || !textureInfo->is_valid()) return; + + auto draw_layer = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, + float sampleAlpha, bool isOnion) + { + auto frame = frame_generate(layerAnimation, sampleTime); + if (!frame.isVisible) return; + + auto& texture = *textureInfo; auto texSize = vec2(texture.size); if (texSize.x <= 0.0f || texSize.y <= 0.0f) return; @@ -835,9 +856,9 @@ namespace anm2ed::imgui vec3 frameColorOffset = frame.colorOffset + colorOffset + sampleColor; vec4 frameTint = frame.tint; - if (isRootTransform) + if (isRootTransform && root) { - auto rootFrame = animation->rootAnimation.frame_generate(sampleTime, anm2::ROOT); + auto rootFrame = frame_generate(*root, sampleTime); frameColorOffset += rootFrame.colorOffset; frameTint *= rootFrame.tint; } @@ -860,39 +881,54 @@ namespace anm2ed::imgui texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color); } - } + }; + + if (layeredOnions) + for (auto& sample : *layeredOnions) + if (auto sampleTime = sample_time_for_item(layerAnimation, sample)) + { + auto sampleTransform = transform_for_time(*sampleTime); + draw_layer(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); + } + + draw_layer(time, transform, {}, 0.0f, false); }; - - if (layeredOnions) - for (auto& sample : *layeredOnions) - if (auto sampleTime = sample_time_for_item(layerAnimation, sample)) - { - auto sampleTransform = transform_for_time(animation, *sampleTime); - draw_layer(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); - } - - draw_layer(time, transform, {}, 0.0f, false); + for (auto& layerAnimation : layerAnimations->children) + layer_animation_draw(layer_animation_draw, layerAnimation); } - for (auto& [id, nullAnimation] : animation->nullAnimations) + if (auto nullAnimations = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS)) { - if (!nullAnimation.isVisible || isOnlyShowLayers) continue; - - auto nullInfo = anm2.content.nulls.find(id); - if (nullInfo == anm2.content.nulls.end()) continue; - auto& isShowRect = nullInfo->second.isShowRect; - - auto draw_null = - [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) + auto null_animation_draw = [&](auto&& self, Element& nullAnimation, bool isParentVisible = true) -> void { - if (auto frame = nullAnimation.frame_generate(sampleTime, anm2::NULL_); frame.isVisible) + if (nullAnimation.type == ElementType::GROUP) { + for (auto& child : nullAnimation.children) + self(self, child, isParentVisible && nullAnimation.isVisible); + return; + } + if (nullAnimation.type != ElementType::NULL_ANIMATION || !isParentVisible || !nullAnimation.isVisible || + !is_track_group_visible(*nullAnimations, nullAnimation) || isOnlyShowLayers) + return; + + auto id = nullAnimation.nullId; + auto nulls = anm2.element_get(ElementType::NULLS); + auto nullInfo = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr; + if (!nullInfo) return; + auto isShowRect = nullInfo->isShowRect; + + auto draw_null = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, + float sampleAlpha, bool isOnion) + { + auto frame = frame_generate(nullAnimation, sampleTime); + if (!frame.isVisible) return; + auto icon = isShowRect ? icon::POINT : isAltIcons ? icon::TARGET_ALT : icon::TARGET; auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE; auto color = isOnion ? vec4(sampleColor, 1.0f - sampleAlpha) - : id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED - : NULL_COLOR; + : id == reference.itemID && referenceItemType == ItemType::NULL_ ? color::RED + : NULL_COLOR; auto nullModel = math::quad_model_get(size, frame.position, size * 0.5f, math::percent_to_unit(frame.scale), frame.rotation); @@ -908,18 +944,20 @@ namespace anm2ed::imgui rect_render(shaderLine, rectTransform, rectModel, color); } - } + }; + + if (layeredOnions) + for (auto& sample : *layeredOnions) + if (auto sampleTime = sample_time_for_item(nullAnimation, sample)) + { + auto sampleTransform = transform_for_time(*sampleTime); + draw_null(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); + } + + draw_null(time, transform, {}, 0.0f, false); }; - - if (layeredOnions) - for (auto& sample : *layeredOnions) - if (auto sampleTime = sample_time_for_item(nullAnimation, sample)) - { - auto sampleTransform = transform_for_time(animation, *sampleTime); - draw_null(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); - } - - draw_null(time, transform, {}, 0.0f, false); + for (auto& nullAnimation : nullAnimations->children) + null_animation_draw(null_animation_draw, nullAnimation); } }; @@ -929,7 +967,7 @@ namespace anm2ed::imgui render(animation, frameTime, {}, 0.0f, layeredOnions, settings.onionskinMode == (int)OnionskinMode::INDEX); - if (auto overlayAnimation = anm2.animation_get(overlayIndex)) + if (auto overlayAnimation = anm2.element_get(ElementType::ANIMATION, overlayIndex)) render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency), layeredOnions, settings.onionskinMode == (int)OnionskinMode::INDEX); } @@ -945,10 +983,10 @@ namespace anm2ed::imgui isPreviewHovered = ImGui::IsItemHovered(); - if (animation && animation->triggers.isVisible && !isOnlyShowLayers && !manager.isRecording) + auto triggers = animation ? animation_item_get(*animation, ItemType::TRIGGER) : nullptr; + if (animation && triggers && triggers->isVisible && !isOnlyShowLayers && !manager.isRecording) { - if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); - trigger.isVisible && trigger.eventID > -1) + if (auto trigger = frame_generate(*triggers, frameTime); trigger.isVisible && trigger.eventId > -1) { auto clipMin = ImGui::GetItemRectMin(); auto clipMax = ImGui::GetItemRectMax(); @@ -958,9 +996,9 @@ namespace anm2ed::imgui drawList->PushClipRect(clipMin, clipMax); ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); auto triggerTextColor = isLightTheme ? TRIGGER_TEXT_COLOR_LIGHT : TRIGGER_TEXT_COLOR_DARK; - if (anm2.content.events.contains(trigger.eventID)) - drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor), - anm2.content.events.at(trigger.eventID).name.c_str()); + auto events = anm2.element_get(ElementType::EVENTS); + auto event = events ? element_child_id_get(*events, ElementType::EVENT_ELEMENT, trigger.eventId) : nullptr; + if (event) drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor), event->name.c_str()); ImGui::PopFont(); drawList->PopClipRect(); } @@ -999,20 +1037,23 @@ namespace anm2ed::imgui auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown; auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; - auto isZoomIn = shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL); - auto isZoomOut = shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL); + auto isZoomIn = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL); + auto isZoomOut = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL); auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); - auto frame = document.frame_get(); - auto item = document.item_get(); + auto frame = + anm2.element_get(reference.animationIndex, referenceItemType, reference.frameIndex, reference.itemID); + auto item = anm2.element_get(reference.animationIndex, referenceItemType, reference.itemID); auto useTool = tool; auto step = isMod ? STEP_FAST : STEP; mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); - auto selectedNullIt = reference.itemType == anm2::NULL_ ? anm2.content.nulls.find(reference.itemID) - : anm2.content.nulls.end(); - bool isSelectedNullRect = selectedNullIt != anm2.content.nulls.end() && selectedNullIt->second.isShowRect; - auto null_rect_top_left = [](const anm2::Frame& frame) { return frame.position - (frame.scale * 0.5f); }; + auto nulls = anm2.element_get(ElementType::NULLS); + auto selectedNull = referenceItemType == ItemType::NULL_ && nulls + ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, reference.itemID) + : nullptr; + bool isSelectedNullRect = selectedNull && selectedNull->isShowRect; + auto null_rect_top_left = [](const Element& frame) { return frame.position - (frame.scale * 0.5f); }; if (isMouseMiddleDown) useTool = tool::PAN; if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE; @@ -1033,8 +1074,31 @@ namespace anm2ed::imgui auto isToolDuring = isToolMouseDown || isKeyDown; auto isToolEnd = isToolMouseReleased || isKeyReleased; - auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST) - { item->frames_change(frameChange, reference.itemType, changeType, frames); }; + auto frame_snapshot = [&](auto message) + { + manager.command_push({manager.selected, + [message](Manager&, Document& document) { document.snapshot(localize.get(message)); }}); + }; + auto frame_change_apply = [&](FrameChange frameChange, ChangeType changeType = ChangeType::ADJUST) + { + auto queuedReference = reference; + auto queuedReferenceItemType = referenceItemType; + auto queuedFrames = frames; + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto item = document.anm2.element_get(queuedReference.animationIndex, + queuedReferenceItemType, + queuedReference.itemID); + if (!item) return; + frames_change(*item, frameChange, queuedReferenceItemType, changeType, queuedFrames); + }}); + }; + auto frames_changed = [&]() + { + manager.command_push( + {manager.selected, [](Manager&, Document& document) { document.anm2_change(Document::FRAMES); }}); + }; auto null_rect_change = [&](vec2 topLeft, vec2 rectSize) { topLeft = vec2(ivec2(topLeft)); @@ -1064,7 +1128,7 @@ namespace anm2ed::imgui if (!item || !frame || frames.empty()) break; if (isToolBegin) { - document.snapshot(localize.get(EDIT_FRAME_POSITION)); + frame_snapshot(EDIT_FRAME_POSITION); if (isToolMouseClicked) { auto origin = isSelectedNullRect ? null_rect_top_left(*frame) : frame->position; @@ -1082,13 +1146,13 @@ namespace anm2ed::imgui frame_change_apply({.positionX = (int)position.x, .positionY = (int)position.y}); } - if (isLeftPressed) frame_change_apply({.positionX = step}, anm2::SUBTRACT); - if (isRightPressed) frame_change_apply({.positionX = step}, anm2::ADD); - if (isUpPressed) frame_change_apply({.positionY = step}, anm2::SUBTRACT); - if (isDownPressed) frame_change_apply({.positionY = step}, anm2::ADD); + if (isLeftPressed) frame_change_apply({.positionX = step}, ChangeType::SUBTRACT); + if (isRightPressed) frame_change_apply({.positionX = step}, ChangeType::ADD); + if (isUpPressed) frame_change_apply({.positionY = step}, ChangeType::SUBTRACT); + if (isDownPressed) frame_change_apply({.positionY = step}, ChangeType::ADD); if (isToolMouseReleased) isMoveDragging = false; - if (isToolEnd) document.change(Document::FRAMES); + if (isToolEnd) frames_changed(); if (isToolDuring) { if (ImGui::BeginTooltip()) @@ -1104,7 +1168,7 @@ namespace anm2ed::imgui if (!item || !frame || frames.empty()) break; if (isToolBegin) { - document.snapshot(localize.get(EDIT_FRAME_SCALE)); + frame_snapshot(EDIT_FRAME_SCALE); if (isToolMouseClicked && isSelectedNullRect) nullRectScaleAnchor = null_rect_top_left(*frame); } if (isToolMouseDown) @@ -1134,21 +1198,17 @@ namespace anm2ed::imgui if (isSelectedNullRect) { - if (isLeftPressed) - frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, anm2::SUBTRACT); - if (isRightPressed) - frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, anm2::ADD); - if (isUpPressed) - frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, anm2::SUBTRACT); - if (isDownPressed) - frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, anm2::ADD); + if (isLeftPressed) frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, ChangeType::SUBTRACT); + if (isRightPressed) frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, ChangeType::ADD); + if (isUpPressed) frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, ChangeType::SUBTRACT); + if (isDownPressed) frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, ChangeType::ADD); } else { - if (isLeftPressed) frame_change_apply({.scaleX = step}, anm2::SUBTRACT); - if (isRightPressed) frame_change_apply({.scaleX = step}, anm2::ADD); - if (isUpPressed) frame_change_apply({.scaleY = step}, anm2::SUBTRACT); - if (isDownPressed) frame_change_apply({.scaleY = step}, anm2::ADD); + if (isLeftPressed) frame_change_apply({.scaleX = step}, ChangeType::SUBTRACT); + if (isRightPressed) frame_change_apply({.scaleX = step}, ChangeType::ADD); + if (isUpPressed) frame_change_apply({.scaleY = step}, ChangeType::SUBTRACT); + if (isDownPressed) frame_change_apply({.scaleY = step}, ChangeType::ADD); } if (isToolDuring) @@ -1173,15 +1233,15 @@ namespace anm2ed::imgui maxPoint = vec2(ivec2(maxPoint)); null_rect_change(minPoint, maxPoint - minPoint); } - document.change(Document::FRAMES); + frames_changed(); } break; case tool::ROTATE: if (!item || !frame || frames.empty()) break; - if (isToolBegin) document.snapshot(localize.get(EDIT_FRAME_ROTATION)); - if (isToolMouseDown) frame_change_apply({.rotation = (int)mouseDelta.x}, anm2::ADD); - if (isLeftPressed || isDownPressed) frame_change_apply({.rotation = step}, anm2::SUBTRACT); - if (isUpPressed || isRightPressed) frame_change_apply({.rotation = step}, anm2::ADD); + if (isToolBegin) frame_snapshot(EDIT_FRAME_ROTATION); + if (isToolMouseDown) frame_change_apply({.rotation = (int)mouseDelta.x}, ChangeType::ADD); + if (isLeftPressed || isDownPressed) frame_change_apply({.rotation = step}, ChangeType::SUBTRACT); + if (isUpPressed || isRightPressed) frame_change_apply({.rotation = step}, ChangeType::ADD); if (isToolDuring) { @@ -1193,7 +1253,7 @@ namespace anm2ed::imgui } } - if (isToolEnd) document.change(Document::FRAMES); + if (isToolEnd) frames_changed(); break; default: break; @@ -1228,27 +1288,17 @@ namespace anm2ed::imgui } } - if (tool == tool::PAN && ImGui::BeginPopupContextWindow("##Animation Preview Context Menu", ImGuiMouseButton_Right)) + if (tool == tool::PAN) { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(LABEL_CENTER_VIEW), settings.shortcutCenterView.c_str())) center_view(); - if (ImGui::MenuItem(localize.get(LABEL_FIT), settings.shortcutFit.c_str(), false, animation)) fit_view(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_IN), settings.shortcutZoomIn.c_str())) zoom_in(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_OUT), settings.shortcutZoomOut.c_str())) zoom_out(); - - ImGui::EndPopup(); + Actions actions{}; + actions_undo_redo_add(actions, manager, document); + actions.separator(); + actions.add(ACTION_CENTER_VIEW, []() { return true; }, center_view); + actions.add(ACTION_FIT_VIEW, [&]() { return animation; }, fit_view); + actions.separator(); + actions.add(ACTION_ZOOM_IN, []() { return true; }, zoom_in); + actions.add(ACTION_ZOOM_OUT, []() { return true; }, zoom_out); + actions_context_window_draw("##Animation Preview Context Menu", actions, settings); } manager.progressPopup.trigger(); diff --git a/src/imgui/window/animation_preview.hpp b/src/imgui/window/animation_preview.hpp index 1ed137b..8e17dd6 100644 --- a/src/imgui/window/animation_preview.hpp +++ b/src/imgui/window/animation_preview.hpp @@ -15,6 +15,7 @@ namespace anm2ed::imgui MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); AudioStream audioStream = AudioStream(mixer); bool wasPlaybackPlaying{}; + bool isFocused{}; bool isPreviewHovered{}; bool isSizeTrySet{true}; Settings savedSettings{}; @@ -38,6 +39,7 @@ namespace anm2ed::imgui public: AnimationPreview(); + bool is_focused_get() const; void tick(Manager&, Settings&, float); void update(Manager&, Settings&, Resources&); }; diff --git a/src/imgui/window/animations.cpp b/src/imgui/window/animations.cpp deleted file mode 100644 index 52fcf6c..0000000 --- a/src/imgui/window/animations.cpp +++ /dev/null @@ -1,501 +0,0 @@ -#include "animations.hpp" - -#include -#include - -#include "log.hpp" -#include "strings.hpp" -#include "toast.hpp" -#include "vector_.hpp" - -using namespace anm2ed::util; -using namespace anm2ed::resource; -using namespace anm2ed::types; - -namespace anm2ed::imgui -{ - void Animations::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) - { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& reference = document.reference; - auto& selection = document.animation.selection; - auto& mergeSelection = document.merge.selection; - auto& mergeReference = document.merge.reference; - auto& overlayIndex = document.overlayIndex; - - auto rename_format_get = [&](int index) - { return std::format("###Document #{} Animation #{}", manager.selected, index); }; - - auto rename = [&]() - { - if (!selection.empty()) renameQueued = *selection.begin(); - }; - - auto add = [&]() - { - auto behavior = [&]() - { - anm2::Animation animation; - animation.name = localize.get(TEXT_NEW_ANIMATION); - if (anm2::Animation* referenceAnimation = document.animation_get()) - { - for (auto [id, layerAnimation] : referenceAnimation->layerAnimations) - animation.layerAnimations[id] = anm2::Item(); - animation.layerOrder = referenceAnimation->layerOrder; - for (auto [id, nullAnimation] : referenceAnimation->nullAnimations) - animation.nullAnimations[id] = anm2::Item(); - } - animation.rootAnimation.frames.emplace_back(anm2::Frame()); - - auto index = (int)anm2.animations.items.size(); - if (!selection.empty()) - { - index = *selection.rbegin() + 1; - index = std::min(index, (int)anm2.animations.items.size()); - } - - if (anm2.animations.items.empty()) anm2.animations.defaultAnimation = animation.name; - - anm2.animations.items.insert(anm2.animations.items.begin() + index, animation); - selection = {index}; - reference = {index}; - newAnimationSelectedIndex = index; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_ADD_ANIMATION), Document::ANIMATIONS, behavior()); - }; - - auto remove = [&]() - { - auto behavior = [&]() - { - if (!selection.empty()) - { - for (auto it = selection.rbegin(); it != selection.rend(); ++it) - { - auto i = *it; - if (overlayIndex == i) overlayIndex = -1; - if (reference.animationIndex == i) reference.animationIndex = -1; - anm2.animations.items.erase(anm2.animations.items.begin() + i); - } - selection.clear(); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ANIMATIONS), Document::ANIMATIONS, behavior()); - }; - - auto duplicate = [&]() - { - auto behavior = [&]() - { - auto duplicated = selection; - auto end = std::ranges::max(duplicated); - for (auto& id : duplicated) - { - anm2.animations.items.insert(anm2.animations.items.begin() + end, anm2.animations.items[id]); - selection.insert(++end); - selection.erase(id); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ANIMATIONS), Document::ANIMATIONS, behavior()); - }; - - auto merge = [&]() - { - auto behavior = [&]() - { - if (mergeSelection.contains(overlayIndex)) overlayIndex = -1; - auto merged = anm2.animations_merge(mergeReference, mergeSelection, (merge::Type)settings.mergeType, - settings.mergeIsDeleteAnimationsAfter); - - selection = {merged}; - reference = {merged}; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_ANIMATIONS), Document::ANIMATIONS, behavior()); - }; - - auto merge_popup_open = [&]() - { - mergePopup.open(); - mergeSelection.clear(); - mergeReference = *selection.begin(); - }; - - auto merge_quick = [&]() - { - auto behavior = [&]() - { - int merged{}; - if (selection.contains(overlayIndex)) overlayIndex = -1; - - if (selection.size() > 1) - merged = anm2.animations_merge(*selection.begin(), selection); - else if (selection.size() == 1 && *selection.begin() != (int)anm2.animations.items.size() - 1) - { - auto start = *selection.begin(); - auto next = *selection.begin() + 1; - std::set animationSet{}; - animationSet.insert(start); - animationSet.insert(next); - - merged = anm2.animations_merge(start, animationSet); - } - else - return; - - selection = {merged}; - reference = {merged}; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_ANIMATIONS), Document::ANIMATIONS, behavior()); - }; - - auto default_set = [&]() - { - DOCUMENT_EDIT(document, localize.get(EDIT_DEFAULT_ANIMATION), Document::ANIMATIONS, - anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name); - }; - - auto copy = [&]() - { - if (selection.empty()) return; - - std::string clipboardText{}; - for (auto& i : selection) - clipboardText += anm2.animations.items[i].to_string(); - clipboard.set(clipboardText); - }; - - auto cut = [&]() - { - copy(); - DOCUMENT_EDIT(document, localize.get(EDIT_CUT_ANIMATIONS), Document::ANIMATIONS, remove()); - }; - - auto paste = [&]() - { - if (clipboard.is_empty()) return; - - auto behavior = [&]() - { - auto clipboardText = clipboard.get(); - auto start = selection.empty() ? anm2.animations.items.size() : *selection.rbegin() + 1; - std::set indices{}; - std::string errorString{}; - if (anm2.animations_deserialize(clipboardText, start, indices, &errorString)) - { - if (!indices.empty()) - { - auto index = *indices.rbegin(); - selection = {index}; - reference = {index}; - newAnimationSelectedIndex = index; - } - } - else - { - toasts.push( - std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED), std::make_format_args(errorString))); - logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED, anm2ed::ENGLISH), - std::make_format_args(errorString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_ANIMATIONS), Document::ANIMATIONS, behavior()); - }; - - if (ImGui::Begin(localize.get(LABEL_ANIMATIONS_WINDOW), &settings.windowIsAnimations)) - { - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::IsKeyPressed(ImGuiKey_Escape)) - reference = {}; - - auto childSize = size_without_footer_get(); - - if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders)) - { - - selection.start(anm2.animations.items.size()); - - for (auto [i, animation] : std::views::enumerate(anm2.animations.items)) - { - ImGui::PushID((int)i); - - auto isDefault = anm2.animations.defaultAnimation == animation.name; - auto isReferenced = reference.animationIndex == (int)i; - auto isNewAnimation = newAnimationSelectedIndex == (int)i; - - auto font = isDefault && isReferenced ? font::BOLD_ITALICS - : isDefault ? font::BOLD - : isReferenced ? font::ITALICS - : font::REGULAR; - - ImGui::PushFont(resources.fonts[font].get(), font::SIZE); - ImGui::SetNextItemSelectionUserData((int)i); - - if (isNewAnimation || renameQueued == i) - { - renameState = RENAME_FORCE_EDIT; - renameQueued = -1; - } - if (selectable_input_text(animation.name, rename_format_get(i), animation.name, selection.contains((int)i), - ImGuiSelectableFlags_None, renameState)) - { - reference = {(int)i}; - document.frames.clear(); - - if (renameState == RENAME_BEGIN) - document.snapshot(localize.get(SNAPSHOT_RENAME_ANIMATION)); - else if (renameState == RENAME_FINISHED) - { - if (anm2.animations.items.size() == 1) anm2.animations.defaultAnimation = animation.name; - document.change(Document::ANIMATIONS); - } - } - - if (isNewAnimation) - { - isUpdateScroll = true; - newAnimationSelectedIndex = -1; - } - ImGui::PopFont(); - - if (isUpdateScroll && isReferenced) - { - ImGui::SetScrollHereY(0.5f); - isUpdateScroll = false; - } - - if (ImGui::BeginItemTooltip()) - { - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(animation.name.c_str()); - ImGui::PopFont(); - - if (isDefault) - { - ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); - ImGui::TextUnformatted(localize.get(BASIC_DEFAULT)); - ImGui::PopFont(); - } - - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_LENGTH), std::make_format_args(animation.frameNum)).c_str()); - auto loopLabel = animation.isLoop ? localize.get(BASIC_YES) : localize.get(BASIC_NO); - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_LOOP), std::make_format_args(loopLabel)).c_str()); - - ImGui::EndTooltip(); - } - - if (ImGui::BeginDragDropSource()) - { - static std::vector dragDropSelection{}; - dragDropSelection.assign(selection.begin(), selection.end()); - ImGui::SetDragDropPayload("Animation Drag Drop", dragDropSelection.data(), - dragDropSelection.size() * sizeof(int)); - for (auto& i : dragDropSelection) - ImGui::Text("%s", anm2.animations.items[(int)i].name.c_str()); - ImGui::EndDragDropSource(); - } - - if (ImGui::BeginDragDropTarget()) - { - if (auto payload = ImGui::AcceptDragDropPayload("Animation Drag Drop")) - { - auto payloadIndices = (int*)(payload->Data); - auto payloadCount = payload->DataSize / sizeof(int); - std::vector indices(payloadIndices, payloadIndices + payloadCount); - std::sort(indices.begin(), indices.end()); - DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_ANIMATIONS), Document::ANIMATIONS, - selection = vector::move_indices(anm2.animations.items, indices, i)); - } - ImGui::EndDragDropTarget(); - } - - ImGui::PopID(); - } - - selection.finish(); - - if (shortcut(manager.chords[SHORTCUT_RENAME], shortcut::FOCUSED)) rename(); - if (shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED)) merge_quick(); - if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut(); - if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); - if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); - - if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_RENAME), settings.shortcutRename.c_str(), false, - selection.size() == 1)) - rename(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ADD), settings.shortcutAdd.c_str())) add(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_DUPLICATE), settings.shortcutDuplicate.c_str())) duplicate(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_MERGE), settings.shortcutMerge.c_str())) merge_quick(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REMOVE), settings.shortcutRemove.c_str())) remove(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_DEFAULT), settings.shortcutDefault.c_str(), false, - selection.size() == 1)) - default_set(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, !selection.empty())) cut(); - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) - copy(); - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - ImGui::EndPopup(); - } - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(5); - - shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_ANIMATION), settings.shortcutAdd); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.empty()); - shortcut(manager.chords[SHORTCUT_DUPLICATE]); - if (ImGui::Button(localize.get(BASIC_DUPLICATE), widgetSize)) duplicate(); - ImGui::EndDisabled(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_DUPLICATE_ANIMATION), settings.shortcutDuplicate); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.size() != 1); - if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize)) merge_popup_open(); - ImGui::EndDisabled(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_OPEN_MERGE_POPUP), settings.shortcutMerge); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.empty()); - shortcut(manager.chords[SHORTCUT_REMOVE]); - if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize)) remove(); - ImGui::EndDisabled(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_ANIMATION), settings.shortcutRemove); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.size() != 1); - shortcut(manager.chords[SHORTCUT_DEFAULT]); - if (ImGui::Button(localize.get(BASIC_DEFAULT), widgetSize)) default_set(); - ImGui::EndDisabled(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_SET_DEFAULT_ANIMATION), settings.shortcutDefault); - - mergePopup.trigger(); - - if (ImGui::BeginPopupModal(mergePopup.label(), &mergePopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto close = [&]() - { - mergeSelection.clear(); - mergePopup.close(); - }; - - auto& type = settings.mergeType; - auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter; - - 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)); - - if (ImGui::BeginChild(localize.get(LABEL_ANIMATIONS_CHILD), animationsSize, ImGuiChildFlags_Borders)) - { - mergeSelection.start(anm2.animations.items.size()); - - for (int i = 0; i < (int)anm2.animations.items.size(); i++) - { - if (i == mergeReference) continue; - - auto& animation = anm2.animations.items[i]; - - ImGui::PushID(i); - - ImGui::SetNextItemSelectionUserData(i); - ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i)); - - ImGui::PopID(); - } - - mergeSelection.finish(); - } - ImGui::EndChild(); - - if (ImGui::BeginChild("##Merge Options", optionsSize, ImGuiChildFlags_Borders)) - { - auto size = ImVec2(optionsSize.x * 0.5f, optionsSize.y - ImGui::GetStyle().WindowPadding.y * 2); - - if (ImGui::BeginChild("##Merge Options 1", size)) - { - ImGui::RadioButton(localize.get(LABEL_APPEND_FRAMES), &type, merge::APPEND); - ImGui::RadioButton(localize.get(LABEL_PREPEND_FRAMES), &type, merge::PREPEND); - } - ImGui::EndChild(); - - ImGui::SameLine(); - - if (ImGui::BeginChild("##Merge Options 2", size)) - { - ImGui::RadioButton(localize.get(LABEL_REPLACE_FRAMES), &type, merge::REPLACE); - ImGui::RadioButton(localize.get(LABEL_IGNORE_FRAMES), &type, merge::IGNORE); - } - ImGui::EndChild(); - } - ImGui::EndChild(); - - if (ImGui::BeginChild("##Merge Delete After", deleteAfterSize, ImGuiChildFlags_Borders)) - ImGui::Checkbox(localize.get(LABEL_DELETE_ANIMATIONS_AFTER), &isDeleteAnimationsAfter); - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - ImGui::BeginDisabled(mergeSelection.empty()); - if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize)) - { - merge(); - close(); - } - ImGui::EndDisabled(); - - ImGui::SameLine(); - if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) close(); - - ImGui::EndPopup(); - } - } - ImGui::End(); - - auto isNextAnimation = shortcut(manager.chords[SHORTCUT_NEXT_ANIMATION], shortcut::GLOBAL); - auto isPreviousAnimation = shortcut(manager.chords[SHORTCUT_PREVIOUS_ANIMATION], shortcut::GLOBAL); - - if ((isPreviousAnimation || isNextAnimation) && !anm2.animations.items.empty()) - { - if (isPreviousAnimation) - reference.animationIndex = glm::clamp(--reference.animationIndex, 0, (int)anm2.animations.items.size() - 1); - if (isNextAnimation) - reference.animationIndex = glm::clamp(++reference.animationIndex, 0, (int)anm2.animations.items.size() - 1); - - selection = {reference.animationIndex}; - isUpdateScroll = true; - } - } -} diff --git a/src/imgui/window/animations.hpp b/src/imgui/window/animations.hpp deleted file mode 100644 index 10fa2ff..0000000 --- a/src/imgui/window/animations.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "clipboard.hpp" -#include "manager.hpp" -#include "resources.hpp" -#include "settings.hpp" -#include "strings.hpp" - -namespace anm2ed::imgui -{ - class Animations - { - PopupHelper mergePopup{PopupHelper(LABEL_ANIMATIONS_MERGE_POPUP)}; - int newAnimationSelectedIndex{-1}; - int renameQueued{-1}; - bool isInContextMenu{}; - bool isUpdateScroll{}; - RenameState renameState{RENAME_SELECTABLE}; - - public: - void update(Manager&, Settings&, Resources&, Clipboard&); - }; -} diff --git a/src/imgui/window/events.cpp b/src/imgui/window/events.cpp deleted file mode 100644 index 8a99134..0000000 --- a/src/imgui/window/events.cpp +++ /dev/null @@ -1,207 +0,0 @@ -#include "events.hpp" - -#include -#include - -#include "log.hpp" -#include "map_.hpp" -#include "strings.hpp" -#include "toast.hpp" - -using namespace anm2ed::util; -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& reference = document.event.reference; - auto& selection = document.event.selection; - - auto rename_format_get = [&](int id) { return std::format("###Document #{} Event #{}", manager.selected, id); }; - auto rename = [&]() - { - if (!selection.empty()) renameQueued = *selection.begin(); - }; - - auto add = [&]() - { - auto behavior = [&]() - { - auto id = map::next_id_get(anm2.content.events); - anm2::Event event{}; - event.name = localize.get(TEXT_NEW_EVENT); - anm2.content.events[id] = event; - selection = {id}; - reference = {id}; - newEventId = id; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_ADD_EVENT), Document::EVENTS, behavior()); - }; - - auto remove_unused = [&]() - { - auto unused = anm2.events_unused(); - if (unused.empty()) return; - - auto behavior = [&]() - { - for (auto& id : unused) - { - for (auto& animation : anm2.animations.items) - for (auto& trigger : animation.triggers.frames) - if (trigger.eventID == id) trigger.eventID = -1; - - anm2.content.events.erase(id); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_EVENTS), Document::EVENTS, behavior()); - }; - - auto copy = [&]() - { - if (selection.empty()) return; - - std::string clipboardText{}; - for (auto& id : selection) - clipboardText += anm2.content.events[id].to_string(id); - clipboard.set(clipboardText); - }; - - auto paste = [&]() - { - if (clipboard.is_empty()) return; - - auto behavior = [&]() - { - auto maxEventIdBefore = anm2.content.events.empty() ? -1 : anm2.content.events.rbegin()->first; - std::string errorString{}; - document.snapshot(localize.get(EDIT_PASTE_EVENTS)); - if (anm2.events_deserialize(clipboard.get(), merge::APPEND, &errorString)) - { - if (!anm2.content.events.empty()) - { - auto maxEventIdAfter = anm2.content.events.rbegin()->first; - if (maxEventIdAfter > maxEventIdBefore) - { - newEventId = maxEventIdAfter; - selection = {maxEventIdAfter}; - reference = maxEventIdAfter; - } - } - document.change(Document::EVENTS); - } - else - { - toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_EVENTS_FAILED), std::make_format_args(errorString))); - logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_EVENTS_FAILED, anm2ed::ENGLISH), - std::make_format_args(errorString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_EVENTS), Document::EVENTS, behavior()); - }; - - if (ImGui::Begin(localize.get(LABEL_EVENTS_WINDOW), &settings.windowIsEvents)) - { - auto childSize = size_without_footer_get(); - - if (ImGui::BeginChild("##Events Child", childSize, true)) - { - selection.start(anm2.content.events.size()); - - for (auto& [id, event] : anm2.content.events) - { - auto isNewEvent = (newEventId == id); - - ImGui::PushID(id); - ImGui::SetNextItemSelectionUserData(id); - if (isNewEvent || renameQueued == id) - { - renameState = RENAME_FORCE_EDIT; - renameQueued = -1; - } - if (selectable_input_text(event.name, rename_format_get(id), event.name, selection.contains(id), - ImGuiSelectableFlags_None, renameState)) - { - if (renameState == RENAME_BEGIN) - document.snapshot(localize.get(EDIT_RENAME_EVENT)); - else if (renameState == RENAME_FINISHED) - document.change(Document::EVENTS); - } - - if (isNewEvent) - { - ImGui::SetScrollHereY(0.5f); - newEventId = -1; - } - - if (ImGui::BeginItemTooltip()) - { - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(event.name.c_str()); - ImGui::PopFont(); - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); - ImGui::EndTooltip(); - } - ImGui::PopID(); - } - - selection.finish(); - - if (shortcut(manager.chords[SHORTCUT_RENAME], shortcut::FOCUSED)) rename(); - if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add(); - if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused(); - if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); - if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); - - if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_RENAME), settings.shortcutRename.c_str(), false, - selection.size() == 1)) - rename(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ADD), settings.shortcutAdd.c_str())) add(); - if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) - copy(); - - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - - ImGui::EndPopup(); - } - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_EVENT), settings.shortcutAdd); - ImGui::SameLine(); - - shortcut(manager.chords[SHORTCUT_REMOVE]); - if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_EVENTS), settings.shortcutRemove); - } - ImGui::End(); - } -} diff --git a/src/imgui/window/events.hpp b/src/imgui/window/events.hpp deleted file mode 100644 index 710d284..0000000 --- a/src/imgui/window/events.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "clipboard.hpp" -#include "manager.hpp" -#include "resources.hpp" -#include "settings.hpp" - -namespace anm2ed::imgui -{ - class Events - { - int newEventId{-1}; - int renameQueued{-1}; - RenameState renameState{RENAME_SELECTABLE}; - - public: - void update(Manager&, Settings&, Resources&, Clipboard&); - }; -} diff --git a/src/imgui/window/frame_properties.cpp b/src/imgui/window/frame_properties.cpp index 0758251..dc9a8a2 100644 --- a/src/imgui/window/frame_properties.cpp +++ b/src/imgui/window/frame_properties.cpp @@ -1,13 +1,14 @@ #include "frame_properties.hpp" -#include #include #include +#include #include -#include "math_.hpp" +#include "math.hpp" #include "strings.hpp" #include "types.hpp" +#include "util/imgui/imgui.hpp" using namespace anm2ed::util::math; using namespace anm2ed::types; @@ -17,11 +18,70 @@ namespace anm2ed::imgui { void FrameProperties::update(Manager& manager, Settings& settings) { - if (ImGui::Begin(localize.get(LABEL_FRAME_PROPERTIES_WINDOW), &settings.windowIsFrameProperties)) + auto& document = *manager.get(); + auto frameSelectionCount = + document.frames.references.empty() ? document.frames.selection.size() : document.frames.references.size(); + auto isSingleFrameSelection = frameSelectionCount == 1; + auto isMultiFrameSelection = frameSelectionCount > 1; + auto isSingleFrameBatchMode = + isSingleFrameSelection && document.reference.itemType != TRIGGER && isBatchMode; + auto isBatchFrameProperties = isMultiFrameSelection || isSingleFrameBatchMode; + auto windowLabel = std::string(localize.get(isBatchFrameProperties ? LABEL_CHANGE_ALL_FRAME_PROPERTIES + : LABEL_FRAME_PROPERTIES_WINDOW)); + if (isBatchFrameProperties) windowLabel += "###Frame Properties"; + + if (ImGui::Begin(windowLabel.c_str(), &settings.windowIsFrameProperties)) { - auto& document = *manager.get(); - auto& frames = document.frames.selection; - auto& type = document.reference.itemType; + auto& anm2 = document.anm2; + auto& reference = document.reference; + auto& type = reference.itemType; + auto itemType = static_cast(type); + auto frame = anm2.element_get(reference.animationIndex, itemType, reference.frameIndex, reference.itemID); + + auto frame_edit = [&](auto message, auto behavior) + { + auto queuedReference = reference; + manager.command_push({manager.selected, + [=](Manager&, Document& document) mutable + { + auto itemType = static_cast(queuedReference.itemType); + auto frame = document.anm2.element_get(queuedReference.animationIndex, itemType, + queuedReference.frameIndex, + queuedReference.itemID); + auto item = + document.anm2.element_get(queuedReference.animationIndex, itemType, + queuedReference.itemID); + if (!frame) return; + + document.snapshot(localize.get(message)); + behavior(document, *frame, item, queuedReference); + document.anm2_change(Document::FRAMES); + }}); + }; + + auto persistent_edit = [&](auto state, auto message, auto behavior) + { + if (state == edit::NONE) return; + + auto queuedReference = reference; + manager.command_push({manager.selected, + [=](Manager&, Document& document) mutable + { + auto itemType = static_cast(queuedReference.itemType); + auto frame = document.anm2.element_get(queuedReference.animationIndex, itemType, + queuedReference.frameIndex, + queuedReference.itemID); + auto item = + document.anm2.element_get(queuedReference.animationIndex, itemType, + queuedReference.itemID); + if (!frame) return; + + if (state == edit::START) document.snapshot(localize.get(message)); + behavior(document, *frame, item, queuedReference); + if (state == edit::END) document.anm2_change(Document::FRAMES); + }}); + }; + auto regionLabelsString = std::vector{localize.get(BASIC_NONE)}; auto regionLabels = std::vector{regionLabelsString[0].c_str()}; auto regionIds = std::vector{-1}; @@ -33,17 +93,14 @@ namespace anm2ed::imgui interpolationLabelsString[2].c_str(), interpolationLabelsString[3].c_str(), interpolationLabelsString[4].c_str()}; auto interpolationValues = - std::vector{anm2::Frame::Interpolation::NONE, anm2::Frame::Interpolation::LINEAR, - anm2::Frame::Interpolation::EASE_IN, anm2::Frame::Interpolation::EASE_OUT, - anm2::Frame::Interpolation::EASE_IN_OUT}; + std::vector{(int)Interpolation::NONE, (int)Interpolation::LINEAR, (int)Interpolation::EASE_IN, + (int)Interpolation::EASE_OUT, (int)Interpolation::EASE_IN_OUT}; - if (type == anm2::LAYER && document.reference.itemID != -1) + if (type == LAYER && reference.itemID != -1) { - if (auto layerIt = document.anm2.content.layers.find(document.reference.itemID); - layerIt != document.anm2.content.layers.end()) + if (auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID)) { - auto spritesheetID = layerIt->second.spritesheetID; - auto regionIt = document.regionBySpritesheet.find(spritesheetID); + auto regionIt = document.regionBySpritesheet.find(layer->spritesheetId); if (regionIt != document.regionBySpritesheet.end() && !regionIt->second.ids.empty() && !regionIt->second.labels.empty()) { @@ -53,37 +110,70 @@ namespace anm2ed::imgui } } - if (frames.size() <= 1) + auto mode_selector_draw = [&]() { - auto frame = document.frame_get(); - auto useFrame = frame ? *frame : anm2::Frame(); - auto displayFrame = frame && type == anm2::LAYER && document.reference.itemID != -1 - ? document.anm2.frame_effective(document.reference.itemID, *frame) + if (!isSingleFrameSelection || type == TRIGGER) return; + ImGui::SeparatorText(localize.get(BASIC_MODE)); + int mode = isBatchMode ? 1 : 0; + ImGui::RadioButton(localize.get(BASIC_SINGLE), &mode, 0); + ImGui::SameLine(); + ImGui::RadioButton(localize.get(BASIC_BATCH), &mode, 1); + isBatchMode = mode == 1; + }; + + if (isSingleFrameBatchMode) + { + changeAllFrameProperties.update(manager, document, settings); + mode_selector_draw(); + } + else if (!isMultiFrameSelection) + { + auto useFrame = frame ? *frame : Element(); + auto displayFrame = frame && type == LAYER && reference.itemID != -1 + ? anm2.frame_effective(reference.itemID, *frame) : useFrame; ImGui::BeginDisabled(!frame); { - if (type == anm2::TRIGGER) + if (type == TRIGGER) { - if (combo_id_mapped(localize.get(BASIC_EVENT), frame ? &useFrame.eventID : &dummy_value_negative(), + if (combo_id_mapped(localize.get(BASIC_EVENT), frame ? &useFrame.eventId : &dummy_value_negative(), document.event.ids, document.event.labels) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_EVENT), Document::FRAMES, - frame->eventID = useFrame.eventID); + { + auto eventId = useFrame.eventId; + frame_edit(EDIT_TRIGGER_EVENT, + [eventId](Document&, Element& frame, Element*, const Reference&) { frame.eventId = eventId; }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_EVENT)); if (input_int_range(localize.get(BASIC_AT_FRAME), frame ? useFrame.atFrame : dummy_value(), 0, std::numeric_limits::max(), STEP, STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_AT_FRAME), Document::FRAMES, - frame->atFrame = useFrame.atFrame); + { + auto atFrame = useFrame.atFrame; + frame_edit(EDIT_TRIGGER_AT_FRAME, + [atFrame](Document& document, Element& frame, Element* item, const Reference&) + { + frame.atFrame = atFrame; + if (!item) return; + frames_sort_by_at_frame(*item); + document.reference.frameIndex = frame_index_from_at_frame_get(*item, atFrame); + document.frames.selection = {document.reference.frameIndex}; + document.frames.references = {document.reference}; + }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_AT_FRAME)); if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value()) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_VISIBILITY), Document::FRAMES, - frame->isVisible = useFrame.isVisible); + { + auto isVisible = useFrame.isVisible; + frame_edit(EDIT_TRIGGER_VISIBILITY, + [isVisible](Document&, Element& frame, Element*, const Reference&) + { frame.isVisible = isVisible; }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_VISIBILITY)); ImGui::SeparatorText(localize.get(LABEL_SOUNDS)); @@ -92,16 +182,23 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Sounds Child", childSize, ImGuiChildFlags_Borders)) { - if (!useFrame.soundIDs.empty()) + if (!useFrame.soundIds.empty()) { - for (auto [i, id] : std::views::enumerate(useFrame.soundIDs)) + for (auto [i, id] : std::views::enumerate(useFrame.soundIds)) { ImGui::PushID(i); if (combo_id_mapped("##Sound", frame ? &id : &dummy_value_negative(), document.sound.ids, document.sound.labels) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_SOUND), Document::FRAMES, - frame->soundIDs[i] = id); + { + auto soundIndex = (std::size_t)i; + auto soundId = id; + frame_edit(EDIT_TRIGGER_SOUND, + [soundIndex, soundId](Document&, Element& frame, Element*, const Reference&) + { + if (soundIndex < frame.soundIds.size()) frame.soundIds[soundIndex] = soundId; + }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_SOUND)); ImGui::PopID(); } @@ -112,125 +209,127 @@ namespace anm2ed::imgui auto widgetSize = imgui::widget_size_with_row_get(2); if (ImGui::Button(localize.get(BASIC_ADD), widgetSize) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_ADD_TRIGGER_SOUND), Document::FRAMES, - frame->soundIDs.push_back(-1)); + frame_edit(EDIT_ADD_TRIGGER_SOUND, + [](Document&, Element& frame, Element*, const Reference&) { frame.soundIds.push_back(-1); }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_TRIGGER_SOUND)); ImGui::SameLine(); - ImGui::BeginDisabled(useFrame.soundIDs.empty()); + ImGui::BeginDisabled(useFrame.soundIds.empty()); if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_TRIGGER_SOUND), Document::FRAMES, - frame->soundIDs.pop_back()); + frame_edit(EDIT_REMOVE_TRIGGER_SOUND, + [](Document&, Element& frame, Element*, const Reference&) + { + if (!frame.soundIds.empty()) frame.soundIds.pop_back(); + }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REMOVE_TRIGGER_SOUND)); ImGui::EndDisabled(); } else { - bool isRegionSet = frame && displayFrame.regionID != -1 && displayFrame.crop == frame->crop && + bool isRegionSet = frame && displayFrame.regionId != -1 && displayFrame.crop == frame->crop && displayFrame.size == frame->size && displayFrame.pivot == frame->pivot; - ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet); + ImGui::BeginDisabled(type == ROOT || type == NULL_ || isRegionSet); { auto cropDisplay = frame ? displayFrame.crop : vec2(); auto cropEdit = drag_float2_persistent(localize.get(BASIC_CROP), frame ? &cropDisplay : &dummy_value(), DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.crop) : ""); - if (cropEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_CROP)); - if (frame && cropEdit != edit::NONE) frame->crop = cropDisplay; - if (cropEdit == edit::END) - document.change(Document::FRAMES); + persistent_edit(cropEdit, EDIT_FRAME_CROP, + [cropDisplay](Document&, Element& frame, Element*, const Reference&) + { frame.crop = cropDisplay; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CROP)); auto sizeDisplay = frame ? displayFrame.size : vec2(); auto sizeEdit = drag_float2_persistent(localize.get(BASIC_SIZE), frame ? &sizeDisplay : &dummy_value(), DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.size) : ""); - if (sizeEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_SIZE)); - if (frame && sizeEdit != edit::NONE) frame->size = sizeDisplay; - if (sizeEdit == edit::END) - document.change(Document::FRAMES); + persistent_edit(sizeEdit, EDIT_FRAME_SIZE, + [sizeDisplay](Document&, Element& frame, Element*, const Reference&) + { frame.size = sizeDisplay; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SIZE)); } ImGui::EndDisabled(); auto positionEdit = - drag_float2_persistent(localize.get(BASIC_POSITION), frame ? &frame->position : &dummy_value(), + drag_float2_persistent(localize.get(BASIC_POSITION), + frame ? &useFrame.position : &dummy_value(), DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->position) : ""); - if (positionEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_POSITION)); - else if (positionEdit == edit::END) - document.change(Document::FRAMES); + persistent_edit(positionEdit, EDIT_FRAME_POSITION, + [position = useFrame.position](Document&, Element& frame, Element*, const Reference&) + { frame.position = position; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_POSITION)); - ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet); + ImGui::BeginDisabled(type == ROOT || type == NULL_ || isRegionSet); { auto pivotDisplay = frame ? displayFrame.pivot : vec2(); auto pivotEdit = drag_float2_persistent(localize.get(BASIC_PIVOT), frame ? &pivotDisplay : &dummy_value(), DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.pivot) : ""); - if (pivotEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_PIVOT)); - if (frame && pivotEdit != edit::NONE) frame->pivot = pivotDisplay; - if (pivotEdit == edit::END) - document.change(Document::FRAMES); + persistent_edit(pivotEdit, EDIT_FRAME_PIVOT, + [pivotDisplay](Document&, Element& frame, Element*, const Reference&) + { frame.pivot = pivotDisplay; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PIVOT)); } ImGui::EndDisabled(); auto scaleEdit = - drag_float2_persistent(localize.get(BASIC_SCALE), frame ? &frame->scale : &dummy_value(), + drag_float2_persistent(localize.get(BASIC_SCALE), frame ? &useFrame.scale : &dummy_value(), DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->scale) : ""); + persistent_edit(scaleEdit, EDIT_FRAME_SCALE, + [scale = useFrame.scale](Document&, Element& frame, Element*, const Reference&) + { frame.scale = scale; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE)); - if (scaleEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_SCALE)); - else if (scaleEdit == edit::END) - document.change(Document::FRAMES); auto rotationEdit = - drag_float_persistent(localize.get(BASIC_ROTATION), frame ? &frame->rotation : &dummy_value(), + drag_float_persistent(localize.get(BASIC_ROTATION), frame ? &useFrame.rotation : &dummy_value(), DRAG_SPEED, 0.0f, 0.0f, frame ? float_format_get(frame->rotation) : ""); + persistent_edit(rotationEdit, EDIT_FRAME_ROTATION, + [rotation = useFrame.rotation](Document&, Element& frame, Element*, const Reference&) + { frame.rotation = rotation; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROTATION)); - if (rotationEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_ROTATION)); - else if (rotationEdit == edit::END) - document.change(Document::FRAMES); if (input_int_range(localize.get(BASIC_DURATION), frame ? useFrame.duration : dummy_value(), - frame ? anm2::FRAME_DURATION_MIN : 0, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST, + frame ? FRAME_DURATION_MIN : 0, FRAME_DURATION_MAX, STEP, STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_DURATION), Document::FRAMES, - frame->duration = useFrame.duration); + { + auto duration = useFrame.duration; + frame_edit(EDIT_FRAME_DURATION, + [duration](Document&, Element& frame, Element*, const Reference&) { frame.duration = duration; }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DURATION)); auto tintEdit = - color_edit4_persistent(localize.get(BASIC_TINT), frame ? &frame->tint : &dummy_value()); + color_edit4_persistent(localize.get(BASIC_TINT), frame ? &useFrame.tint : &dummy_value()); + persistent_edit(tintEdit, EDIT_FRAME_TINT, + [tint = useFrame.tint](Document&, Element& frame, Element*, const Reference&) + { frame.tint = tint; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TINT)); - if (tintEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_TINT)); - else if (tintEdit == edit::END) - document.change(Document::FRAMES); auto colorOffsetEdit = color_edit3_persistent(localize.get(BASIC_COLOR_OFFSET), - frame ? &frame->colorOffset : &dummy_value()); + frame ? &useFrame.colorOffset : &dummy_value()); + persistent_edit(colorOffsetEdit, EDIT_FRAME_COLOR_OFFSET, + [colorOffset = useFrame.colorOffset](Document&, Element& frame, Element*, const Reference&) + { frame.colorOffset = colorOffset; }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COLOR_OFFSET)); - if (colorOffsetEdit == edit::START) - document.snapshot(localize.get(EDIT_FRAME_COLOR_OFFSET)); - else if (colorOffsetEdit == edit::END) - document.change(Document::FRAMES); - ImGui::BeginDisabled(type != anm2::LAYER); - if (combo_id_mapped(localize.get(BASIC_REGION), frame ? &useFrame.regionID : &dummy_value_negative(), - regionIds, regionLabels) && + ImGui::BeginDisabled(type != LAYER); + if (combo_id_mapped(localize.get(BASIC_REGION), frame ? &useFrame.regionId : &dummy_value_negative(), + regionIds, regionLabels) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_SET_REGION_PROPERTIES), Document::FRAMES, - frame->regionID = useFrame.regionID; - auto effectiveFrame = document.anm2.frame_effective(document.reference.itemID, *frame); - frame->crop = effectiveFrame.crop; - frame->size = effectiveFrame.size; - frame->pivot = effectiveFrame.pivot); + { + auto regionId = useFrame.regionId; + frame_edit(EDIT_SET_REGION_PROPERTIES, + [regionId](Document& document, Element& frame, Element*, const Reference& reference) + { + frame.regionId = regionId; + auto effectiveFrame = document.anm2.frame_effective(reference.itemID, frame); + frame.crop = effectiveFrame.crop; + frame.size = effectiveFrame.size; + frame.pivot = effectiveFrame.pivot; + }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION)); ImGui::EndDisabled(); @@ -238,14 +337,19 @@ namespace anm2ed::imgui if (combo_id_mapped(localize.get(BASIC_INTERPOLATED), &interpolationValue, interpolationValues, interpolationLabels) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_INTERPOLATION), Document::FRAMES, - frame->interpolation = static_cast(interpolationValue)); + frame_edit(EDIT_FRAME_INTERPOLATION, + [interpolationValue](Document&, Element& frame, Element*, const Reference&) + { frame.interpolation = static_cast(interpolationValue); }); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FRAME_INTERPOLATION)); if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value()) && frame) - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_VISIBILITY), Document::FRAMES, - frame->isVisible = useFrame.isVisible); + { + auto isVisible = useFrame.isVisible; + frame_edit(EDIT_FRAME_VISIBILITY, + [isVisible](Document&, Element& frame, Element*, const Reference&) + { frame.isVisible = isVisible; }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FRAME_VISIBILITY)); auto widgetSize = widget_size_with_row_get(2); @@ -253,40 +357,40 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(LABEL_FLIP_X), widgetSize) && frame) { if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) - { - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_X), Document::FRAMES, - frame->scale.x = -frame->scale.x; - frame->position.x = -frame->position.x); - } + frame_edit(EDIT_FRAME_FLIP_X, + [](Document&, Element& frame, Element*, const Reference&) + { + frame.scale.x = -frame.scale.x; + frame.position.x = -frame.position.x; + }); else - { - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_X), Document::FRAMES, - frame->scale.x = -frame->scale.x); - } + frame_edit(EDIT_FRAME_FLIP_X, + [](Document&, Element& frame, Element*, const Reference&) { frame.scale.x = -frame.scale.x; }); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FLIP_X)); ImGui::SameLine(); if (ImGui::Button(localize.get(LABEL_FLIP_Y), widgetSize) && frame) { if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) - { - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_Y), Document::FRAMES, - frame->scale.y = -frame->scale.y; - frame->position.y = -frame->position.y); - } + frame_edit(EDIT_FRAME_FLIP_Y, + [](Document&, Element& frame, Element*, const Reference&) + { + frame.scale.y = -frame.scale.y; + frame.position.y = -frame.position.y; + }); else - { - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_Y), Document::FRAMES, - frame->scale.y = -frame->scale.y); - } + frame_edit(EDIT_FRAME_FLIP_Y, + [](Document&, Element& frame, Element*, const Reference&) { frame.scale.y = -frame.scale.y; }); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FLIP_Y)); } } ImGui::EndDisabled(); + + mode_selector_draw(); } else - changeAllFrameProperties.update(document, settings); + changeAllFrameProperties.update(manager, document, settings); } ImGui::End(); diff --git a/src/imgui/window/frame_properties.hpp b/src/imgui/window/frame_properties.hpp index 3ff82b7..29ae294 100644 --- a/src/imgui/window/frame_properties.hpp +++ b/src/imgui/window/frame_properties.hpp @@ -10,6 +10,7 @@ namespace anm2ed::imgui class FrameProperties { wizard::ChangeAllFrameProperties changeAllFrameProperties{}; + bool isBatchMode{}; public: void update(Manager&, Settings&); diff --git a/src/imgui/window/layers.cpp b/src/imgui/window/layers.cpp deleted file mode 100644 index e59235f..0000000 --- a/src/imgui/window/layers.cpp +++ /dev/null @@ -1,235 +0,0 @@ -#include "layers.hpp" - -#include -#include - -#include "log.hpp" -#include "map_.hpp" -#include "strings.hpp" -#include "toast.hpp" - -using namespace anm2ed::util; -using namespace anm2ed::resource; -using namespace anm2ed::types; - -namespace anm2ed::imgui -{ - void Layers::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) - { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& reference = document.layer.reference; - auto& selection = document.layer.selection; - auto& propertiesPopup = manager.layerPropertiesPopup; - - auto add = [&]() { manager.layer_properties_open(); }; - - auto remove_unused = [&]() - { - auto unused = anm2.layers_unused(); - if (unused.empty()) return; - - auto behavior = [&]() - { - for (auto& id : unused) - anm2.content.layers.erase(id); - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_LAYERS), Document::LAYERS, behavior()); - }; - - auto copy = [&]() - { - if (selection.empty()) return; - - std::string clipboardText{}; - for (auto& id : selection) - clipboardText += anm2.content.layers[id].to_string(id); - clipboard.set(clipboardText); - }; - - auto paste = [&]() - { - if (clipboard.is_empty()) return; - - auto behavior = [&]() - { - auto maxLayerIdBefore = anm2.content.layers.empty() ? -1 : anm2.content.layers.rbegin()->first; - std::string errorString{}; - document.snapshot(localize.get(EDIT_PASTE_LAYERS)); - if (anm2.layers_deserialize(clipboard.get(), merge::APPEND, &errorString)) - { - if (!anm2.content.layers.empty()) - { - auto maxLayerIdAfter = anm2.content.layers.rbegin()->first; - if (maxLayerIdAfter > maxLayerIdBefore) - { - newLayerId = maxLayerIdAfter; - selection = {maxLayerIdAfter}; - reference = maxLayerIdAfter; - } - } - document.change(Document::NULLS); - } - else - { - toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_LAYERS_FAILED), std::make_format_args(errorString))); - logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_LAYERS_FAILED, anm2ed::ENGLISH), - std::make_format_args(errorString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_LAYERS), Document::LAYERS, behavior()); - }; - - auto properties = [&](int id) { manager.layer_properties_open(id); }; - - if (ImGui::Begin(localize.get(LABEL_LAYERS_WINDOW), &settings.windowIsLayers)) - { - auto childSize = size_without_footer_get(); - - if (ImGui::BeginChild("##Layers Child", childSize, true)) - { - selection.start(anm2.content.layers.size()); - - for (auto& [id, layer] : anm2.content.layers) - { - auto isSelected = selection.contains(id); - - ImGui::PushID(id); - - ImGui::SetNextItemSelectionUserData(id); - ImGui::Selectable( - std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, layer.name, layer.spritesheetID)) - .c_str(), - isSelected); - if (newLayerId == id) - { - ImGui::SetScrollHereY(0.5f); - newLayerId = -1; - } - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) properties(id); - - if (ImGui::BeginItemTooltip()) - { - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(layer.name.c_str()); - ImGui::PopFont(); - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer.spritesheetID)).c_str()); - ImGui::EndTooltip(); - } - ImGui::PopID(); - } - - selection.finish(); - - if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add(); - if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused(); - if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); - if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); - - if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1)) - properties(*selection.begin()); - if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add(); - if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) - copy(); - - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - - ImGui::EndPopup(); - } - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_LAYER), settings.shortcutAdd); - ImGui::SameLine(); - - shortcut(manager.chords[SHORTCUT_REMOVE]); - if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_LAYERS), settings.shortcutRemove); - } - ImGui::End(); - - manager.layer_properties_trigger(); - - if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto childSize = child_size_get(2); - auto& layer = manager.editLayer; - - if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders)) - { - if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); - input_text_string(localize.get(BASIC_NAME), &layer.name); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); - - combo_id_mapped(localize.get(LABEL_SPRITESHEET), &layer.spritesheetID, document.spritesheet.ids, - document.spritesheet.labels); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET)); - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_CONFIRM]); - if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) - { - if (reference == -1) - { - auto layer_add = [&]() - { - auto id = map::next_id_get(anm2.content.layers); - anm2.content.layers[id] = layer; - selection = {id}; - newLayerId = id; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_ADD_LAYER), Document::LAYERS, layer_add()); - } - else - { - auto layer_set = [&]() - { - anm2.content.layers[reference] = layer; - selection = {reference}; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_SET_LAYER_PROPERTIES), Document::LAYERS, layer_set()); - } - - manager.layer_properties_close(); - } - - ImGui::SameLine(); - - shortcut(manager.chords[SHORTCUT_CANCEL]); - if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.layer_properties_close(); - - manager.layer_properties_end(); - ImGui::EndPopup(); - } - } -} diff --git a/src/imgui/window/layers.hpp b/src/imgui/window/layers.hpp deleted file mode 100644 index 4e190c5..0000000 --- a/src/imgui/window/layers.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "clipboard.hpp" -#include "manager.hpp" -#include "resources.hpp" -#include "settings.hpp" - -namespace anm2ed::imgui -{ - class Layers - { - int newLayerId{-1}; - - public: - void update(Manager&, Settings&, Resources&, Clipboard&); - }; -} diff --git a/src/imgui/window/nulls.cpp b/src/imgui/window/nulls.cpp deleted file mode 100644 index c93ae9a..0000000 --- a/src/imgui/window/nulls.cpp +++ /dev/null @@ -1,233 +0,0 @@ -#include "nulls.hpp" - -#include -#include - -#include "log.hpp" -#include "map_.hpp" -#include "strings.hpp" -#include "toast.hpp" - -using namespace anm2ed::resource; -using namespace anm2ed::util; -using namespace anm2ed::types; - -namespace anm2ed::imgui -{ - void Nulls::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) - { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& reference = document.null.reference; - auto& selection = document.null.selection; - auto& propertiesPopup = manager.nullPropertiesPopup; - - auto add = [&]() { manager.null_properties_open(); }; - - auto remove_unused = [&]() - { - auto unused = anm2.nulls_unused(); - if (unused.empty()) return; - auto behavior = [&]() - { - for (auto& id : unused) - anm2.content.nulls.erase(id); - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_NULLS), Document::NULLS, behavior()); - }; - - auto copy = [&]() - { - if (selection.empty()) return; - - std::string clipboardText{}; - for (auto& id : selection) - clipboardText += anm2.content.nulls[id].to_string(id); - clipboard.set(clipboardText); - }; - - auto paste = [&]() - { - if (clipboard.is_empty()) return; - - auto behavior = [&]() - { - auto maxNullIdBefore = anm2.content.nulls.empty() ? -1 : anm2.content.nulls.rbegin()->first; - std::string errorString{}; - document.snapshot(localize.get(EDIT_PASTE_NULLS)); - if (anm2.nulls_deserialize(clipboard.get(), merge::APPEND, &errorString)) - { - if (!anm2.content.nulls.empty()) - { - auto maxNullIdAfter = anm2.content.nulls.rbegin()->first; - if (maxNullIdAfter > maxNullIdBefore) - { - newNullId = maxNullIdAfter; - selection = {maxNullIdAfter}; - reference = maxNullIdAfter; - } - } - document.change(Document::NULLS); - } - else - { - toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_NULLS_FAILED), std::make_format_args(errorString))); - logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_NULLS_FAILED, anm2ed::ENGLISH), - std::make_format_args(errorString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_NULLS), Document::NULLS, behavior()); - }; - - auto properties = [&](int id) { manager.null_properties_open(id); }; - - if (ImGui::Begin(localize.get(LABEL_NULLS_WINDOW), &settings.windowIsNulls)) - { - auto childSize = size_without_footer_get(); - - if (ImGui::BeginChild("##Nulls Child", childSize, true)) - { - selection.start(anm2.content.nulls.size()); - - for (auto& [id, null] : anm2.content.nulls) - { - auto isSelected = selection.contains(id); - auto isReferenced = reference == id; - - ImGui::PushID(id); - ImGui::SetNextItemSelectionUserData(id); - if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); - ImGui::Selectable(std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name)).c_str(), - isSelected); - if (newNullId == id) - { - ImGui::SetScrollHereY(0.5f); - newNullId = -1; - } - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) properties(id); - - if (isReferenced) ImGui::PopFont(); - - if (ImGui::BeginItemTooltip()) - { - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(null.name.c_str()); - ImGui::PopFont(); - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); - ImGui::EndTooltip(); - } - ImGui::PopID(); - } - - selection.finish(); - - if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add(); - if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused(); - if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); - if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); - - if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1)) - properties(*selection.begin()); - if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add(); - if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) - copy(); - - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - - ImGui::EndPopup(); - } - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_NULL), settings.shortcutAdd); - ImGui::SameLine(); - - shortcut(manager.chords[SHORTCUT_REMOVE]); - if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_NULLS), settings.shortcutRemove); - } - ImGui::End(); - - manager.null_properties_trigger(); - - if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto childSize = child_size_get(2); - auto& null = manager.editNull; - - if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders)) - { - if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); - input_text_string(localize.get(BASIC_NAME), &null.name); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_NAME)); - - ImGui::Checkbox(localize.get(LABEL_RECT), &null.isShowRect); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_RECT)); - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_CONFIRM]); - if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) - { - if (reference == -1) - { - auto null_add = [&]() - { - auto id = map::next_id_get(anm2.content.nulls); - anm2.content.nulls[id] = null; - selection = {id}; - newNullId = id; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_ADD_NULL), Document::NULLS, null_add()); - } - else - { - auto null_set = [&]() - { - anm2.content.nulls[reference] = null; - selection = {reference}; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_SET_NULL_PROPERTIES), Document::NULLS, null_set()); - } - - manager.null_properties_close(); - } - - ImGui::SameLine(); - - shortcut(manager.chords[SHORTCUT_CANCEL]); - if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.null_properties_close(); - - ImGui::EndPopup(); - } - - manager.null_properties_end(); - } -} diff --git a/src/imgui/window/nulls.hpp b/src/imgui/window/nulls.hpp deleted file mode 100644 index ceb32fe..0000000 --- a/src/imgui/window/nulls.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "clipboard.hpp" -#include "manager.hpp" -#include "resources.hpp" -#include "settings.hpp" - -namespace anm2ed::imgui -{ - class Nulls - { - int newNullId{-1}; - - public: - void update(Manager&, Settings&, Resources&, Clipboard&); - }; -} diff --git a/src/imgui/window/onionskin.cpp b/src/imgui/window/onionskin.cpp index 944cd12..76982a0 100644 --- a/src/imgui/window/onionskin.cpp +++ b/src/imgui/window/onionskin.cpp @@ -2,7 +2,7 @@ #include -#include "imgui_.hpp" +#include "util/imgui/imgui.hpp" #include "strings.hpp" using namespace anm2ed::types; diff --git a/src/imgui/window/regions.cpp b/src/imgui/window/regions.cpp deleted file mode 100644 index 07863a5..0000000 --- a/src/imgui/window/regions.cpp +++ /dev/null @@ -1,605 +0,0 @@ -#include "regions.hpp" - -#include -#include - -#include - -#include "document.hpp" -#include "log.hpp" -#include "map_.hpp" -#include "math_.hpp" -#include "strings.hpp" -#include "toast.hpp" -#include "vector_.hpp" - -#include "../../util/map_.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::resource; -using namespace anm2ed::util; -using namespace glm; - -namespace anm2ed::imgui -{ - void Regions::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) - { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& selection = document.region.selection; - auto& frame = document.frames; - auto& spritesheetReference = document.spritesheet.reference; - auto& reference = document.region.reference; - auto style = ImGui::GetStyle(); - - auto spritesheet = map::find(anm2.content.spritesheets, spritesheetReference); - - if (manager.isMakeRegionRequested) - { - spritesheetReference = manager.makeRegionSpritesheetId; - reference = -1; - editRegion = manager.makeRegion; - isPreserveEditRegionOnOpen = true; - propertiesPopup.open(); - manager.isMakeRegionRequested = false; - spritesheet = map::find(anm2.content.spritesheets, spritesheetReference); - } - - auto remove_unused = [&]() - { - if (!spritesheet) return; - - auto unused = anm2.regions_unused(*spritesheet); - if (unused.empty()) return; - - auto behavior = [&]() - { - for (auto& id : unused) - { - for (auto& animation : anm2.animations.items) - for (auto& layerAnimation : animation.layerAnimations | std::views::values) - for (auto& frame : layerAnimation.frames) - if (frame.regionID == id) frame.regionID = -1; - - spritesheet->regions.erase(id); - auto& order = spritesheet->regionOrder; - order.erase(std::remove(order.begin(), order.end(), id), order.end()); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_REGIONS), Document::SPRITESHEETS, behavior()); - }; - - auto trim = [&]() - { - if (!spritesheet || selection.empty()) return; - - auto behavior = [&]() - { - if (anm2.regions_trim(spritesheetReference, selection)) - { - if (reference != -1 && !selection.contains(reference)) reference = *selection.begin(); - document.reference = {document.reference.animationIndex}; - frame.reference = -1; - frame.selection.clear(); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_TRIM_REGIONS), Document::SPRITESHEETS, behavior()); - }; - - auto copy = [&]() - { - if (!spritesheet || selection.empty()) return; - - std::string clipboardText{}; - for (auto& id : selection) - clipboardText += spritesheet->region_to_string(id); - clipboard.set(clipboardText); - }; - - auto paste = [&]() - { - if (!spritesheet || clipboard.is_empty()) return; - - auto behavior = [&]() - { - auto maxRegionIdBefore = spritesheet->regions.empty() ? -1 : spritesheet->regions.rbegin()->first; - std::string errorString{}; - document.snapshot(localize.get(EDIT_PASTE_REGIONS)); - if (spritesheet->regions_deserialize(clipboard.get(), merge::APPEND, &errorString)) - { - if (!spritesheet->regions.empty()) - { - auto maxRegionIdAfter = spritesheet->regions.rbegin()->first; - if (maxRegionIdAfter > maxRegionIdBefore) - { - newRegionId = maxRegionIdAfter; - selection = {maxRegionIdAfter}; - reference = maxRegionIdAfter; - } - } - document.change(Document::SPRITESHEETS); - } - else - { - toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED), std::make_format_args(errorString))); - logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED, anm2ed::ENGLISH), - std::make_format_args(errorString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_REGIONS), Document::SPRITESHEETS, behavior()); - }; - - auto add_open = [&]() - { - reference = -1; - editRegion = anm2::Spritesheet::Region{}; - propertiesPopup.open(); - }; - - auto properties_open = [&](int id) - { - if (!spritesheet || !spritesheet->regions.contains(id)) return; - reference = id; - propertiesPopup.open(); - }; - - auto context_menu = [&]() - { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && - ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - ImGui::OpenPopup("##Region Context Menu"); - - if (ImGui::BeginPopup("##Region Context Menu")) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1)) - properties_open(*selection.begin()); - if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open(); - if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); - if (ImGui::MenuItem(localize.get(BASIC_TRIM), nullptr, false, !selection.empty())) trim(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIM_REGIONS)); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy(); - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - - ImGui::EndPopup(); - } - ImGui::PopStyleVar(2); - }; - - if (ImGui::Begin(localize.get(LABEL_REGIONS_WINDOW), &settings.windowIsRegions)) - { - if (!spritesheet) - { - ImGui::TextUnformatted(localize.get(TEXT_SELECT_SPRITESHEET)); - ImGui::End(); - return; - } - - auto childSize = size_without_footer_get(); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); - if (ImGui::BeginChild("##Regions Child", childSize, ImGuiChildFlags_Borders)) - { - auto regionChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); - - auto rebuild_order = [&]() - { - spritesheet->regionOrder.clear(); - spritesheet->regionOrder.reserve(spritesheet->regions.size()); - for (auto id : spritesheet->regions | std::views::keys) - spritesheet->regionOrder.push_back(id); - }; - if (spritesheet->regionOrder.size() != spritesheet->regions.size()) - rebuild_order(); - else - { - bool isOrderValid = true; - for (auto id : spritesheet->regionOrder) - if (!spritesheet->regions.contains(id)) - { - isOrderValid = false; - break; - } - if (!isOrderValid) rebuild_order(); - } - - selection.set_index_map(&spritesheet->regionOrder); - selection.start(spritesheet->regionOrder.size()); - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) - { - selection.clear(); - for (auto& id : spritesheet->regionOrder) - selection.insert(id); - } - if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear(); - auto scroll_to_item = [&](float itemHeight, bool isTarget) - { - if (!isTarget) return; - auto windowHeight = ImGui::GetWindowHeight(); - auto targetTop = ImGui::GetCursorPosY(); - auto targetBottom = targetTop + itemHeight; - auto visibleTop = ImGui::GetScrollY(); - auto visibleBottom = visibleTop + windowHeight; - if (targetTop < visibleTop) - ImGui::SetScrollY(targetTop); - else if (targetBottom > visibleBottom) - ImGui::SetScrollY(targetBottom - windowHeight); - }; - int scrollTargetId = -1; - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - (ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true))) - { - auto& order = spritesheet->regionOrder; - if (!order.empty()) - { - int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1; - int current = reference; - if (current == -1 && !selection.empty()) current = *selection.begin(); - auto it = std::find(order.begin(), order.end(), current); - int index = it == order.end() ? 0 : (int)std::distance(order.begin(), it); - index = std::clamp(index + delta, 0, (int)order.size() - 1); - int nextId = order[index]; - selection = {nextId}; - reference = nextId; - document.reference = {document.reference.animationIndex}; - frame.reference = -1; - frame.selection.clear(); - scrollTargetId = nextId; - } - } - bool isValid = spritesheet->is_valid(); - auto& texture = isValid ? spritesheet->texture : resources.icons[icon::NONE]; - auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - static std::vector dragDropSelection{}; - bool isRegionDragDropSourceSubmitted = false; - - for (int i = 0; i < (int)spritesheet->regionOrder.size(); i++) - { - int id = spritesheet->regionOrder[i]; - auto regionIt = spritesheet->regions.find(id); - if (regionIt == spritesheet->regions.end()) continue; - auto& region = regionIt->second; - auto isNewRegion = newRegionId == id; - auto nameCStr = region.name.c_str(); - auto isSelected = selection.contains(id); - auto isReferenced = id == reference; - - ImGui::PushID(id); - - scroll_to_item(regionChildSize.y, scrollTargetId == id); - - if (ImGui::BeginChild("##Region Child", regionChildSize, ImGuiChildFlags_Borders)) - { - auto cursorPos = ImGui::GetCursorPos(); - - ImGui::SetNextItemSelectionUserData(i); - ImGui::SetNextItemStorageID(id); - if (ImGui::Selectable("##Region Selectable", isSelected, 0, regionChildSize)) - { - reference = id; - document.reference = {document.reference.animationIndex}; - frame.reference = -1; - frame.selection.clear(); - } - if (scrollTargetId == id) ImGui::SetItemDefaultFocus(); - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) propertiesPopup.open(); - - auto viewport = ImGui::GetMainViewport(); - auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f; - vec2 regionSize = glm::max(region.size, vec2(1.0f)); - vec2 previewSize = regionSize; - if (previewSize.x > maxPreviewSize.x || previewSize.y > maxPreviewSize.y) - { - auto scale = glm::min(maxPreviewSize.x / previewSize.x, maxPreviewSize.y / previewSize.y); - previewSize *= scale; - } - vec2 uvMin{}; - vec2 uvMax{1.0f, 1.0f}; - if (isValid) - { - uvMin = region.crop / vec2(texture.size); - uvMax = (region.crop + region.size) / vec2(texture.size); - } - - auto textWidth = ImGui::CalcTextSize(nameCStr).x; - auto tooltipPadding = style.WindowPadding.x * 4.0f; - auto minWidth = previewSize.x + style.ItemSpacing.x + textWidth + tooltipPadding; - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && !ImGui::IsMouseDragging(ImGuiMouseButton_Left)) - { - ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing); - if (ImGui::BeginItemTooltip()) - { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); - auto childFlags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; - auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; - - if (ImGui::BeginChild("##Region Tooltip Image Child", to_imvec2(previewSize), childFlags, - noScrollFlags)) - ImGui::ImageWithBg(texture.id, to_imvec2(previewSize), to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(), - tintColor); - ImGui::EndChild(); - ImGui::PopStyleVar(); - - ImGui::SameLine(); - - auto infoChildFlags = ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; - if (ImGui::BeginChild("##Region Info Tooltip Child", ImVec2(), infoChildFlags, noScrollFlags)) - { - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(nameCStr); - ImGui::PopFont(); - - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region.crop.x, region.crop.y)) - .c_str()); - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y)) - .c_str()); - if (region.origin == anm2::Spritesheet::Region::CUSTOM) - { - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y)) - .c_str()); - } - else - { - StringType originString = LABEL_REGION_ORIGIN_CENTER; - if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) - originString = LABEL_REGION_ORIGIN_TOP_LEFT; - auto originLabel = localize.get(originString); - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str()); - } - } - ImGui::EndChild(); - ImGui::EndTooltip(); - } - } - ImGui::PopStyleVar(2); - - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) - { - isRegionDragDropSourceSubmitted = true; - if (selection.contains(id)) - dragDropSelection.assign(selection.begin(), selection.end()); - else - dragDropSelection = {id}; - - ImGui::SetDragDropPayload("Region Drag Drop", dragDropSelection.data(), - dragDropSelection.size() * sizeof(int)); - ImGui::EndDragDropSource(); - } - - if (ImGui::BeginDragDropTarget()) - { - if (auto payload = ImGui::AcceptDragDropPayload("Region Drag Drop")) - { - auto payloadIds = (int*)(payload->Data); - int payloadCount = (int)(payload->DataSize / sizeof(int)); - std::vector indices{}; - indices.reserve(payloadCount); - for (int payloadIndex = 0; payloadIndex < payloadCount; payloadIndex++) - { - int payloadId = payloadIds[payloadIndex]; - int index = vector::find_index(spritesheet->regionOrder, payloadId); - if (index != -1) indices.push_back(index); - } - if (!indices.empty()) - { - std::sort(indices.begin(), indices.end()); - DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_REGIONS), Document::SPRITESHEETS, - vector::move_indices(spritesheet->regionOrder, indices, i)); - } - } - ImGui::EndDragDropTarget(); - } - - auto imageSize = to_imvec2(vec2(regionChildSize.y)); - auto aspectRatio = region.size.y != 0.0f ? (float)region.size.x / region.size.y : 1.0f; - - if (imageSize.x / imageSize.y > aspectRatio) - imageSize.x = imageSize.y * aspectRatio; - else - imageSize.y = imageSize.x / aspectRatio; - - ImGui::SetCursorPos(cursorPos); - ImGui::ImageWithBg(texture.id, imageSize, to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(), tintColor); - - ImGui::SetCursorPos(ImVec2(regionChildSize.y + style.ItemSpacing.x, - regionChildSize.y - regionChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); - - if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); - ImGui::TextUnformatted(nameCStr); - if (isReferenced) ImGui::PopFont(); - } - - ImGui::EndChild(); - - if (isNewRegion) - { - ImGui::SetScrollHereY(0.5f); - newRegionId = -1; - } - - ImGui::PopID(); - } - - auto regionDragPayload = ImGui::GetDragDropPayload(); - bool isRegionDragActive = regionDragPayload && regionDragPayload->IsDataType("Region Drag Drop"); - if (isRegionDragActive && !isRegionDragDropSourceSubmitted && !dragDropSelection.empty() && - ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceExtern | ImGuiDragDropFlags_SourceNoPreviewTooltip)) - { - ImGui::SetDragDropPayload("Region Drag Drop", dragDropSelection.data(), - dragDropSelection.size() * sizeof(int)); - ImGui::EndDragDropSource(); - } - - if (isRegionDragActive && !dragDropSelection.empty()) - { - auto mousePos = ImGui::GetIO().MousePos; - auto tooltipOffset = ImVec2(16.0f, 16.0f); - ImGui::SetNextWindowPos(ImVec2(mousePos.x + tooltipOffset.x, mousePos.y + tooltipOffset.y)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - if (ImGui::BeginTooltip()) - { - for (auto regionId : dragDropSelection) - { - auto dragIt = spritesheet->regions.find(regionId); - if (dragIt == spritesheet->regions.end()) continue; - ImGui::TextUnformatted(dragIt->second.name.c_str()); - } - ImGui::EndTooltip(); - } - ImGui::PopStyleVar(2); - } - - ImGui::PopStyleVar(); - selection.finish(); - - if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open(); - if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused(); - if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); - if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - - context_menu(); - - auto rowOneWidgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) add_open(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_REGION), settings.shortcutAdd); - - ImGui::SameLine(); - - shortcut(manager.chords[SHORTCUT_REMOVE]); - if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowOneWidgetSize)) remove_unused(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_REGIONS), settings.shortcutAdd); - } - - ImGui::End(); - - propertiesPopup.trigger(); - - if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) - { - if (!spritesheet || (reference != -1 && !spritesheet->regions.contains(reference))) - { - propertiesPopup.close(); - ImGui::EndPopup(); - return; - } - - auto childSize = child_size_get(5); - auto& region = reference == -1 ? editRegion : spritesheet->regions.at(reference); - - if (propertiesPopup.isJustOpened) - { - if (!isPreserveEditRegionOnOpen) - editRegion = anm2::Spritesheet::Region{}; - isPreserveEditRegionOnOpen = false; - } - - if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders)) - { - const char* originOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT), - localize.get(LABEL_REGION_ORIGIN_CENTER), - localize.get(LABEL_REGION_ORIGIN_CUSTOM)}; - - if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); - input_text_string(localize.get(BASIC_NAME), ®ion.name); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); - ImGui::DragFloat2(localize.get(BASIC_CROP), value_ptr(region.crop), DRAG_SPEED, 0.0f, 0.0f, - math::vec2_format_get(region.crop)); - ImGui::DragFloat2(localize.get(BASIC_SIZE), value_ptr(region.size), DRAG_SPEED, 0.0f, 0.0f, - math::vec2_format_get(region.size)); - ImGui::BeginDisabled(region.origin != anm2::Spritesheet::Region::CUSTOM); - ImGui::DragFloat2(localize.get(BASIC_PIVOT), value_ptr(region.pivot), DRAG_SPEED, 0.0f, 0.0f, - math::vec2_format_get(region.pivot)); - ImGui::EndDisabled(); - - if (ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), (int*)®ion.origin, originOptions, - IM_ARRAYSIZE(originOptions))) - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION_PROPERTIES_ORIGIN)); - - if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) - region.pivot = {}; - else if (region.origin == anm2::Spritesheet::Region::ORIGIN_CENTER) - region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)}; - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_CONFIRM]); - if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) - { - if (reference == -1) - { - auto add = [&]() - { - auto id = map::next_id_get(spritesheet->regions); - spritesheet->regions[id] = region; - spritesheet->regionOrder.push_back(id); - selection = {id}; - newRegionId = id; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_ADD_REGION), Document::SPRITESHEETS, add()); - } - else - { - auto set = [&]() - { - spritesheet->regions.at(reference) = region; - selection = {reference}; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_SET_REGION_PROPERTIES), Document::SPRITESHEETS, set()); - } - - frame.reference = -1; - frame.selection.clear(); - - propertiesPopup.close(); - } - - ImGui::SameLine(); - - shortcut(manager.chords[SHORTCUT_CANCEL]); - if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) propertiesPopup.close(); - - ImGui::EndPopup(); - } - - propertiesPopup.end(); - } -} diff --git a/src/imgui/window/regions.hpp b/src/imgui/window/regions.hpp deleted file mode 100644 index 49d75df..0000000 --- a/src/imgui/window/regions.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "clipboard.hpp" -#include "manager.hpp" -#include "resources.hpp" -#include "settings.hpp" - -namespace anm2ed::imgui -{ - class Regions - { - public: - anm2::Spritesheet::Region editRegion{}; - int newRegionId{-1}; - bool isPreserveEditRegionOnOpen{}; - imgui::PopupHelper propertiesPopup{imgui::PopupHelper(LABEL_REGION_PROPERTIES, imgui::POPUP_SMALL_NO_HEIGHT)}; - - void update(Manager&, Settings&, Resources&, Clipboard&); - }; -} diff --git a/src/imgui/window/sounds.cpp b/src/imgui/window/sounds.cpp deleted file mode 100644 index 5021ae4..0000000 --- a/src/imgui/window/sounds.cpp +++ /dev/null @@ -1,391 +0,0 @@ -#include "sounds.hpp" - -#include -#include -#include -#include - -#include "log.hpp" -#include "path_.hpp" -#include "strings.hpp" -#include "toast.hpp" - -using namespace anm2ed::util; -using namespace anm2ed::types; -using namespace anm2ed::resource; -using namespace glm; - -namespace anm2ed::imgui -{ - void Sounds::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, Clipboard& clipboard) - { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& reference = document.sound.reference; - auto& selection = document.sound.selection; - auto style = ImGui::GetStyle(); - - auto add_open = [&]() { dialog.file_open(Dialog::SOUND_OPEN); }; - auto replace_open = [&]() { dialog.file_open(Dialog::SOUND_REPLACE); }; - - auto play = [&](anm2::Sound& sound) { sound.play(); }; - - auto add = [&](const std::filesystem::path& path) - { - auto behavior = [&]() - { - int id{}; - auto pathString = path::to_utf8(path); - if (anm2.sound_add(document.directory_get(), path, id)) - { - selection = {id}; - newSoundId = id; - toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, pathString))); - logger.info(std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - } - else - { - toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathString))); - logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH), - std::make_format_args(pathString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_ADD_SOUND), Document::SOUNDS, behavior()); - }; - - auto remove_unused = [&]() - { - auto unused = anm2.sounds_unused(); - if (unused.empty()) return; - - auto behavior = [&]() - { - for (auto& id : unused) - anm2.content.sounds.erase(id); - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SOUNDS), Document::SOUNDS, behavior()); - }; - - auto reload = [&]() - { - auto behavior = [&]() - { - for (auto& id : selection) - { - anm2::Sound& sound = anm2.content.sounds[id]; - sound.reload(document.directory_get()); - auto pathString = path::to_utf8(sound.path); - toasts.push(std::vformat(localize.get(TOAST_RELOAD_SOUND), std::make_format_args(id, pathString))); - logger.info( - std::vformat(localize.get(TOAST_RELOAD_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_RELOAD_SOUNDS), Document::SOUNDS, behavior()); - }; - - auto replace = [&](const std::filesystem::path& path) - { - if (selection.size() != 1 || path.empty()) return; - - auto behavior = [&]() - { - auto& id = *selection.begin(); - anm2::Sound& sound = anm2.content.sounds[id]; - sound = anm2::Sound(document.directory_get(), path); - auto pathString = path::to_utf8(sound.path); - toasts.push(std::vformat(localize.get(TOAST_REPLACE_SOUND), std::make_format_args(id, pathString))); - logger.info( - std::vformat(localize.get(TOAST_REPLACE_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString))); - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SOUND), Document::SOUNDS, behavior()); - }; - - auto open_directory = [&](anm2::Sound& sound) - { - if (sound.path.empty()) return; - std::error_code ec{}; - auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / sound.path, ec); - if (ec) absolutePath = document.directory_get() / sound.path; - auto target = std::filesystem::is_directory(absolutePath) ? absolutePath - : std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path() - : document.directory_get(); - dialog.file_explorer_open(target); - }; - - auto copy = [&]() - { - if (selection.empty()) return; - - std::string clipboardText{}; - for (auto& id : selection) - clipboardText += anm2.content.sounds[id].to_string(id); - clipboard.set(clipboardText); - }; - - auto paste = [&]() - { - if (clipboard.is_empty()) return; - - auto behavior = [&]() - { - auto maxSoundIdBefore = anm2.content.sounds.empty() ? -1 : anm2.content.sounds.rbegin()->first; - std::string errorString{}; - document.snapshot(localize.get(TOAST_SOUNDS_PASTE)); - if (anm2.sounds_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString)) - { - if (!anm2.content.sounds.empty()) - { - auto maxSoundIdAfter = anm2.content.sounds.rbegin()->first; - if (maxSoundIdAfter > maxSoundIdBefore) - { - newSoundId = maxSoundIdAfter; - selection = {maxSoundIdAfter}; - reference = maxSoundIdAfter; - } - } - document.change(Document::SOUNDS); - } - else - { - toasts.push(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR), std::make_format_args(errorString))); - logger.error(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR, anm2ed::ENGLISH), - std::make_format_args(errorString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_SOUNDS), Document::SOUNDS, behavior()); - }; - - auto context_menu = [&]() - { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && - ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - ImGui::OpenPopup("##Sound Context Menu"); - - if (ImGui::BeginPopup("##Sound Context Menu")) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(LABEL_PLAY), nullptr, false, selection.size() == 1)) - play(anm2.content.sounds[*selection.begin()]); - if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1)) - open_directory(anm2.content.sounds[*selection.begin()]); - - if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open(); - if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); - - if (ImGui::MenuItem(localize.get(BASIC_RELOAD), nullptr, false, !selection.empty())) reload(); - if (ImGui::MenuItem(localize.get(BASIC_REPLACE), nullptr, false, selection.size() == 1)) replace_open(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy(); - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - - ImGui::EndPopup(); - } - ImGui::PopStyleVar(2); - }; - - if (ImGui::Begin(localize.get(LABEL_SOUNDS_WINDOW), &settings.windowIsSounds)) - { - auto childSize = imgui::size_without_footer_get(); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); - if (ImGui::BeginChild("##Sounds Child", childSize, ImGuiChildFlags_Borders)) - { - auto soundChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); - - selection.start(anm2.content.sounds.size()); - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) - { - selection.clear(); - for (auto& id : anm2.content.sounds | std::views::keys) - selection.insert(id); - } - if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear(); - auto scroll_to_item = [&](float itemHeight, bool isTarget) - { - if (!isTarget) return; - auto windowHeight = ImGui::GetWindowHeight(); - auto targetTop = ImGui::GetCursorPosY(); - auto targetBottom = targetTop + itemHeight; - auto visibleTop = ImGui::GetScrollY(); - auto visibleBottom = visibleTop + windowHeight; - if (targetTop < visibleTop) - ImGui::SetScrollY(targetTop); - else if (targetBottom > visibleBottom) - ImGui::SetScrollY(targetBottom - windowHeight); - }; - int scrollTargetId = -1; - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - (ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true))) - { - std::vector ids{}; - ids.reserve(anm2.content.sounds.size()); - for (auto& [id, sound] : anm2.content.sounds) - ids.push_back(id); - if (!ids.empty()) - { - int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1; - int current = reference; - if (current == -1 && !selection.empty()) current = *selection.begin(); - auto it = std::find(ids.begin(), ids.end(), current); - int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it); - index = std::clamp(index + delta, 0, (int)ids.size() - 1); - int nextId = ids[index]; - selection = {nextId}; - reference = nextId; - scrollTargetId = nextId; - } - } - - for (auto& [id, sound] : anm2.content.sounds) - { - auto isNewSound = newSoundId == id; - ImGui::PushID(id); - - scroll_to_item(soundChildSize.y, scrollTargetId == id); - - if (ImGui::BeginChild("##Sound Child", soundChildSize, ImGuiChildFlags_Borders)) - { - auto isSelected = selection.contains(id); - auto cursorPos = ImGui::GetCursorPos(); - bool isValid = sound.is_valid(); - auto& soundIcon = isValid ? resources.icons[icon::SOUND] : resources.icons[icon::NONE]; - auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - auto pathString = path::to_utf8(sound.path); - - ImGui::SetNextItemSelectionUserData(id); - ImGui::SetNextItemStorageID(id); - if (ImGui::Selectable("##Sound Selectable", isSelected, 0, soundChildSize)) - { - reference = id; - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) play(sound); - } - if (scrollTargetId == id) ImGui::SetItemDefaultFocus(); - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) open_directory(sound); - - auto textWidth = ImGui::CalcTextSize(pathString.c_str()).x; - auto tooltipPadding = style.WindowPadding.x * 4.0f; - auto minWidth = textWidth + style.ItemSpacing.x + tooltipPadding; - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing); - if (ImGui::BeginItemTooltip()) - { - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(pathString.c_str()); - ImGui::PopFont(); - ImGui::Text("%s: %d", localize.get(BASIC_ID), id); - if (!isValid) - { - ImGui::Spacing(); - ImGui::TextWrapped("%s", localize.get(TOOLTIP_SOUND_INVALID)); - } - else - { - ImGui::Text("%s", localize.get(TEXT_SOUND_PLAY)); - ImGui::Text("%s", localize.get(TEXT_OPEN_DIRECTORY)); - } - ImGui::EndTooltip(); - } - ImGui::PopStyleVar(2); - - ImGui::SetCursorPos(cursorPos); - auto imageSize = to_imvec2(vec2(soundChildSize.y)); - ImGui::ImageWithBg(soundIcon.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor); - - ImGui::SetCursorPos(ImVec2(soundChildSize.y + style.ItemSpacing.x, - soundChildSize.y - soundChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); - - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString)).c_str()); - } - - ImGui::EndChild(); - - if (isNewSound) - { - ImGui::SetScrollHereY(0.5f); - newSoundId = -1; - } - - ImGui::PopID(); - } - - ImGui::PopStyleVar(); - selection.finish(); - } - - ImGui::EndChild(); - ImGui::PopStyleVar(); - - context_menu(); - - auto widgetSize = imgui::widget_size_with_row_get(4); - - imgui::shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add_open(); - imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_SOUND_ADD), settings.shortcutAdd); - - if (dialog.is_selected(Dialog::SOUND_OPEN)) - { - add(dialog.path); - dialog.reset(); - } - - ImGui::SameLine(); - - imgui::shortcut(manager.chords[SHORTCUT_REMOVE]); - if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused(); - imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SOUNDS), settings.shortcutRemove); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.empty()); - if (ImGui::Button(localize.get(BASIC_RELOAD), widgetSize)) reload(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SOUNDS)); - ImGui::EndDisabled(); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.size() != 1); - if (ImGui::Button(localize.get(BASIC_REPLACE), widgetSize)) replace_open(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SOUND)); - ImGui::EndDisabled(); - - if (dialog.is_selected(Dialog::SOUND_REPLACE)) - { - replace(dialog.path); - dialog.reset(); - } - - if (imgui::shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open(); - if (imgui::shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused(); - if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); - if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); - } - ImGui::End(); - } -} diff --git a/src/imgui/window/sounds.hpp b/src/imgui/window/sounds.hpp deleted file mode 100644 index f5ab6f1..0000000 --- a/src/imgui/window/sounds.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "clipboard.hpp" -#include "dialog.hpp" -#include "manager.hpp" -#include "resources.hpp" -#include "settings.hpp" - -namespace anm2ed::imgui -{ - class Sounds - { - int newSoundId{-1}; - - public: - void update(Manager&, Settings&, Resources&, Dialog&, Clipboard&); - }; -} diff --git a/src/imgui/window/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp index 0defad8..3ff8bed 100644 --- a/src/imgui/window/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -2,11 +2,13 @@ #include #include +#include #include -#include "imgui_.hpp" +#include "actions.hpp" +#include "util/imgui/imgui.hpp" #include "imgui_internal.h" -#include "math_.hpp" +#include "math.hpp" #include "strings.hpp" #include "tool.hpp" #include "types.hpp" @@ -20,12 +22,17 @@ namespace anm2ed::imgui { SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) {} + bool SpritesheetEditor::is_focused_get() const { return isFocused; } + void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources) { + isFocused = false; + auto& document = *manager.get(); auto& anm2 = document.anm2; auto& reference = document.reference; auto& referenceSpritesheet = document.spritesheet.reference; + auto referenceItemType = static_cast(reference.itemType); auto& pan = document.editorPan; auto& zoom = document.editorZoom; auto& backgroundColor = settings.editorBackgroundColor; @@ -38,7 +45,8 @@ namespace anm2ed::imgui auto& zoomStep = settings.inputZoomStep; auto& isBorder = settings.editorIsBorder; auto& isTransparent = settings.editorIsTransparent; - auto spritesheet = document.spritesheet_get(); + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, referenceSpritesheet); + auto texture = document.texture_get(referenceSpritesheet); auto& tool = settings.tool; auto& shaderGrid = resources.shaders[shader::GRID]; auto& shaderTexture = resources.shaders[shader::TEXTURE]; @@ -79,8 +87,8 @@ namespace anm2ed::imgui auto fit_view = [&]() { - if (spritesheet && spritesheet->texture.is_valid()) - set_to_rect(zoom, pan, {0, 0, (float)spritesheet->texture.size.x, (float)spritesheet->texture.size.y}); + if (texture && texture->is_valid()) + set_to_rect(zoom, pan, {0, 0, (float)texture->size.x, (float)texture->size.y}); }; auto zoom_adjust = [&](float delta) @@ -94,8 +102,12 @@ namespace anm2ed::imgui auto zoom_in = [&]() { zoom_adjust(zoomStep); }; auto zoom_out = [&]() { zoom_adjust(-zoomStep); }; + auto region_get = [&](int id) + { return spritesheet ? element_child_id_get(*spritesheet, ElementType::REGION, id) : nullptr; }; + if (ImGui::Begin(localize.get(LABEL_SPRITESHEET_EDITOR_WINDOW), &settings.windowIsSpritesheetEditor)) { + isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); auto childSize = ImVec2(imgui::row_widget_width_get(3), (ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2)); @@ -196,18 +208,17 @@ namespace anm2ed::imgui viewport_set(); clear(isTransparent ? vec4(0) : vec4(backgroundColor, 1.0f)); - auto frame = document.frame_get(); - auto item = document.item_get(); + auto frame = + anm2.element_get(reference.animationIndex, referenceItemType, reference.frameIndex, reference.itemID); - if (spritesheet && spritesheet->texture.is_valid()) + if (spritesheet && texture && texture->is_valid()) { - auto& texture = spritesheet->texture; auto transform = transform_get(zoom, pan); - auto spritesheetModel = math::quad_model_get(texture.size); + auto spritesheetModel = math::quad_model_get(texture->size); auto spritesheetTransform = transform * spritesheetModel; - texture_render(shaderTexture, texture.id, spritesheetTransform); + texture_render(shaderTexture, texture->id, spritesheetTransform); if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); @@ -217,32 +228,29 @@ namespace anm2ed::imgui if (hoveredRegionId != -1) { - auto regionIt = spritesheet->regions.find(hoveredRegionId); - if (regionIt != spritesheet->regions.end()) + if (auto region = region_get(hoveredRegionId)) { - auto& region = regionIt->second; - auto cropModel = math::quad_model_get(region.size, region.crop); + auto cropModel = math::quad_model_get(region->size, region->crop); auto cropTransform = transform * cropModel; rect_fill_render(shaderLine, cropTransform, cropModel, vec4(1.0f, 1.0f, 1.0f, 0.5f)); } } - auto layerIt = anm2.content.layers.find(reference.itemID); + auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID); bool isReferenceLayerOnSpritesheet = - frame && reference.itemID > -1 && layerIt != anm2.content.layers.end() && - layerIt->second.spritesheetID == referenceSpritesheet; + frame && reference.itemID > -1 && layer && layer->spritesheetId == referenceSpritesheet; int highlightedRegionId = -1; - if (isReferenceLayerOnSpritesheet && frame->regionID != -1 && spritesheet->regions.contains(frame->regionID)) + if (isReferenceLayerOnSpritesheet && frame->regionId != -1 && region_get(frame->regionId)) { - highlightedRegionId = frame->regionID; + highlightedRegionId = frame->regionId; } - else if (regionReference != -1 && spritesheet->regions.contains(regionReference)) + else if (regionReference != -1 && region_get(regionReference)) { highlightedRegionId = regionReference; } - auto draw_region_rect = [&](anm2::Spritesheet::Region& region, vec4 regionColor) + auto draw_region_rect = [&](Element& region, vec4 regionColor) { auto cropModel = math::quad_model_get(region.size, region.crop); auto cropTransform = transform * cropModel; @@ -250,8 +258,10 @@ namespace anm2ed::imgui BORDER_DASH_OFFSET); }; - for (auto& [id, region] : spritesheet->regions) + for (auto& region : spritesheet->children) { + if (region.type != ElementType::REGION) continue; + auto id = region.id; if (id == highlightedRegionId) continue; draw_region_rect(region, color::WHITE); @@ -262,26 +272,25 @@ namespace anm2ed::imgui if (highlightedRegionId != -1) { - auto regionIt = spritesheet->regions.find(highlightedRegionId); - if (regionIt != spritesheet->regions.end()) + if (auto region = region_get(highlightedRegionId)) { - auto& region = regionIt->second; - draw_region_rect(region, color::RED); + draw_region_rect(*region, color::RED); auto pivotTransform = - transform * math::quad_model_get(PIVOT_SIZE, region.crop + region.pivot, PIVOT_SIZE * 0.5f); + transform * math::quad_model_get(PIVOT_SIZE, region->crop + region->pivot, PIVOT_SIZE * 0.5f); texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR); } } bool isFrameOnSpritesheet = isReferenceLayerOnSpritesheet; - if (isFrameOnSpritesheet && frame->regionID == -1) + if (isFrameOnSpritesheet && frame->regionId == -1) { auto frameModel = math::quad_model_get(frame->size, frame->crop); auto frameTransform = transform * frameModel; rect_render(shaderLine, frameTransform, frameModel, color::RED); - auto pivotTransform = transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); + auto pivotTransform = + transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR); } } @@ -293,7 +302,7 @@ namespace anm2ed::imgui render_checker_background(drawList, min, max, -size * 0.5f - checkerPan, CHECKER_SIZE); else drawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(vec4(backgroundColor, 1.0f)))); - ImGui::Image(texture, to_imvec2(size)); + ImGui::Image(this->texture, to_imvec2(size)); if (ImGui::IsItemHovered()) { @@ -330,8 +339,8 @@ namespace anm2ed::imgui auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown; auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; - auto isZoomIn = shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL); - auto isZoomOut = shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL); + auto isZoomIn = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL); + auto isZoomOut = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL); auto isBegin = isMouseClicked || isKeyJustPressed; auto isDuring = isMouseDown || isKeyDown; @@ -339,11 +348,12 @@ namespace anm2ed::imgui auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); - auto frame = document.frame_get(); - auto layerIt = anm2.content.layers.find(reference.itemID); + auto frame = + anm2.element_get(reference.animationIndex, referenceItemType, reference.frameIndex, reference.itemID); + auto item = anm2.element_get(reference.animationIndex, referenceItemType, reference.itemID); + auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID); bool isReferenceLayerOnSpritesheet = - frame && reference.itemID > -1 && layerIt != anm2.content.layers.end() && - layerIt->second.spritesheetID == referenceSpritesheet; + frame && reference.itemID > -1 && layer && layer->spritesheetId == referenceSpritesheet; auto useTool = tool; auto step = isMod ? STEP_FAST : STEP; auto stepX = isGridSnap ? step * gridSize.x : step; @@ -373,31 +383,212 @@ namespace anm2ed::imgui return std::pair{minPoint, maxPoint}; }; - auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST) - { item->frames_change(frameChange, reference.itemType, changeType, frames); }; - auto clamp_vec2_to_int = [](const vec2& value) - { return vec2(ivec2(value)); }; + auto clamp_vec2_to_int = [](const vec2& value) { return vec2(ivec2(value)); }; + auto snapshot_push = [&](StringType messageType) + { + auto message = std::string(localize.get(messageType)); + manager.command_push( + {manager.selected, [message](Manager&, Document& document) { document.snapshot(message); }}); + }; + auto document_change_push = [&](Document::ChangeType changeType) + { + manager.command_push({manager.selected, + [changeType](Manager&, Document& document) { document.anm2_change(changeType); }}); + }; + auto frame_change_apply = [&](FrameChange frameChange, ChangeType changeType = ChangeType::ADJUST) + { + auto queuedReference = reference; + auto queuedReferenceItemType = referenceItemType; + std::set queuedFrames(frames.begin(), frames.end()); + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto item = document.anm2.element_get(queuedReference.animationIndex, + queuedReferenceItemType, + queuedReference.itemID); + if (!item) return; + frames_change(*item, frameChange, queuedReferenceItemType, changeType, queuedFrames); + }}); + }; + auto frame_change_from_current_apply = [&](auto frameChangeGet, ChangeType changeType = ChangeType::ADJUST) + { + auto queuedReference = reference; + auto queuedReferenceItemType = referenceItemType; + std::set queuedFrames(frames.begin(), frames.end()); + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto item = document.anm2.element_get(queuedReference.animationIndex, + queuedReferenceItemType, + queuedReference.itemID); + auto frame = document.anm2.element_get(queuedReference.animationIndex, + queuedReferenceItemType, + queuedReference.frameIndex, + queuedReference.itemID); + if (!item || !frame) return; + frames_change(*item, frameChangeGet(*frame), queuedReferenceItemType, changeType, + queuedFrames); + }}); + }; + auto frame_crop_normalize_apply = [&](bool isSnap, ivec2 snapGridSize, ivec2 snapGridOffset) + { + frame_change_from_current_apply( + [=](const Element& frame) + { + auto minPoint = glm::min(frame.crop, frame.crop + frame.size); + auto maxPoint = glm::max(frame.crop, frame.crop + frame.size); + + if (isSnap) + { + if (snapGridSize.x != 0) + { + auto offsetX = (float)snapGridOffset.x; + auto sizeX = (float)snapGridSize.x; + minPoint.x = std::floor((minPoint.x - offsetX) / sizeX) * sizeX + offsetX; + maxPoint.x = std::ceil((maxPoint.x - offsetX) / sizeX) * sizeX + offsetX; + } + if (snapGridSize.y != 0) + { + auto offsetY = (float)snapGridOffset.y; + auto sizeY = (float)snapGridSize.y; + minPoint.y = std::floor((minPoint.y - offsetY) / sizeY) * sizeY + offsetY; + maxPoint.y = std::ceil((maxPoint.y - offsetY) / sizeY) * sizeY + offsetY; + } + } + + return FrameChange{.cropX = minPoint.x, + .cropY = minPoint.y, + .sizeX = maxPoint.x - minPoint.x, + .sizeY = maxPoint.y - minPoint.y}; + }); + }; + auto region_update = [&](int id, auto update) + { + auto queuedSpritesheet = referenceSpritesheet; + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, + queuedSpritesheet); + if (!spritesheet) return; + auto region = element_child_id_get(*spritesheet, ElementType::REGION, id); + if (!region) return; + update(*region); + }}); + }; + auto region_update_all = [&](auto update) + { + auto queuedSpritesheet = referenceSpritesheet; + std::set queuedSelection(regionSelection.begin(), regionSelection.end()); + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, + queuedSpritesheet); + if (!spritesheet) return; + for (auto id : queuedSelection) + { + auto region = element_child_id_get(*spritesheet, ElementType::REGION, id); + if (!region) continue; + update(*region); + } + }}); + }; auto region_set_all = [&](const vec2& crop, const vec2& size) { - if (!spritesheet) return; - for (auto id : regionSelection) - { - auto it = spritesheet->regions.find(id); - if (it == spritesheet->regions.end()) continue; - it->second.crop = clamp_vec2_to_int(crop); - it->second.size = clamp_vec2_to_int(size); - } + auto queuedCrop = clamp_vec2_to_int(crop); + auto queuedSize = clamp_vec2_to_int(size); + region_update_all( + [=](Element& region) + { + region.crop = queuedCrop; + region.size = queuedSize; + }); }; auto region_offset_all = [&](const vec2& delta) { - if (!spritesheet) return; - for (auto id : regionSelection) - { - auto it = spritesheet->regions.find(id); - if (it == spritesheet->regions.end()) continue; - it->second.crop = clamp_vec2_to_int(it->second.crop + delta); - it->second.size = clamp_vec2_to_int(it->second.size); - } + region_update_all( + [=](Element& region) + { + region.crop = clamp_vec2_to_int(region.crop + delta); + region.size = clamp_vec2_to_int(region.size); + }); + }; + auto region_crop_normalize_all = [&](bool isSnap, ivec2 snapGridSize, ivec2 snapGridOffset) + { + region_update_all( + [=](Element& region) + { + auto minPoint = glm::min(region.crop, region.crop + region.size); + auto maxPoint = glm::max(region.crop, region.crop + region.size); + + if (isSnap) + { + if (snapGridSize.x != 0) + { + auto offsetX = (float)snapGridOffset.x; + auto sizeX = (float)snapGridSize.x; + minPoint.x = std::floor((minPoint.x - offsetX) / sizeX) * sizeX + offsetX; + maxPoint.x = std::ceil((maxPoint.x - offsetX) / sizeX) * sizeX + offsetX; + } + if (snapGridSize.y != 0) + { + auto offsetY = (float)snapGridOffset.y; + auto sizeY = (float)snapGridSize.y; + minPoint.y = std::floor((minPoint.y - offsetY) / sizeY) * sizeY + offsetY; + maxPoint.y = std::ceil((maxPoint.y - offsetY) / sizeY) * sizeY + offsetY; + } + } + + region.crop = clamp_vec2_to_int(minPoint); + region.size = clamp_vec2_to_int(maxPoint - minPoint); + }); + }; + auto texture_line_apply = [&](ivec2 start, ivec2 end, vec4 color) + { + auto queuedSpritesheet = referenceSpritesheet; + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto texture = document.texture_get(queuedSpritesheet); + if (!texture) return; + texture->pixel_line(start, end, color); + }}); + }; + auto texture_change_push = [&]() + { + auto queuedSpritesheet = referenceSpritesheet; + manager.command_push({manager.selected, + [=](Manager&, Document& document) { document.texture_change(queuedSpritesheet); }}); + }; + + auto region_selection_set = [&](int id) + { + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + document.region.reference = id; + document.region.selection = {id}; + }}); + }; + auto region_pivot_set = [&](int id, vec2 pivot) + { + auto queuedPivot = clamp_vec2_to_int(pivot); + region_update(id, + [=](Element& region) + { + region.origin = Origin::CUSTOM; + region.pivot = queuedPivot; + }); + }; + auto region_pivot_offset = [&](int id, vec2 delta) + { + region_update(id, + [=](Element& region) + { + region.origin = Origin::CUSTOM; + region.pivot = clamp_vec2_to_int(region.pivot + delta); + }); }; if (isMouseMiddleDown) useTool = tool::PAN; @@ -408,16 +599,17 @@ namespace anm2ed::imgui hoveredRegionId = -1; - if (useTool == tool::PAN && spritesheet && spritesheet->texture.is_valid() && isMouseOverCanvas) + if (useTool == tool::PAN && spritesheet && texture && texture->is_valid() && isMouseOverCanvas) { - for (auto& [id, region] : spritesheet->regions) + for (auto& region : spritesheet->children) { + if (region.type != ElementType::REGION) continue; auto minPoint = glm::min(region.crop, region.crop + region.size); auto maxPoint = glm::max(region.crop, region.crop + region.size); if (hoverMousePos.x >= minPoint.x && hoverMousePos.x <= maxPoint.x && hoverMousePos.y >= minPoint.y && hoverMousePos.y <= maxPoint.y) { - hoveredRegionId = id; + hoveredRegionId = region.id; break; } } @@ -428,12 +620,12 @@ namespace anm2ed::imgui bool isAreaAllowed = areaType == tool::ALL || areaType == tool::SPRITESHEET_EDITOR; bool isFrameRequired = !(useTool == tool::PAN || useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER); - bool isRegionInUse = frame && frame->regionID != -1 && (useTool == tool::CROP || useTool == tool::MOVE); + bool isRegionInUse = frame && frame->regionId != -1 && (useTool == tool::CROP || useTool == tool::MOVE); bool isFrameAvailable = !isFrameRequired || (frame && !isRegionInUse) || (useTool == tool::CROP && !frame && !regionSelection.empty()) || (useTool == tool::MOVE && !frame && regionReference != -1); bool isSpritesheetRequired = useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER; - bool isSpritesheetAvailable = !isSpritesheetRequired || (spritesheet && spritesheet->texture.is_valid()); + bool isSpritesheetAvailable = !isSpritesheetRequired || (texture && texture->is_valid()); auto cursor = (isAreaAllowed && isFrameAvailable && isSpritesheetAvailable) ? toolInfo.cursor : ImGuiMouseCursor_NotAllowed; ImGui::SetMouseCursor(cursor); @@ -444,29 +636,23 @@ namespace anm2ed::imgui case tool::PAN: if (isMouseLeftClicked && hoveredRegionId != -1) { - regionReference = hoveredRegionId; - regionSelection = {hoveredRegionId}; + region_selection_set(hoveredRegionId); if (isReferenceLayerOnSpritesheet) { - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_REGION), Document::FRAMES, - { - anm2::FrameChange change{}; - change.regionID = hoveredRegionId; - if (spritesheet) - { - auto regionIt = spritesheet->regions.find(hoveredRegionId); - if (regionIt != spritesheet->regions.end()) - { - change.cropX = regionIt->second.crop.x; - change.cropY = regionIt->second.crop.y; - change.sizeX = regionIt->second.size.x; - change.sizeY = regionIt->second.size.y; - change.pivotX = regionIt->second.pivot.x; - change.pivotY = regionIt->second.pivot.y; - } - } - frame_change_apply(change); - }); + snapshot_push(EDIT_FRAME_REGION); + FrameChange change{}; + change.regionId = hoveredRegionId; + if (auto region = region_get(hoveredRegionId)) + { + change.cropX = region->crop.x; + change.cropY = region->crop.y; + change.sizeX = region->size.x; + change.sizeY = region->size.y; + change.pivotX = region->pivot.x; + change.pivotY = region->pivot.y; + } + frame_change_apply(change); + document_change_push(Document::FRAMES); } } if (isMouseDown || isMouseMiddleDown) pan += mouseDelta; @@ -476,48 +662,48 @@ namespace anm2ed::imgui if (!frame && regionReference != -1) { if (!spritesheet) break; - auto regionIt = spritesheet->regions.find(regionReference); - if (regionIt == spritesheet->regions.end()) break; + auto region = region_get(regionReference); + if (!region) break; - auto& region = regionIt->second; - if (isBegin) document.snapshot(localize.get(EDIT_REGION_MOVE)); - bool isPivotEdited = isMouseDown || isLeftPressed || isRightPressed || isUpPressed || isDownPressed; - if (isPivotEdited) region.origin = anm2::Spritesheet::Region::CUSTOM; - if (isMouseDown) region.pivot = ivec2(mousePos) - ivec2(region.crop); - if (isLeftPressed) region.pivot.x -= step; - if (isRightPressed) region.pivot.x += step; - if (isUpPressed) region.pivot.y -= step; - if (isDownPressed) region.pivot.y += step; + if (isBegin) snapshot_push(EDIT_REGION_MOVE); + if (isMouseDown) region_pivot_set(regionReference, ivec2(mousePos) - ivec2(region->crop)); + if (isLeftPressed) region_pivot_offset(regionReference, vec2(-step, 0)); + if (isRightPressed) region_pivot_offset(regionReference, vec2(step, 0)); + if (isUpPressed) region_pivot_offset(regionReference, vec2(0, -step)); + if (isDownPressed) region_pivot_offset(regionReference, vec2(0, step)); if (isDuring) { if (ImGui::BeginTooltip()) { ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y)) + std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region->pivot.x, region->pivot.y)) .c_str()); ImGui::EndTooltip(); } } - if (isEnd) document.change(Document::SPRITESHEETS); + if (isEnd) document_change_push(Document::SPRITESHEETS); break; } if (!item || frames.empty()) break; - if (isBegin) document.snapshot(localize.get(EDIT_FRAME_PIVOT)); + if (isBegin) snapshot_push(EDIT_FRAME_PIVOT); if (isMouseDown) { - frame->crop = ivec2(frame->crop); - frame_change_apply( - {.pivotX = (int)(mousePos.x - frame->crop.x), .pivotY = (int)(mousePos.y - frame->crop.y)}); + auto frameCrop = ivec2(frame->crop); + frame_change_apply({.pivotX = (int)(mousePos.x - frameCrop.x), + .pivotY = (int)(mousePos.y - frameCrop.y), + .cropX = frameCrop.x, + .cropY = frameCrop.y}); } - if (isLeftPressed) frame_change_apply({.pivotX = step}, anm2::SUBTRACT); - if (isRightPressed) frame_change_apply({.pivotX = step}, anm2::ADD); - if (isUpPressed) frame_change_apply({.pivotY = step}, anm2::SUBTRACT); - if (isDownPressed) frame_change_apply({.pivotY = step}, anm2::ADD); + if (isLeftPressed) frame_change_apply({.pivotX = step}, ChangeType::SUBTRACT); + if (isRightPressed) frame_change_apply({.pivotX = step}, ChangeType::ADD); + if (isUpPressed) frame_change_apply({.pivotY = step}, ChangeType::SUBTRACT); + if (isDownPressed) frame_change_apply({.pivotY = step}, ChangeType::ADD); - frame_change_apply({.pivotX = (int)frame->pivot.x, .pivotY = (int)frame->pivot.y}); + frame_change_from_current_apply( + [](const Element& frame) { return FrameChange{.pivotX = (int)frame.pivot.x, .pivotY = (int)frame.pivot.y}; }); if (isDuring) { @@ -530,14 +716,14 @@ namespace anm2ed::imgui } } - if (isEnd) document.change(Document::FRAMES); + if (isEnd) document_change_push(Document::FRAMES); break; case tool::CROP: if (isRegionInUse) break; if (frames.empty()) { if (!spritesheet || regionSelection.empty()) break; - if (isBegin) document.snapshot(localize.get(EDIT_REGION_CROP)); + if (isBegin) snapshot_push(EDIT_REGION_CROP); if (isMouseClicked) { @@ -557,49 +743,29 @@ namespace anm2ed::imgui if (isDuring) { if (!isMouseDown) - { - for (auto id : regionSelection) - { - auto it = spritesheet->regions.find(id); - if (it == spritesheet->regions.end()) continue; - - auto& region = it->second; - auto minPoint = glm::min(region.crop, region.crop + region.size); - auto maxPoint = glm::max(region.crop, region.crop + region.size); - region.crop = clamp_vec2_to_int(minPoint); - region.size = clamp_vec2_to_int(maxPoint - minPoint); - - if (isGridSnap) - { - auto [snapMin, snapMax] = snap_rect(region.crop, region.crop + region.size); - region.crop = clamp_vec2_to_int(snapMin); - region.size = clamp_vec2_to_int(snapMax - snapMin); - } - } - } + region_crop_normalize_all(isGridSnap, gridSize, gridOffset); if (ImGui::BeginTooltip()) { - auto it = spritesheet->regions.find(*regionSelection.begin()); - if (it != spritesheet->regions.end()) + if (auto region = region_get(*regionSelection.begin())) { - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_CROP), - std::make_format_args(it->second.crop.x, it->second.crop.y)) - .c_str()); - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_SIZE), - std::make_format_args(it->second.size.x, it->second.size.y)) - .c_str()); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region->crop.x, region->crop.y)) + .c_str()); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region->size.x, region->size.y)) + .c_str()); } ImGui::EndTooltip(); } } - if (isEnd) document.change(Document::SPRITESHEETS); + if (isEnd) document_change_push(Document::SPRITESHEETS); break; } if (!item) break; - if (isBegin) document.snapshot(localize.get(EDIT_FRAME_CROP)); + if (isBegin) snapshot_push(EDIT_FRAME_CROP); if (isMouseClicked) { @@ -614,10 +780,10 @@ namespace anm2ed::imgui .sizeX = maxPoint.x - minPoint.x, .sizeY = maxPoint.y - minPoint.y}); } - if (isLeftPressed) frame_change_apply({.cropX = stepX}, anm2::SUBTRACT); - if (isRightPressed) frame_change_apply({.cropX = stepX}, anm2::ADD); - if (isUpPressed) frame_change_apply({.cropY = stepY}, anm2::SUBTRACT); - if (isDownPressed) frame_change_apply({.cropY = stepY}, anm2::ADD); + if (isLeftPressed) frame_change_apply({.cropX = stepX}, ChangeType::SUBTRACT); + if (isRightPressed) frame_change_apply({.cropX = stepX}, ChangeType::ADD); + if (isUpPressed) frame_change_apply({.cropY = stepY}, ChangeType::SUBTRACT); + if (isDownPressed) frame_change_apply({.cropY = stepY}, ChangeType::ADD); frame_change_apply( {.cropX = frame->crop.x, .cropY = frame->crop.y, .sizeX = frame->size.x, .sizeY = frame->size.y}); @@ -626,23 +792,7 @@ namespace anm2ed::imgui { if (!isMouseDown) { - auto minPoint = glm::min(frame->crop, frame->crop + frame->size); - auto maxPoint = glm::max(frame->crop, frame->crop + frame->size); - - frame_change_apply({.cropX = minPoint.x, - .cropY = minPoint.y, - .sizeX = maxPoint.x - minPoint.x, - .sizeY = maxPoint.y - minPoint.y}); - - if (isGridSnap) - { - auto [snapMin, snapMax] = snap_rect(frame->crop, frame->crop + frame->size); - - frame_change_apply({.cropX = snapMin.x, - .cropY = snapMin.y, - .sizeX = snapMax.x - snapMin.x, - .sizeY = snapMax.y - snapMin.y}); - } + frame_crop_normalize_apply(isGridSnap, gridSize, gridOffset); } if (ImGui::BeginTooltip()) { @@ -655,27 +805,24 @@ namespace anm2ed::imgui ImGui::EndTooltip(); } } - if (isEnd) document.change(Document::FRAMES); + if (isEnd) document_change_push(Document::FRAMES); break; case tool::DRAW: case tool::ERASE: { - if (!spritesheet) break; + if (!texture) break; auto color = useTool == tool::DRAW ? toolColor : vec4(); if (isMouseClicked) - document.snapshot(useTool == tool::DRAW ? localize.get(EDIT_DRAW) : localize.get(EDIT_ERASE)); - if (isMouseDown) spritesheet->texture.pixel_line(ivec2(previousMousePos), ivec2(mousePos), color); - if (isMouseReleased) - { - document.change(Document::SPRITESHEETS); - } + snapshot_push(useTool == tool::DRAW ? EDIT_DRAW : EDIT_ERASE); + if (isMouseDown) texture_line_apply(ivec2(previousMousePos), ivec2(mousePos), color); + if (isMouseReleased) texture_change_push(); break; } case tool::COLOR_PICKER: { - if (spritesheet && isDuring) + if (texture && isDuring) { - toolColor = spritesheet->texture.pixel_read(mousePos); + toolColor = texture->pixel_read(mousePos); if (ImGui::BeginTooltip()) { ImGui::ColorButton("##Color Picker Button", to_imvec4(toolColor)); @@ -696,31 +843,31 @@ namespace anm2ed::imgui if (tool == tool::PAN && hoveredRegionId != -1 && spritesheet) { - auto regionIt = spritesheet->regions.find(hoveredRegionId); - if (regionIt != spritesheet->regions.end()) + if (auto region = region_get(hoveredRegionId)) { if (ImGui::BeginTooltip()) { - auto& region = regionIt->second; ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(region.name.c_str()); + ImGui::TextUnformatted(region->name.c_str()); ImGui::PopFont(); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_ID), std::make_format_args(hoveredRegionId)).c_str()); ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region.crop.x, region.crop.y)).c_str()); + std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region->crop.x, region->crop.y)) + .c_str()); ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y)).c_str()); - if (region.origin == anm2::Spritesheet::Region::CUSTOM) + std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region->size.x, region->size.y)) + .c_str()); + if (region->origin == Origin::CUSTOM) { ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y)) + std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region->pivot.x, region->pivot.y)) .c_str()); } else { StringType originString = LABEL_REGION_ORIGIN_CENTER; - if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT; + if (region->origin == Origin::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT; auto originLabel = localize.get(originString); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str()); @@ -766,8 +913,7 @@ namespace anm2ed::imgui if (mouseWheel != 0 || isZoomIn || isZoomOut) { auto focus = mouseWheel != 0 ? vec2(mousePos) : vec2(); - if (auto spritesheet = document.spritesheet_get(); spritesheet && mouseWheel == 0) - focus = spritesheet->texture.size / 2; + if (texture && mouseWheel == 0) focus = texture->size / 2; auto previousZoom = zoom; zoom_set(zoom, pan, focus, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); @@ -776,31 +922,17 @@ namespace anm2ed::imgui } } - if (tool == tool::PAN && - ImGui::BeginPopupContextWindow("##Spritesheet Editor Context Menu", ImGuiMouseButton_Right)) + if (tool == tool::PAN) { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(LABEL_CENTER_VIEW), settings.shortcutCenterView.c_str())) center_view(); - - if (ImGui::MenuItem(localize.get(LABEL_FIT), settings.shortcutFit.c_str(), false, - spritesheet && spritesheet->texture.is_valid())) - fit_view(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_IN), settings.shortcutZoomIn.c_str())) zoom_in(); - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_OUT), settings.shortcutZoomOut.c_str())) zoom_out(); - - ImGui::EndPopup(); + Actions actions{}; + actions_undo_redo_add(actions, manager, document); + actions.separator(); + actions.add(ACTION_CENTER_VIEW, []() { return true; }, center_view); + actions.add(ACTION_FIT_VIEW, [&]() { return texture && texture->is_valid(); }, fit_view); + actions.separator(); + actions.add(ACTION_ZOOM_IN, []() { return true; }, zoom_in); + actions.add(ACTION_ZOOM_OUT, []() { return true; }, zoom_out); + actions_context_window_draw("##Spritesheet Editor Context Menu", actions, settings); } if (!document.isSpritesheetEditorSet) diff --git a/src/imgui/window/spritesheet_editor.hpp b/src/imgui/window/spritesheet_editor.hpp index 857cccc..0db9d44 100644 --- a/src/imgui/window/spritesheet_editor.hpp +++ b/src/imgui/window/spritesheet_editor.hpp @@ -17,10 +17,12 @@ namespace anm2ed::imgui float checkerSyncZoom{}; bool isCheckerPanInitialized{}; bool hasPendingZoomPanAdjust{}; + bool isFocused{}; int hoveredRegionId{-1}; public: SpritesheetEditor(); + bool is_focused_get() const; void update(Manager&, Settings&, Resources&); }; } diff --git a/src/imgui/window/spritesheets.cpp b/src/imgui/window/spritesheets.cpp deleted file mode 100644 index 595e90d..0000000 --- a/src/imgui/window/spritesheets.cpp +++ /dev/null @@ -1,709 +0,0 @@ -#include "spritesheets.hpp" - -#include -#include -#include - -#include -#include -#include - -#include "document.hpp" -#include "log.hpp" -#include "path_.hpp" -#include "strings.hpp" -#include "toast.hpp" -#include "working_directory.hpp" - -using namespace anm2ed::types; -using namespace anm2ed::resource; -using namespace anm2ed::util; -using namespace glm; - -namespace anm2ed::imgui -{ - static constexpr auto PADDING_MAX = 100; - - void Spritesheets::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, - Clipboard& clipboard) - { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& selection = document.spritesheet.selection; - auto& reference = document.spritesheet.reference; - auto& region = document.region; - auto style = ImGui::GetStyle(); - std::function pack{}; - - auto add_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_OPEN); }; - auto replace_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_REPLACE); }; - auto set_file_path_open = [&]() { dialog.file_save(Dialog::SPRITESHEET_PATH_SET); }; - auto merge_open = [&]() - { - if (selection.size() <= 1) return; - mergeSelection = selection; - mergePopup.open(); - }; - auto pack_open = [&]() - { - if (selection.size() != 1) return; - auto id = *selection.begin(); - if (!anm2.content.spritesheets.contains(id)) return; - if (anm2.content.spritesheets.at(id).regions.empty()) return; - packId = id; - packPopup.open(); - }; - - auto add = [&](const std::filesystem::path& path) - { - if (path.empty()) return; - document.spritesheet_add(path); - newSpritesheetId = document.spritesheet.reference; - }; - - auto remove_unused = [&]() - { - auto unused = anm2.spritesheets_unused(); - if (unused.empty()) return; - - auto behavior = [&]() - { - for (auto& id : unused) - { - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - auto pathString = path::to_utf8(spritesheet.path); - toasts.push(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET), std::make_format_args(id, pathString))); - logger.info(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - anm2.content.spritesheets.erase(id); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SPRITESHEETS), Document::ALL, behavior()); - }; - - auto reload = [&]() - { - if (selection.empty()) return; - - auto behavior = [&]() - { - for (auto& id : selection) - { - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - spritesheet.reload(document.directory_get()); - document.spritesheet_hash_set_saved(id); - auto pathString = path::to_utf8(spritesheet.path); - toasts.push(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET), std::make_format_args(id, pathString))); - logger.info(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_RELOAD_SPRITESHEETS), Document::SPRITESHEETS, behavior()); - }; - - auto replace = [&](const std::filesystem::path& path) - { - if (selection.size() != 1 || path.empty()) return; - - auto behavior = [&]() - { - auto& id = *selection.begin(); - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - spritesheet.reload(document.directory_get(), path); - document.spritesheet_hash_set_saved(id); - auto pathString = path::to_utf8(spritesheet.path); - toasts.push(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET), std::make_format_args(id, pathString))); - logger.info(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SPRITESHEET), Document::SPRITESHEETS, behavior()); - }; - - auto set_file_path = [&](const std::filesystem::path& path) - { - if (selection.size() != 1 || path.empty()) return; - - auto behavior = [&]() - { - auto id = *selection.begin(); - if (!anm2.content.spritesheets.contains(id)) return; - WorkingDirectory workingDirectory(document.directory_get()); - anm2.content.spritesheets[id].path = path::make_relative(path); - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_SET_SPRITESHEET_FILE_PATH), Document::SPRITESHEETS, behavior()); - }; - - auto save = [&](const std::set& ids) - { - if (ids.empty()) return; - - for (auto& id : ids) - { - if (!anm2.content.spritesheets.contains(id)) continue; - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - auto pathString = path::to_utf8(spritesheet.path); - if (spritesheet.save(document.directory_get())) - { - document.spritesheet_hash_set_saved(id); - toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString))); - logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - } - else - { - toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString))); - logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH), - std::make_format_args(id, pathString))); - } - } - }; - - auto save_open = [&]() - { - if (selection.empty()) return; - if (settings.fileIsWarnOverwrite) - { - saveSelection = selection; - overwritePopup.open(); - } - else - { - save(selection); - } - }; - - auto merge = [&]() - { - if (mergeSelection.size() <= 1) return; - - auto behavior = [&]() - { - auto baseID = *mergeSelection.begin(); - if (anm2.spritesheets_merge(mergeSelection, (anm2::SpritesheetMergeOrigin)settings.mergeSpritesheetsOrigin, - settings.mergeSpritesheetsIsMakeRegions, - settings.mergeSpritesheetsIsMakePrimaryRegion, - (origin::Type)settings.mergeSpritesheetsRegionOrigin)) - { - selection = {baseID}; - reference = baseID; - region.reference = -1; - region.selection.clear(); - toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS)); - logger.info(localize.get(TOAST_MERGE_SPRITESHEETS, anm2ed::ENGLISH)); - } - else - { - toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED)); - logger.error(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED, anm2ed::ENGLISH)); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_SPRITESHEETS), Document::ALL, behavior()); - }; - pack = [&]() - { - int id = packId != -1 ? packId : (selection.size() == 1 ? *selection.begin() : -1); - if (id == -1) return; - if (!anm2.content.spritesheets.contains(id)) return; - if (anm2.content.spritesheets.at(id).regions.empty()) return; - - auto behavior = [&]() - { - auto padding = std::max(0, settings.packPadding); - if (anm2.spritesheet_pack(id, padding)) - { - toasts.push(localize.get(TOAST_PACK_SPRITESHEET)); - logger.info(localize.get(TOAST_PACK_SPRITESHEET, anm2ed::ENGLISH)); - } - else - { - toasts.push(localize.get(TOAST_PACK_SPRITESHEET_FAILED)); - logger.error(localize.get(TOAST_PACK_SPRITESHEET_FAILED, anm2ed::ENGLISH)); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PACK_SPRITESHEET), Document::SPRITESHEETS, behavior()); - }; - - auto open_directory = [&](anm2::Spritesheet& spritesheet) - { - if (spritesheet.path.empty()) return; - std::error_code ec{}; - auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / spritesheet.path, ec); - if (ec) absolutePath = document.directory_get() / spritesheet.path; - auto target = std::filesystem::is_directory(absolutePath) ? absolutePath - : std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path() - : document.directory_get(); - dialog.file_explorer_open(target); - }; - - auto copy = [&]() - { - if (selection.empty()) return; - - std::string clipboardText{}; - for (auto& id : selection) - clipboardText += anm2.content.spritesheets[id].to_string(id); - clipboard.set(clipboardText); - }; - - auto paste = [&]() - { - if (clipboard.is_empty()) return; - - auto behavior = [&]() - { - auto maxSpritesheetIdBefore = - anm2.content.spritesheets.empty() ? -1 : anm2.content.spritesheets.rbegin()->first; - std::string errorString{}; - document.snapshot(localize.get(EDIT_PASTE_SPRITESHEETS)); - if (anm2.spritesheets_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString)) - { - if (!anm2.content.spritesheets.empty()) - { - auto maxSpritesheetIdAfter = anm2.content.spritesheets.rbegin()->first; - if (maxSpritesheetIdAfter > maxSpritesheetIdBefore) - { - newSpritesheetId = maxSpritesheetIdAfter; - selection = {maxSpritesheetIdAfter}; - reference = maxSpritesheetIdAfter; - region.reference = -1; - region.selection.clear(); - } - } - document.change(Document::SPRITESHEETS); - } - else - { - toasts.push( - std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED), std::make_format_args(errorString))); - logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED, anm2ed::ENGLISH), - std::make_format_args(errorString))); - }; - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_SPRITESHEETS), Document::SPRITESHEETS, behavior()); - }; - - auto context_menu = [&]() - { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && - ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - ImGui::OpenPopup("##Spritesheet Context Menu"); - - if (ImGui::BeginPopup("##Spritesheet Context Menu")) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1)) - open_directory(anm2.content.spritesheets[*selection.begin()]); - if (ImGui::MenuItem(localize.get(BASIC_SET_FILE_PATH), nullptr, false, selection.size() == 1)) - set_file_path_open(); - - if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open(); - if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); - - bool isPackable = selection.size() == 1 && anm2.content.spritesheets.contains(*selection.begin()) && - !anm2.content.spritesheets.at(*selection.begin()).regions.empty(); - - if (ImGui::MenuItem(localize.get(BASIC_RELOAD), nullptr, false, !selection.empty())) reload(); - if (ImGui::MenuItem(localize.get(BASIC_REPLACE), nullptr, false, selection.size() == 1)) replace_open(); - if (ImGui::MenuItem(localize.get(BASIC_MERGE), settings.shortcutMerge.c_str(), false, selection.size() > 1)) - merge_open(); - if (ImGui::MenuItem(localize.get(BASIC_PACK), nullptr, false, isPackable)) pack_open(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PACK_SPRITESHEET)); - if (ImGui::MenuItem(localize.get(BASIC_SAVE), nullptr, false, !selection.empty())) save_open(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy(); - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - ImGui::EndPopup(); - } - ImGui::PopStyleVar(2); - }; - - if (ImGui::Begin(localize.get(LABEL_SPRITESHEETS_WINDOW), &settings.windowIsSpritesheets)) - { - auto childSize = size_without_footer_get(2); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); - if (ImGui::BeginChild("##Spritesheets Child", childSize, ImGuiChildFlags_Borders)) - { - auto spritesheetChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 4); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); - - selection.start(anm2.content.spritesheets.size()); - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) - { - selection.clear(); - for (auto& id : anm2.content.spritesheets | std::views::keys) - selection.insert(id); - } - if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear(); - auto scroll_to_item = [&](float itemHeight, bool isTarget) - { - if (!isTarget) return; - auto windowHeight = ImGui::GetWindowHeight(); - auto targetTop = ImGui::GetCursorPosY(); - auto targetBottom = targetTop + itemHeight; - auto visibleTop = ImGui::GetScrollY(); - auto visibleBottom = visibleTop + windowHeight; - if (targetTop < visibleTop) - ImGui::SetScrollY(targetTop); - else if (targetBottom > visibleBottom) - ImGui::SetScrollY(targetBottom - windowHeight); - }; - int scrollTargetId = -1; - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - (ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true))) - { - std::vector ids{}; - ids.reserve(anm2.content.spritesheets.size()); - for (auto& [id, sheet] : anm2.content.spritesheets) - ids.push_back(id); - if (!ids.empty()) - { - int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1; - int current = reference; - if (current == -1 && !selection.empty()) current = *selection.begin(); - auto it = std::find(ids.begin(), ids.end(), current); - int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it); - index = std::clamp(index + delta, 0, (int)ids.size() - 1); - int nextId = ids[index]; - selection = {nextId}; - reference = nextId; - region.reference = -1; - region.selection.clear(); - scrollTargetId = nextId; - } - } - - for (auto& [id, spritesheet] : anm2.content.spritesheets) - { - auto isNewSpritesheet = newSpritesheetId == id; - ImGui::PushID(id); - - scroll_to_item(spritesheetChildSize.y, scrollTargetId == id); - - if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders)) - { - auto isSelected = selection.contains(id); - auto isReferenced = id == reference; - auto cursorPos = ImGui::GetCursorPos(); - bool isValid = spritesheet.texture.is_valid(); - auto& texture = isValid ? spritesheet.texture : resources.icons[icon::NONE]; - auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - auto pathString = path::to_utf8(spritesheet.path); - auto pathCStr = pathString.c_str(); - - ImGui::SetNextItemSelectionUserData(id); - ImGui::SetNextItemStorageID(id); - if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize)) - { - reference = id; - region.reference = -1; - region.selection.clear(); - } - if (scrollTargetId == id) ImGui::SetItemDefaultFocus(); - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) - open_directory(spritesheet); - - auto viewport = ImGui::GetMainViewport(); - auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f; - vec2 textureSize = vec2(glm::max(texture.size.x, 1), glm::max(texture.size.y, 1)); - if (textureSize.x > maxPreviewSize.x || textureSize.y > maxPreviewSize.y) - { - auto scale = glm::min(maxPreviewSize.x / textureSize.x, maxPreviewSize.y / textureSize.y); - textureSize *= scale; - } - - auto textWidth = ImGui::CalcTextSize(pathCStr).x; - auto tooltipPadding = style.WindowPadding.x * 4.0f; - auto minWidth = textureSize.x + style.ItemSpacing.x + textWidth + tooltipPadding; - - ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - if (ImGui::BeginItemTooltip()) - { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); - auto childFlags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; - auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; - if (ImGui::BeginChild("##Spritesheet Tooltip Image Child", to_imvec2(textureSize), childFlags, - noScrollFlags)) - ImGui::ImageWithBg(texture.id, to_imvec2(textureSize), ImVec2(), ImVec2(1, 1), ImVec4(), tintColor); - ImGui::EndChild(); - ImGui::PopStyleVar(); - - ImGui::SameLine(); - - auto infoChildFlags = ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; - if (ImGui::BeginChild("##Spritesheet Info Tooltip Child", ImVec2(), infoChildFlags, noScrollFlags)) - { - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(pathCStr); - ImGui::PopFont(); - - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); - - if (!isValid) - ImGui::TextUnformatted(localize.get(TOOLTIP_SPRITESHEET_INVALID)); - else - ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_TEXTURE_SIZE), - std::make_format_args(texture.size.x, texture.size.y)) - .c_str()); - - ImGui::TextUnformatted(localize.get(TEXT_OPEN_DIRECTORY)); - } - ImGui::EndChild(); - - ImGui::EndTooltip(); - } - ImGui::PopStyleVar(2); - - auto imageSize = to_imvec2(vec2(spritesheetChildSize.y)); - auto aspectRatio = (float)texture.size.x / texture.size.y; - - if (imageSize.x / imageSize.y > aspectRatio) - imageSize.x = imageSize.y * aspectRatio; - else - imageSize.y = imageSize.x / aspectRatio; - - ImGui::SetCursorPos(cursorPos); - ImGui::ImageWithBg(texture.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor); - - ImGui::SetCursorPos( - ImVec2(spritesheetChildSize.y + style.ItemSpacing.x, - spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); - - if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); - auto spritesheetLabel = std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathCStr)); - if (document.spritesheet_is_dirty(id)) - spritesheetLabel = - std::vformat(localize.get(FORMAT_SPRITESHEET_NOT_SAVED), std::make_format_args(spritesheetLabel)); - ImGui::TextUnformatted(spritesheetLabel.c_str()); - if (isReferenced) ImGui::PopFont(); - } - - ImGui::EndChild(); - - if (isNewSpritesheet) - { - ImGui::SetScrollHereY(0.5f); - newSpritesheetId = -1; - } - - ImGui::PopID(); - } - - ImGui::PopStyleVar(); - selection.finish(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - - context_menu(); - - auto rowOneWidgetSize = widget_size_with_row_get(3); - - shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) add_open(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_SPRITESHEET), settings.shortcutAdd); - - if (dialog.is_selected(Dialog::SPRITESHEET_OPEN)) - { - add(dialog.path); - dialog.reset(); - } - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.empty()); - if (ImGui::Button(localize.get(BASIC_RELOAD), rowOneWidgetSize)) reload(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SPRITESHEETS)); - ImGui::EndDisabled(); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.size() != 1); - if (ImGui::Button(localize.get(BASIC_REPLACE), rowOneWidgetSize)) replace_open(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SPRITESHEET)); - ImGui::EndDisabled(); - - if (dialog.is_selected(Dialog::SPRITESHEET_REPLACE)) - { - replace(dialog.path); - dialog.reset(); - } - - if (dialog.is_selected(Dialog::SPRITESHEET_PATH_SET)) - { - set_file_path(dialog.path); - dialog.reset(); - } - - auto rowTwoWidgetSize = widget_size_with_row_get(2); - - shortcut(manager.chords[SHORTCUT_REMOVE]); - if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowTwoWidgetSize)) remove_unused(); - set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SPRITESHEETS), settings.shortcutRemove); - - ImGui::SameLine(); - - ImGui::BeginDisabled(selection.empty()); - if (ImGui::Button(localize.get(BASIC_SAVE), rowTwoWidgetSize)) save_open(); - ImGui::EndDisabled(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SAVE_SPRITESHEETS)); - - if (imgui::shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open(); - if (imgui::shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused(); - if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); - if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); - if (imgui::shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED) && selection.size() > 1) merge_open(); - } - ImGui::End(); - - mergePopup.trigger(); - if (ImGui::BeginPopupModal(mergePopup.label(), &mergePopup.isOpen, ImGuiWindowFlags_NoResize)) - { - settings.mergeSpritesheetsRegionOrigin = - glm::clamp(settings.mergeSpritesheetsRegionOrigin, (int)origin::TOP_LEFT, (int)origin::ORIGIN_CENTER); - - auto close = [&]() - { - mergeSelection.clear(); - mergePopup.close(); - }; - - auto optionsSize = child_size_get(6); - if (ImGui::BeginChild("##Merge Spritesheets Options", optionsSize, ImGuiChildFlags_Borders)) - { - ImGui::SeparatorText(localize.get(LABEL_REGION_PROPERTIES_ORIGIN)); - ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_BOTTOM), &settings.mergeSpritesheetsOrigin, - anm2::APPEND_BOTTOM); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_BOTTOM_LEFT)); - ImGui::SameLine(); - ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_RIGHT), &settings.mergeSpritesheetsOrigin, - anm2::APPEND_RIGHT); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_TOP_RIGHT)); - - ImGui::SeparatorText(localize.get(LABEL_OPTIONS)); - ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_SPRITESHEET_REGIONS), &settings.mergeSpritesheetsIsMakeRegions); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_SPRITESHEET_REGIONS)); - - ImGui::BeginDisabled(!settings.mergeSpritesheetsIsMakeRegions); - ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION), - &settings.mergeSpritesheetsIsMakePrimaryRegion); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION)); - - const char* regionOriginOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT), - localize.get(LABEL_REGION_ORIGIN_CENTER)}; - ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), &settings.mergeSpritesheetsRegionOrigin, - regionOriginOptions, IM_ARRAYSIZE(regionOriginOptions)); - ImGui::EndDisabled(); - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - shortcut(manager.chords[SHORTCUT_CONFIRM]); - ImGui::BeginDisabled(mergeSelection.size() <= 1); - if (ImGui::Button(localize.get(BASIC_MERGE), widgetSize)) - { - merge(); - close(); - } - ImGui::EndDisabled(); - - ImGui::SameLine(); - shortcut(manager.chords[SHORTCUT_CANCEL]); - if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close(); - - ImGui::EndPopup(); - } - mergePopup.end(); - - packPopup.trigger(); - if (ImGui::BeginPopupModal(packPopup.label(), &packPopup.isOpen, ImGuiWindowFlags_NoResize)) - { - settings.packPadding = std::max(0, settings.packPadding); - - auto close = [&]() - { - packId = -1; - packPopup.close(); - }; - - auto optionsSize = child_size_get(1); - if (ImGui::BeginChild("##Pack Spritesheet Options", optionsSize, ImGuiChildFlags_Borders)) - { - ImGui::DragInt(localize.get(LABEL_PACK_PADDING), &settings.packPadding, DRAG_SPEED, 0, PADDING_MAX); - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - shortcut(manager.chords[SHORTCUT_CONFIRM]); - bool isPackable = packId != -1 && anm2.content.spritesheets.contains(packId) && - !anm2.content.spritesheets.at(packId).regions.empty(); - ImGui::BeginDisabled(!isPackable); - if (ImGui::Button(localize.get(BASIC_PACK), widgetSize)) - { - if (pack) pack(); - close(); - } - ImGui::EndDisabled(); - - ImGui::SameLine(); - shortcut(manager.chords[SHORTCUT_CANCEL]); - if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close(); - - ImGui::EndPopup(); - } - packPopup.end(); - - overwritePopup.trigger(); - if (ImGui::BeginPopupModal(overwritePopup.label(), &overwritePopup.isOpen, ImGuiWindowFlags_NoResize)) - { - ImGui::TextUnformatted(localize.get(LABEL_OVERWRITE_CONFIRMATION)); - - auto widgetSize = widget_size_with_row_get(2); - - if (ImGui::Button(localize.get(BASIC_YES), widgetSize)) - { - save(saveSelection); - saveSelection.clear(); - overwritePopup.close(); - } - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(BASIC_NO), widgetSize)) - { - saveSelection.clear(); - overwritePopup.close(); - } - - ImGui::EndPopup(); - } - overwritePopup.end(); - } -} diff --git a/src/imgui/window/spritesheets.hpp b/src/imgui/window/spritesheets.hpp deleted file mode 100644 index 878b53c..0000000 --- a/src/imgui/window/spritesheets.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "clipboard.hpp" -#include "dialog.hpp" -#include "manager.hpp" -#include "resources.hpp" -#include "settings.hpp" - -namespace anm2ed::imgui -{ - class Spritesheets - { - int newSpritesheetId{-1}; - PopupHelper mergePopup{PopupHelper(LABEL_SPRITESHEETS_MERGE_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)}; - PopupHelper packPopup{PopupHelper(LABEL_SPRITESHEETS_PACK_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)}; - PopupHelper overwritePopup{PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, imgui::POPUP_SMALL_NO_HEIGHT)}; - std::set mergeSelection{}; - int packId{-1}; - std::set saveSelection{}; - - public: - void update(Manager&, Settings&, Resources&, Dialog&, Clipboard& clipboard); - }; -} diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index 801c510..394725c 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -4,13 +4,17 @@ #include #include #include +#include +#include #include +#include "actions.hpp" #include "log.hpp" #include "toast.hpp" +#include "util/imgui/draw.hpp" -#include "vector_.hpp" +#include "vector.hpp" using namespace anm2ed::resource; using namespace anm2ed::types; @@ -81,10 +85,20 @@ namespace anm2ed::imgui {0.6353f, 0.2235f, 0.3647f, 1.0f}}; constexpr auto FRAME_MULTIPLE = 5; - constexpr auto FRAME_TOOLTIP_HOVER_DELAY = 0.75f; // Extra delay for frame info tooltip. + constexpr auto FRAME_TOOLTIP_HOVER_DELAY = 0.75f; #define ITEM_CHILD_WIDTH ImGui::GetTextLineHeightWithSpacing() * 12.5 + struct TimelineItemRow + { + int type{NONE}; + int id{-1}; + int index{-1}; + int groupId{-1}; + int depth{}; + bool isGroup{}; + }; + void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { auto& document = *manager.get(); @@ -93,9 +107,8 @@ namespace anm2ed::imgui auto& reference = document.reference; auto& frames = document.frames; auto& region = document.region; - auto animation = document.animation_get(); - auto itemFrameChildHeight = (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y * 1.5f) * - settings.timelineItemHeight; + auto animation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex); + auto rowFrameChildHeight = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y * 1.5f; style = ImGui::GetStyle(); auto isLightTheme = settings.theme == theme::LIGHT; @@ -106,32 +119,317 @@ namespace anm2ed::imgui isTextPushed = true; } - auto type_index = [](anm2::Type type) { return std::clamp((int)type, 0, (int)anm2::TRIGGER); }; + auto type_index = [](int type) { return std::clamp(type, 0, (int)TRIGGER); }; + auto item_type_get = [](int type) { return static_cast(type); }; + auto item_get = [&](int type, int id = -1) + { return animation ? animation_item_get(*animation, item_type_get(type), id) : nullptr; }; + auto frame_get = [&]() + { + return anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.frameIndex, + reference.itemID); + }; + auto selected_item_get = [&]() + { return anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID); }; + auto item_frames_count = [](const Element* item) { return item ? (int)item->children.size() : 0; }; + auto layer_get = [&](int id) { return anm2.element_get(ElementType::LAYER_ELEMENT, id); }; + auto null_get = [&](int id) + { + auto nulls = anm2.element_get(ElementType::NULLS); + return nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr; + }; + auto spritesheet_get = [&](int id) { return anm2.element_get(ElementType::SPRITESHEET, id); }; + auto info_get = [&]() { return element_first_get(anm2.root, ElementType::INFO); }; + auto container_type_get = [](int type) + { + if (type == LAYER) return ElementType::LAYER_ANIMATIONS; + if (type == NULL_) return ElementType::NULL_ANIMATIONS; + return ElementType::UNKNOWN; + }; + auto track_type_get = [](int type) + { + if (type == LAYER) return ElementType::LAYER_ANIMATION; + if (type == NULL_) return ElementType::NULL_ANIMATION; + return ElementType::UNKNOWN; + }; + auto track_id_get = [](const Element& track, int type) + { + if (type == LAYER) return track.layerId; + if (type == NULL_) return track.nullId; + return -1; + }; + auto track_container_get = [&](int type) + { + return animation ? element_child_first_get(*animation, container_type_get(type)) : nullptr; + }; + auto track_group_get = [&](int type, int groupId) + { + auto container = track_container_get(type); + return container ? element_child_id_get(*container, ElementType::GROUP, groupId) : nullptr; + }; + auto is_track_group_visible = [&](int type, int groupId) + { + if (groupId == -1) return true; + auto group = track_group_get(type, groupId); + return !group || group->isVisible; + }; + auto row_group_get = [&](const TimelineItemRow& row) -> Element* + { + return row.isGroup ? track_group_get(row.type, row.id) : nullptr; + }; + auto group_items_count_get = [&](int type, int groupId) + { + auto container = track_container_get(type); + auto trackType = track_type_get(type); + int count{}; + if (!container) return count; + for (auto& item : container->children) + if (item.type == trackType && item.groupId == groupId) ++count; + return count; + }; - auto type_color_base_vec = [&](anm2::Type type) - { return isLightTheme ? FRAME_COLOR_LIGHT_BASE[type_index(type)] : anm2::TYPE_COLOR[type]; }; + auto command_animation_get = [](Document& document, int animationIndex) + { return document.anm2.element_get(ElementType::ANIMATION, animationIndex); }; + auto command_item_get = [](Document& document, int animationIndex, int type, int id) + { + auto animation = document.anm2.element_get(ElementType::ANIMATION, animationIndex); + return animation ? animation_item_get(*animation, static_cast(type), id) : nullptr; + }; + auto command_frame_get = [](Document& document, const Reference& targetReference) + { + return document.anm2.element_get(targetReference.animationIndex, static_cast(targetReference.itemType), + targetReference.frameIndex, targetReference.itemID); + }; + auto command_layer_get = [](Document& document, int id) + { return document.anm2.element_get(ElementType::LAYER_ELEMENT, id); }; + auto command_spritesheet_get = [](Document& document, int id) + { return document.anm2.element_get(ElementType::SPRITESHEET, id); }; + auto command_info_get = [](Document& document) + { return element_first_get(document.anm2.root, ElementType::INFO); }; + auto item_reference_get = [&](int type, int id) { return Reference{reference.animationIndex, type, id}; }; + auto item_reference_from_frame_get = [](Reference frameReference) + { + frameReference.frameIndex = -1; + return frameReference; + }; + auto is_same_item = [](const Reference& left, const Reference& right) + { + return left.animationIndex == right.animationIndex && left.itemType == right.itemType && + left.itemID == right.itemID; + }; + auto group_selection_reset = [this]() + { + groupReferences.clear(); + isRowSelectionAnchorSet = false; + }; + auto frame_references_for_current_get = [&]() + { + std::set result = frames.references; + if (result.empty()) + for (auto frameIndex : frames.selection) + result.insert({reference.animationIndex, reference.itemType, reference.itemID, frameIndex}); + return result; + }; + auto item_references_for_current_get = [&]() + { + std::set result = document.items.references; + if (result.empty() && reference.itemType != NONE) + result.insert(item_reference_get(reference.itemType, reference.itemID)); + return result; + }; + auto frames_selection_sync_for = [&](Document& targetDocument) + { + targetDocument.frames.selection.clear(); + for (const auto& frameReference : targetDocument.frames.references) + if (is_same_item(frameReference, targetDocument.reference) && frameReference.frameIndex >= 0) + targetDocument.frames.selection.insert(frameReference.frameIndex); + frameSelectionSnapshot.assign(targetDocument.frames.selection.begin(), targetDocument.frames.selection.end()); + frameSelectionSnapshotReference = targetDocument.reference; + }; + auto item_selection_set_for = [&](Document& targetDocument, Reference itemReference) + { + itemReference.frameIndex = -1; + targetDocument.items.references = {itemReference}; + }; + auto frame_selection_set_for = [&](Document& targetDocument, Reference frameReference) + { + group_selection_reset(); + targetDocument.frames.references = {frameReference}; + targetDocument.reference = frameReference; + item_selection_set_for(targetDocument, item_reference_from_frame_get(frameReference)); + frames_selection_sync_for(targetDocument); + }; + auto frame_selection_toggle_for = [&](Document& targetDocument, Reference frameReference) + { + group_selection_reset(); + auto& selection = targetDocument.frames.references; + auto itemReference = item_reference_from_frame_get(frameReference); + if (selection.contains(frameReference)) + { + if (selection.size() > 1) selection.erase(frameReference); + bool isItemStillSelected{}; + for (const auto& selectedFrame : selection) + if (is_same_item(selectedFrame, itemReference)) isItemStillSelected = true; + if (!isItemStillSelected && targetDocument.items.references.size() > 1) + targetDocument.items.references.erase(itemReference); + } + else + { + selection.insert(frameReference); + targetDocument.items.references.insert(itemReference); + } + targetDocument.reference = frameReference; + frames_selection_sync_for(targetDocument); + }; + auto frame_selection_range_set_for = [&](Document& targetDocument, Reference firstReference, + Reference lastReference, bool isAdditive) -> bool + { + group_selection_reset(); + if (!is_same_item(firstReference, lastReference) || firstReference.frameIndex < 0 || lastReference.frameIndex < 0) + return false; - auto type_color_active_vec = [&](anm2::Type type) + auto item = command_item_get(targetDocument, lastReference.animationIndex, lastReference.itemType, + lastReference.itemID); + if (!item) return false; + if (firstReference.frameIndex >= (int)item->children.size() || + lastReference.frameIndex >= (int)item->children.size()) + return false; + + auto firstIndex = firstReference.frameIndex; + auto lastIndex = lastReference.frameIndex; + if (firstIndex > lastIndex) std::swap(firstIndex, lastIndex); + auto itemReference = item_reference_from_frame_get(lastReference); + if (!isAdditive) + { + targetDocument.frames.references.clear(); + item_selection_set_for(targetDocument, itemReference); + } + else + targetDocument.items.references.insert(itemReference); + + for (int i = firstIndex; i <= lastIndex; ++i) + targetDocument.frames.references.insert( + {lastReference.animationIndex, lastReference.itemType, lastReference.itemID, i}); + + targetDocument.reference = lastReference; + frames_selection_sync_for(targetDocument); + return true; + }; + auto all_frame_references_for_item_get = [&](const Reference& itemReference) + { + std::set result{}; + auto item = item_get(itemReference.itemType, itemReference.itemID); + if (!item) return result; + for (int i = 0; i < (int)item->children.size(); ++i) + result.insert({itemReference.animationIndex, itemReference.itemType, itemReference.itemID, i}); + return result; + }; + auto all_frame_references_for_items_get = [&]() + { + std::set result{}; + auto itemReferences = item_references_for_current_get(); + if (itemReferences.empty()) + itemReferences.insert(item_reference_get(reference.itemType, reference.itemID)); + for (auto itemReference : itemReferences) + { + auto itemFrames = all_frame_references_for_item_get(itemReference); + result.insert(itemFrames.begin(), itemFrames.end()); + } + return result; + }; + auto frames_selection_reset_for = [this](Document& targetDocument) + { + targetDocument.frames.selection.clear(); + targetDocument.frames.references.clear(); + frameSelectionSnapshot.clear(); + frameSelectionLocked.clear(); + isFrameSelectionLocked = false; + frameFocusRequested = false; + frameFocusIndex = -1; + frameSelectionSnapshotReference = targetDocument.reference; + }; + auto frames_selection_set_reference_for = [this](Document& targetDocument) + { + auto& targetFrames = targetDocument.frames; + auto& targetReference = targetDocument.reference; + targetFrames.selection.clear(); + targetFrames.references.clear(); + if (targetReference.frameIndex >= 0) + { + targetFrames.selection.insert(targetReference.frameIndex); + targetFrames.references.insert(targetReference); + } + frameSelectionSnapshot.assign(targetFrames.selection.begin(), targetFrames.selection.end()); + frameSelectionSnapshotReference = targetReference; + frameSelectionLocked.clear(); + isFrameSelectionLocked = false; + frameFocusIndex = targetReference.frameIndex; + frameFocusRequested = targetReference.frameIndex >= 0; + }; + auto reference_clear_for = [=](Document& targetDocument) + { + targetDocument.reference = {targetDocument.reference.animationIndex}; + frames_selection_reset_for(targetDocument); + targetDocument.items.references.clear(); + }; + auto reference_set_item_for = [=](Document& targetDocument, int type, int id) + { + targetDocument.reference = {targetDocument.reference.animationIndex, type, id}; + frames_selection_reset_for(targetDocument); + item_selection_set_for(targetDocument, targetDocument.reference); + }; + auto reference_set_timeline_item_for = [=](Document& targetDocument, int type, int id) + { + if (type == LAYER) + if (auto layer = command_layer_get(targetDocument, id)) + targetDocument.spritesheet.reference = layer->spritesheetId; + reference_set_item_for(targetDocument, type, id); + }; + auto command_push = [&](auto run) + { + manager.command_push({manager.selected, + [run](Manager& manager, Document& document) mutable { run(manager, document); }}); + }; + auto snapshot_command_push = [&](StringType messageType) + { + auto message = std::string(localize.get(messageType)); + manager.command_push( + {manager.selected, [message](Manager&, Document& document) { document.snapshot(message); }}); + }; + auto edit_command_push = [&](StringType messageType, Document::ChangeType changeType, auto run) + { + auto message = std::string(localize.get(messageType)); + manager.command_push({manager.selected, + [=](Manager& manager, Document& document) mutable + { + document.snapshot(message); + run(manager, document); + document.anm2_change(changeType); + }}); + }; + auto type_color_base_vec = [&](int type) + { return isLightTheme ? FRAME_COLOR_LIGHT_BASE[type_index(type)] : TYPE_COLOR[type]; }; + + auto type_color_active_vec = [&](int type) { if (isLightTheme) return FRAME_COLOR_LIGHT_ACTIVE[type_index(type)]; - return anm2::TYPE_COLOR_ACTIVE[type]; + return TYPE_COLOR_ACTIVE[type]; }; - auto type_color_hovered_vec = [&](anm2::Type type) + auto type_color_hovered_vec = [&](int type) { if (isLightTheme) return FRAME_COLOR_LIGHT_HOVERED[type_index(type)]; - return anm2::TYPE_COLOR_HOVERED[type]; + return TYPE_COLOR_HOVERED[type]; }; - auto item_color_vec = [&](anm2::Type type) + auto item_color_vec = [&](int type) { - if (!isLightTheme) return anm2::TYPE_COLOR[type]; + if (!isLightTheme) return TYPE_COLOR[type]; return ITEM_COLOR_LIGHT_BASE[type_index(type)]; }; - auto item_color_active_vec = [&](anm2::Type type) + auto item_color_active_vec = [&](int type) { - if (!isLightTheme) return anm2::TYPE_COLOR_ACTIVE[type]; + if (!isLightTheme) return TYPE_COLOR_ACTIVE[type]; return ITEM_COLOR_LIGHT_ACTIVE[type_index(type)]; }; @@ -163,14 +461,7 @@ namespace anm2ed::imgui auto frames_selection_set_reference = [&]() { - frames.selection.clear(); - if (reference.frameIndex >= 0) frames.selection.insert(reference.frameIndex); - frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); - frameSelectionSnapshotReference = reference; - frameSelectionLocked.clear(); - isFrameSelectionLocked = false; - frameFocusIndex = reference.frameIndex; - frameFocusRequested = reference.frameIndex >= 0; + frames_selection_set_reference_for(document); }; auto playback_stop = [&]() @@ -180,249 +471,488 @@ namespace anm2ed::imgui playback.timing_reset(); }; - auto frame_insert = [&](anm2::Item* item) + auto frame_insert = [&]() { - if (!animation || !item) return; + auto targetReference = reference; + auto targetFrameTime = document.frameTime; + if (!animation || !command_item_get(document, targetReference.animationIndex, targetReference.itemType, + targetReference.itemID)) + return; - auto behavior = [&, item]() - { - if (reference.itemType == anm2::TRIGGER) - { - for (auto& trigger : animation->triggers.frames) - if (document.frameTime == trigger.atFrame) return; + edit_command_push(EDIT_INSERT_FRAME, Document::FRAMES, + [=, this](Manager&, Document& document) mutable + { + auto animation = command_animation_get(document, targetReference.animationIndex); + auto item = command_item_get(document, targetReference.animationIndex, + targetReference.itemType, targetReference.itemID); + if (!animation || !item) return; - anm2::Frame addFrame{}; - addFrame.atFrame = document.frameTime; - item->frames.push_back(addFrame); - item->frames_sort_by_at_frame(); - reference.frameIndex = item->frame_index_from_at_frame_get(addFrame.atFrame); - } - else - { - auto frame = document.frame_get(); - if (frame) - { - auto addFrame = *frame; - item->frames.insert(item->frames.begin() + reference.frameIndex + 1, addFrame); - reference.frameIndex++; - } - else if (!item->frames.empty()) - { - auto addFrame = item->frames.back(); - item->frames.emplace_back(addFrame); - reference.frameIndex = (int)(item->frames.size()) - 1; - } - else - { - item->frames.emplace_back(anm2::Frame()); - reference.frameIndex = 0; - } - } + auto newReference = targetReference; - frames_selection_set_reference(); - if (reference.itemType != anm2::TRIGGER) - document.frameTime = item->frame_time_from_index_get(reference.frameIndex); - }; + if (targetReference.itemType == TRIGGER) + { + for (auto& trigger : item->children) + if (targetFrameTime == trigger.atFrame) return; - DOCUMENT_EDIT(document, localize.get(EDIT_INSERT_FRAME), Document::FRAMES, behavior()); + auto addFrame = element_make(ElementType::TRIGGER); + addFrame.atFrame = targetFrameTime; + item->children.push_back(addFrame); + frames_sort_by_at_frame(*item); + newReference.frameIndex = frame_index_from_at_frame_get(*item, addFrame.atFrame); + } + else + { + auto frame = command_frame_get(document, targetReference); + if (frame) + { + auto addFrame = *frame; + auto insertIndex = + std::clamp(targetReference.frameIndex + 1, 0, (int)item->children.size()); + item->children.insert(item->children.begin() + insertIndex, addFrame); + newReference.frameIndex = insertIndex; + } + else if (!item->children.empty()) + { + auto addFrame = item->children.back(); + item->children.emplace_back(addFrame); + newReference.frameIndex = (int)item->children.size() - 1; + } + else + { + item->children.emplace_back(element_make(ElementType::FRAME)); + newReference.frameIndex = 0; + } + } + + document.reference = newReference; + frames_selection_set_reference_for(document); + if (newReference.itemType != TRIGGER) + document.frameTime = frame_time_from_index_get(*item, newReference.frameIndex); + }); }; - auto frames_delete = [&]() + auto frames_delete_for = [=](Document& document, std::set selectedFrames) mutable { - if (!animation) return; - if (auto item = animation->item_get(reference.itemType, reference.itemID); item) + std::map> groupedFrames{}; + for (auto frameReference : selectedFrames) { - for (auto it = frames.selection.rbegin(); it != frames.selection.rend(); ++it) + auto itemReference = item_reference_from_frame_get(frameReference); + groupedFrames[itemReference].insert(frameReference.frameIndex); + } + + for (auto& [itemReference, indices] : groupedFrames) + { + auto item = command_item_get(document, itemReference.animationIndex, itemReference.itemType, + itemReference.itemID); + if (!item) continue; + + for (auto it = indices.rbegin(); it != indices.rend(); ++it) { auto i = *it; - item->frames.erase(item->frames.begin() + i); + if (i >= 0 && i < (int)item->children.size()) item->children.erase(item->children.begin() + i); } - - if (item->frames.empty()) - reference.frameIndex = -1; - else - reference.frameIndex = glm::clamp(--reference.frameIndex, 0, (int)item->frames.size() - 1); - frames_selection_set_reference(); } + + auto item = command_item_get(document, document.reference.animationIndex, document.reference.itemType, + document.reference.itemID); + if (!item || item->children.empty()) + { + document.reference.frameIndex = -1; + frames_selection_reset_for(document); + return; + } + + document.reference.frameIndex = glm::clamp(document.reference.frameIndex, 0, (int)item->children.size() - 1); + frames_selection_set_reference_for(document); }; auto frames_delete_action = [&]() { - if (!document.frame_get()) return; - DOCUMENT_EDIT(document, localize.get(EDIT_DELETE_FRAMES), Document::FRAMES, frames_delete()); + auto selectedFrames = frame_references_for_current_get(); + if (selectedFrames.empty()) return; + edit_command_push(EDIT_DELETE_FRAMES, Document::FRAMES, + [=](Manager&, Document& document) mutable + { frames_delete_for(document, selectedFrames); }); }; auto frames_bake = [&]() { - auto behavior = [&]() - { - if (auto item = document.item_get()) - { - std::set bakedSelection{}; - std::vector selectedIndices(frames.selection.begin(), frames.selection.end()); - int insertedBefore = 0; + auto selectedFrames = frame_references_for_current_get(); + auto bakeInterval = settings.bakeInterval; + auto isRoundScale = settings.bakeIsRoundScale; + auto isRoundRotation = settings.bakeIsRoundRotation; + std::erase_if(selectedFrames, + [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); + if (selectedFrames.empty()) return; - for (auto originalIndex : selectedIndices) - { - auto i = originalIndex + insertedBefore; - if (!vector::in_bounds(item->frames, i)) continue; + edit_command_push(EDIT_BAKE_FRAMES, Document::FRAMES, + [=, this](Manager&, Document& document) mutable + { + std::map> groupedFrames{}; + for (auto frameReference : selectedFrames) + { + auto itemReference = item_reference_from_frame_get(frameReference); + groupedFrames[itemReference].insert(frameReference.frameIndex); + } - auto originalDuration = item->frames[i].duration; - item->frames_bake(i, settings.bakeInterval, settings.bakeIsRoundScale, settings.bakeIsRoundRotation); + std::set bakedSelection{}; + for (auto& [itemReference, indices] : groupedFrames) + { + auto item = command_item_get(document, itemReference.animationIndex, + itemReference.itemType, itemReference.itemID); + if (!item) continue; - auto bakedCount = originalDuration <= anm2::FRAME_DURATION_MIN - ? 1 - : (int)std::ceil((float)originalDuration / settings.bakeInterval); - for (int offset = 0; offset < bakedCount; ++offset) - bakedSelection.insert(i + offset); + int insertedBefore = 0; + for (auto originalIndex : indices) + { + auto i = originalIndex + insertedBefore; + if (!vector::in_bounds(item->children, i)) continue; - insertedBefore += bakedCount - 1; - } + auto originalDuration = item->children[i].duration; + frame_bake(*item, i, bakeInterval, isRoundScale, isRoundRotation); - frames.selection = std::move(bakedSelection); - if (!frames.selection.empty()) - { - reference.frameIndex = *frames.selection.begin(); - frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); - frameSelectionSnapshotReference = reference; - frameSelectionLocked.clear(); - isFrameSelectionLocked = false; - frameFocusIndex = reference.frameIndex; - frameFocusRequested = true; - } - } - }; + auto bakedCount = originalDuration <= FRAME_DURATION_MIN + ? 1 + : (int)std::ceil((float)originalDuration / bakeInterval); + for (int offset = 0; offset < bakedCount; ++offset) + bakedSelection.insert( + {itemReference.animationIndex, itemReference.itemType, itemReference.itemID, + i + offset}); - DOCUMENT_EDIT(document, localize.get(EDIT_BAKE_FRAMES), Document::FRAMES, behavior()); + insertedBefore += bakedCount - 1; + } + } + + document.frames.references = std::move(bakedSelection); + if (!document.frames.references.empty()) + { + document.reference = *document.frames.references.begin(); + frames_selection_sync_for(document); + frameSelectionSnapshotReference = document.reference; + frameSelectionLocked.clear(); + isFrameSelectionLocked = false; + frameFocusIndex = document.reference.frameIndex; + frameFocusRequested = true; + } + else + frames_selection_reset_for(document); + }); }; auto frame_split = [&]() { - auto behavior = [&]() - { - if (reference.itemType == anm2::TRIGGER) return; + auto selectedFrames = frame_references_for_current_get(); + if (selectedFrames.size() != 1) return; + auto targetReference = *selectedFrames.begin(); + auto playheadTime = (int)std::floor(playback.time); + if (targetReference.itemType == TRIGGER) return; - auto item = document.item_get(); - auto frame = document.frame_get(); + edit_command_push(EDIT_SPLIT_FRAME, Document::FRAMES, + [=](Manager&, Document& document) mutable + { + if (targetReference.itemType == TRIGGER) return; - if (!item || !frame) return; + auto item = command_item_get(document, targetReference.animationIndex, + targetReference.itemType, targetReference.itemID); + auto frame = command_frame_get(document, targetReference); - auto originalDuration = frame->duration; - if (originalDuration <= 1) return; + if (!item || !frame) return; - auto frameStartTime = item->frame_time_from_index_get(reference.frameIndex); - int frameStart = (int)std::round(frameStartTime); - int playheadTime = (int)std::floor(playback.time); - int firstDuration = playheadTime - frameStart + 1; + auto originalDuration = frame->duration; + if (originalDuration <= 1) return; - if (firstDuration <= 0 || firstDuration >= originalDuration) return; + auto frameStartTime = frame_time_from_index_get(*item, targetReference.frameIndex); + int frameStart = (int)std::round(frameStartTime); + int firstDuration = playheadTime - frameStart + 1; - int secondDuration = originalDuration - firstDuration; - anm2::Frame splitFrame = *frame; - splitFrame.duration = secondDuration; + if (firstDuration <= 0 || firstDuration >= originalDuration) return; - auto nextFrame = vector::in_bounds(item->frames, reference.frameIndex + 1) - ? &item->frames[reference.frameIndex + 1] - : nullptr; - if (frame->interpolation != anm2::Frame::Interpolation::NONE && nextFrame) - { - float interpolation = (float)firstDuration / (float)originalDuration; - switch (frame->interpolation) - { - case anm2::Frame::Interpolation::EASE_IN: - interpolation *= interpolation; - break; - case anm2::Frame::Interpolation::EASE_OUT: - interpolation = 1.0f - ((1.0f - interpolation) * (1.0f - interpolation)); - break; - case anm2::Frame::Interpolation::EASE_IN_OUT: - interpolation = interpolation < 0.5f ? (2.0f * interpolation * interpolation) - : (1.0f - std::pow(-2.0f * interpolation + 2.0f, 2.0f) * 0.5f); - break; - case anm2::Frame::Interpolation::LINEAR: - case anm2::Frame::Interpolation::NONE: - default: - break; - } + int secondDuration = originalDuration - firstDuration; + auto splitFrame = *frame; + splitFrame.duration = secondDuration; - splitFrame.rotation = glm::mix(frame->rotation, nextFrame->rotation, interpolation); - splitFrame.position = glm::mix(frame->position, nextFrame->position, interpolation); - splitFrame.scale = glm::mix(frame->scale, nextFrame->scale, interpolation); - splitFrame.colorOffset = glm::mix(frame->colorOffset, nextFrame->colorOffset, interpolation); - splitFrame.tint = glm::mix(frame->tint, nextFrame->tint, interpolation); - } + auto nextFrame = track_frame_get(*item, targetReference.frameIndex + 1); + if (frame->interpolation != Interpolation::NONE && nextFrame) + { + float interpolation = (float)firstDuration / (float)originalDuration; + switch (frame->interpolation) + { + case Interpolation::EASE_IN: + interpolation *= interpolation; + break; + case Interpolation::EASE_OUT: + interpolation = 1.0f - ((1.0f - interpolation) * (1.0f - interpolation)); + break; + case Interpolation::EASE_IN_OUT: + interpolation = interpolation < 0.5f + ? (2.0f * interpolation * interpolation) + : (1.0f - std::pow(-2.0f * interpolation + 2.0f, 2.0f) * 0.5f); + break; + case Interpolation::LINEAR: + case Interpolation::NONE: + default: + break; + } - frame->duration = firstDuration; - item->frames.insert(item->frames.begin() + reference.frameIndex + 1, splitFrame); - frames_selection_set_reference(); - }; + splitFrame.rotation = glm::mix(frame->rotation, nextFrame->rotation, interpolation); + splitFrame.position = glm::mix(frame->position, nextFrame->position, interpolation); + splitFrame.scale = glm::mix(frame->scale, nextFrame->scale, interpolation); + splitFrame.colorOffset = glm::mix(frame->colorOffset, nextFrame->colorOffset, interpolation); + splitFrame.tint = glm::mix(frame->tint, nextFrame->tint, interpolation); + } - DOCUMENT_EDIT(document, localize.get(EDIT_SPLIT_FRAME), Document::FRAMES, behavior()); - }; - - auto frames_selection_reset = [&]() - { - frames.clear(); - frameSelectionSnapshot.clear(); - frameSelectionLocked.clear(); - isFrameSelectionLocked = false; - frameFocusRequested = false; - frameFocusIndex = -1; - frameSelectionSnapshotReference = reference; + frame->duration = firstDuration; + item->children.insert(item->children.begin() + targetReference.frameIndex + 1, splitFrame); + document.reference = targetReference; + frames_selection_set_reference_for(document); + }); }; auto reference_clear = [&]() { - reference = {reference.animationIndex}; - frames_selection_reset(); + group_selection_reset(); + reference_clear_for(document); }; - auto reference_set_item = [&](anm2::Type type, int id) + auto reference_set_item = [&](int type, int id) { - reference = {reference.animationIndex, type, id}; - frames_selection_reset(); + group_selection_reset(); + reference_set_item_for(document, type, id); }; - auto reference_set_timeline_item = [&](anm2::Type type, int id) + auto reference_set_timeline_item = [&](int type, int id) { - if (type == anm2::LAYER) - if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end()) - document.spritesheet.reference = it->second.spritesheetID; - reference_set_item(type, id); + group_selection_reset(); + reference_set_timeline_item_for(document, type, id); + }; + + auto timeline_item_rows_get = [&]() + { + std::vector rows{}; + if (!animation) return rows; + + auto track_row_push = [&](const Element& item, int type, int index, int depth = 0) + { + rows.push_back({.type = type, + .id = track_id_get(item, type), + .index = index, + .groupId = item.groupId, + .depth = depth}); + }; + auto group_row_push = [&](const Element& group, int type, int index) + { + rows.push_back({.type = type, .id = group.id, .index = index, .isGroup = true}); + }; + auto group_ids_get = [](const Element& container) + { + std::set result{}; + for (const auto& item : container.children) + if (item.type == ElementType::GROUP) result.insert(item.id); + return result; + }; + + rows.push_back({.type = ROOT}); + + if (auto layerAnimations = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS)) + { + auto groupIds = group_ids_get(*layerAnimations); + auto layer_track_push = [&](int groupId, int depth) + { + for (int j = (int)layerAnimations->children.size() - 1; j >= 0; --j) + { + auto& item = layerAnimations->children[j]; + if (item.type != ElementType::LAYER_ANIMATION || item.groupId != groupId) continue; + if (settings.timelineIsShowUnused || !item.children.empty()) track_row_push(item, LAYER, j, depth); + } + }; + + for (int i = (int)layerAnimations->children.size() - 1; i >= 0; --i) + { + auto& item = layerAnimations->children[i]; + if (item.type == ElementType::GROUP) + { + group_row_push(item, LAYER, i); + if (item.isExpanded) layer_track_push(item.id, 1); + } + else if (item.type == ElementType::LAYER_ANIMATION && !groupIds.contains(item.groupId) && + (settings.timelineIsShowUnused || !item.children.empty())) + track_row_push(item, LAYER, i); + } + } + + if (auto nullAnimations = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS)) + { + auto groupIds = group_ids_get(*nullAnimations); + auto null_track_push = [&](int groupId, int depth) + { + for (int j = 0; j < (int)nullAnimations->children.size(); ++j) + { + auto& item = nullAnimations->children[j]; + if (item.type != ElementType::NULL_ANIMATION || item.groupId != groupId) continue; + if (settings.timelineIsShowUnused || !item.children.empty()) track_row_push(item, NULL_, j, depth); + } + }; + + for (int i = 0; i < (int)nullAnimations->children.size(); ++i) + { + auto& item = nullAnimations->children[i]; + if (item.type == ElementType::GROUP) + { + group_row_push(item, NULL_, i); + if (item.isExpanded) null_track_push(item.id, 1); + } + else if (item.type == ElementType::NULL_ANIMATION && !groupIds.contains(item.groupId) && + (settings.timelineIsShowUnused || !item.children.empty())) + track_row_push(item, NULL_, i); + } + } + + rows.push_back({.type = TRIGGER}); + return rows; }; auto timeline_item_references_get = [&]() { - std::vector itemReferences; - if (!animation) return itemReferences; - - itemReferences.push_back({reference.animationIndex, anm2::ROOT}); - - for (auto& id : animation->layerOrder | std::views::reverse) + std::vector itemReferences; + for (const auto& row : timeline_item_rows_get()) { - auto item = animation->item_get(anm2::LAYER, id); - if (!item || (!settings.timelineIsShowUnused && item->frames.empty())) continue; - itemReferences.push_back({reference.animationIndex, anm2::LAYER, id}); + if (row.isGroup) continue; + itemReferences.push_back({reference.animationIndex, row.type, row.id}); } - - for (auto& [id, item] : animation->nullAnimations) - { - if (!settings.timelineIsShowUnused && item.frames.empty()) continue; - itemReferences.push_back({reference.animationIndex, anm2::NULL_, id}); - } - - itemReferences.push_back({reference.animationIndex, anm2::TRIGGER}); return itemReferences; }; + auto group_reference_get = [&](const TimelineItemRow& row) + { return TimelineGroupReference{manager.selected, reference.animationIndex, row.type, row.id}; }; + + auto row_reference_get = [&](const TimelineItemRow& row) + { + return TimelineRowReference{manager.selected, reference.animationIndex, row.type, row.id, row.index, row.isGroup}; + }; + + auto row_item_reference_get = [](const TimelineRowReference& row) + { return Reference{row.animationIndex, row.type, row.id}; }; + + auto timeline_row_references_get = [&]() + { + std::vector rowReferences; + for (const auto& row : timeline_item_rows_get()) + { + if (row.type == NONE) continue; + rowReferences.push_back(row_reference_get(row)); + } + return rowReferences; + }; + + auto is_group_selected = [&](const TimelineItemRow& row) + { + return groupReferences.contains(group_reference_get(row)); + }; + + auto is_row_selected = [&](const TimelineItemRow& row) + { + if (row.isGroup) return is_group_selected(row); + auto itemReference = item_reference_get(row.type, row.id); + auto isReferenced = reference.itemType == row.type && reference.itemID == row.id; + return document.items.references.contains(itemReference) || + (document.items.references.empty() && groupReferences.empty() && isReferenced); + }; + + auto row_selection_clear = [&]() + { + document.items.references.clear(); + groupReferences.clear(); + }; + + auto row_selection_insert = [&](const TimelineRowReference& row) + { + if (row.isGroup) + groupReferences.insert({row.documentIndex, row.animationIndex, row.type, row.id}); + else + document.items.references.insert(row_item_reference_get(row)); + }; + + auto row_selection_erase = [&](const TimelineRowReference& row) + { + if (row.isGroup) + groupReferences.erase({row.documentIndex, row.animationIndex, row.type, row.id}); + else + document.items.references.erase(row_item_reference_get(row)); + }; + + auto is_row_reference_selected = [&](const TimelineRowReference& row) + { + if (row.isGroup) return groupReferences.contains({row.documentIndex, row.animationIndex, row.type, row.id}); + return document.items.references.contains(row_item_reference_get(row)); + }; + + auto row_selection_count_get = [&]() { return document.items.references.size() + groupReferences.size(); }; + + auto row_selection_set = [&](const TimelineItemRow& row) + { + auto rowReference = row_reference_get(row); + auto isCtrlDown = ImGui::IsKeyDown(ImGuiMod_Ctrl); + auto isShiftDown = ImGui::IsKeyDown(ImGuiMod_Shift); + + if (row.isGroup) + reference = {reference.animationIndex}; + else + { + if (row.type == LAYER) + if (auto layer = layer_get(row.id)) document.spritesheet.reference = layer->spritesheetId; + reference = row_item_reference_get(rowReference); + } + frames_selection_reset_for(document); + + if (isShiftDown) + { + auto rowSelection = timeline_row_references_get(); + auto anchor = isRowSelectionAnchorSet ? rowSelectionAnchor : rowReference; + auto first = std::find(rowSelection.begin(), rowSelection.end(), anchor); + auto last = std::find(rowSelection.begin(), rowSelection.end(), rowReference); + if (first == rowSelection.end() || last == rowSelection.end()) + { + if (!isCtrlDown) row_selection_clear(); + row_selection_insert(rowReference); + rowSelectionAnchor = rowReference; + } + else + { + auto firstIndex = (int)std::distance(rowSelection.begin(), first); + auto lastIndex = (int)std::distance(rowSelection.begin(), last); + if (firstIndex > lastIndex) std::swap(firstIndex, lastIndex); + if (!isCtrlDown) row_selection_clear(); + for (int i = firstIndex; i <= lastIndex; ++i) + row_selection_insert(rowSelection[i]); + if (!isRowSelectionAnchorSet) rowSelectionAnchor = rowReference; + } + } + else if (isCtrlDown) + { + if (is_row_reference_selected(rowReference) && row_selection_count_get() > 1) + row_selection_erase(rowReference); + else + row_selection_insert(rowReference); + rowSelectionAnchor = rowReference; + } + else + { + row_selection_clear(); + row_selection_insert(rowReference); + rowSelectionAnchor = rowReference; + } + + isRowSelectionAnchorSet = true; + }; + auto reference_set_adjacent_item = [&](int direction) { auto itemReferences = timeline_item_references_get(); if (itemReferences.empty()) return; - auto it = std::find_if(itemReferences.begin(), itemReferences.end(), [&](const anm2::Reference& itemReference) { - return itemReference.itemType == reference.itemType && itemReference.itemID == reference.itemID; - }); + auto it = std::find_if( + itemReferences.begin(), itemReferences.end(), [&](const Reference& itemReference) + { return itemReference.itemType == reference.itemType && itemReference.itemID == reference.itemID; }); int index = direction > 0 ? 0 : (int)itemReferences.size() - 1; if (it != itemReferences.end()) index = (int)std::distance(itemReferences.begin(), it) + direction; @@ -432,26 +962,314 @@ namespace anm2ed::imgui reference_set_timeline_item(itemReference.itemType, itemReference.itemID); }; + auto selected_row_references_get = [&]() + { + std::vector result{}; + for (const auto& row : timeline_item_rows_get()) + if (is_row_selected(row)) result.push_back(row_reference_get(row)); + return result; + }; + + auto row_drag_references_get = [&](const TimelineItemRow& row) + { + auto clicked = row_reference_get(row); + auto rows = selected_row_references_get(); + if (!is_row_reference_selected(clicked)) rows = {clicked}; + if (rows.empty()) rows = {clicked}; + + auto type = rows.front().type; + if (type != LAYER && type != NULL_) return std::vector{clicked}; + for (auto rowReference : rows) + if (rowReference.type != type || (rowReference.type != LAYER && rowReference.type != NULL_)) + return std::vector{clicked}; + return rows; + }; + + auto row_label_get = [&](const TimelineRowReference& row) -> std::string + { + if (row.isGroup) + { + TimelineItemRow groupRow{.type = row.type, .id = row.id, .index = row.index, .isGroup = true}; + auto group = row_group_get(groupRow); + if (!group || group->name.empty()) return localize.get(TEXT_NEW_GROUP); + return group->name; + } + if (row.type == LAYER) + { + auto layer = layer_get(row.id); + if (!layer) return localize.get(TYPE_STRINGS[row.type]); + return std::vformat(localize.get(FORMAT_LAYER), + std::make_format_args(layer->id, layer->name, layer->spritesheetId)); + } + if (row.type == NULL_) + { + auto null = null_get(row.id); + if (!null) return localize.get(TYPE_STRINGS[row.type]); + return std::vformat(localize.get(FORMAT_NULL), std::make_format_args(null->id, null->name)); + } + return localize.get(TYPE_STRINGS[type_index(row.type)]); + }; + + auto row_drag_tooltip_draw = [&](const std::vector& rows) + { + for (auto row : rows) + { + auto label = row_label_get(row); + ImGui::TextUnformatted(label.c_str()); + } + }; + auto item_remove = [&]() { - auto behavior = [&]() + auto targetRows = selected_row_references_get(); + std::map> targetIds{}; + std::map> targetGroupIds{}; + for (auto row : targetRows) { - if (!animation) return; - if (reference.itemType == anm2::LAYER || reference.itemType == anm2::NULL_) - animation->item_remove(reference.itemType, reference.itemID); - reference_clear(); - }; + if (row.type != LAYER && row.type != NULL_) continue; + if (row.isGroup) + targetGroupIds[row.type].insert(row.id); + else + targetIds[row.type].insert(row.id); + } + auto animationIndex = reference.animationIndex; + if (!animation) return; + if (targetIds.empty() && targetGroupIds.empty()) return; + edit_command_push(EDIT_REMOVE_ITEMS, Document::ITEMS, + [=, this](Manager&, Document& document) mutable + { + auto animation = command_animation_get(document, animationIndex); + if (!animation) return; + for (auto targetType : {LAYER, NULL_}) + { + auto containerType = container_type_get(targetType); + auto targetTrackType = track_type_get(targetType); + auto container = element_child_first_get(*animation, containerType); + if (!container) continue; + auto ids = targetIds.contains(targetType) ? targetIds.at(targetType) : std::set{}; + auto groupIds = + targetGroupIds.contains(targetType) ? targetGroupIds.at(targetType) : std::set{}; + for (int i = (int)container->children.size() - 1; i >= 0; --i) + { + auto& item = container->children[i]; + if (item.type == ElementType::GROUP && groupIds.contains(item.id)) + { + container->children.erase(container->children.begin() + i); + continue; + } + if (item.type == targetTrackType && + (ids.contains(track_id_get(item, targetType)) || groupIds.contains(item.groupId))) + container->children.erase(container->children.begin() + i); + } + } + reference_clear_for(document); + group_selection_reset(); + }); + }; - DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ITEMS), Document::ITEMS, behavior()); + auto item_references_groupable_get = [&]() + { + if (!groupReferences.empty()) return std::vector{}; + auto selectedItems = item_references_for_current_get(); + std::erase_if(selectedItems, [](const Reference& itemReference) + { return itemReference.itemType != LAYER && itemReference.itemType != NULL_; }); + if (selectedItems.empty()) return std::vector{}; + auto itemType = selectedItems.begin()->itemType; + for (const auto& itemReference : selectedItems) + if (itemReference.itemType != itemType) return std::vector{}; + + std::vector result{}; + for (const auto& row : timeline_item_rows_get()) + { + if (row.isGroup || row.type != itemType) continue; + auto itemReference = item_reference_get(row.type, row.id); + if (!selectedItems.contains(itemReference)) continue; + if (row.groupId != -1) return std::vector{}; + result.push_back(itemReference); + } + return result; + }; + + auto item_group = [&]() + { + auto targetReferences = item_references_groupable_get(); + if (targetReferences.empty()) return; + auto targetType = targetReferences.front().itemType; + auto animationIndex = reference.animationIndex; + auto groupName = std::string(localize.get(TEXT_NEW_GROUP)); + + edit_command_push(EDIT_GROUP_ITEMS, Document::ITEMS, + [=](Manager&, Document& document) mutable + { + auto animation = command_animation_get(document, animationIndex); + if (!animation) return; + + auto containerType = container_type_get(targetType); + auto targetTrackType = track_type_get(targetType); + auto container = element_child_first_get(*animation, containerType); + if (!container) return; + + std::set targetIds{}; + for (auto itemReference : targetReferences) + targetIds.insert(itemReference.itemID); + + auto group = element_make(ElementType::GROUP); + group.id = element_child_next_id_get(*container, ElementType::GROUP); + group.name = groupName; + group.isExpanded = true; + int insertIndex = (int)container->children.size(); + + for (int i = 0; i < (int)container->children.size(); ++i) + { + auto& item = container->children[i]; + if (item.type == targetTrackType && targetIds.contains(track_id_get(item, targetType))) + { + insertIndex = std::min(insertIndex, i); + item.groupId = group.id; + } + } + + if (insertIndex == (int)container->children.size()) return; + insertIndex = std::clamp(insertIndex, 0, (int)container->children.size()); + container->children.insert(container->children.begin() + insertIndex, group); + + document.items.references.clear(); + for (auto itemReference : targetReferences) + document.items.references.insert(itemReference); + document.reference = targetReferences.front(); + frames_selection_reset_for(document); + }); + }; + + auto rows_move_to_row = [&](std::vector draggedRows, TimelineItemRow targetRow, + bool isDropAfter) + { + if (draggedRows.empty()) return; + auto targetType = draggedRows.front().type; + if (targetType != LAYER && targetType != NULL_) return; + for (const auto& draggedRow : draggedRows) + if (draggedRow.type != targetType) return; + + if (targetRow.isGroup && targetRow.type != targetType) return; + if (!targetRow.isGroup && targetRow.type != targetType && targetRow.type != NONE && targetRow.type != ROOT && + targetRow.type != TRIGGER) + return; + + auto animationIndex = reference.animationIndex; + edit_command_push(EDIT_MOVE_ITEMS, Document::ITEMS, + [=, this](Manager&, Document& document) mutable + { + auto animation = command_animation_get(document, animationIndex); + if (!animation) return; + auto container = element_child_first_get(*animation, container_type_get(targetType)); + if (!container) return; + auto targetTrackType = track_type_get(targetType); + + std::set draggedIds{}; + std::set draggedGroupIds{}; + for (const auto& draggedRow : draggedRows) + { + if (draggedRow.isGroup) + draggedGroupIds.insert(draggedRow.id); + else + draggedIds.insert(draggedRow.id); + } + + if (targetRow.groupId != -1 && draggedGroupIds.contains(targetRow.groupId)) return; + + std::vector movedItems{}; + for (const auto& item : container->children) + { + auto isDraggedGroup = item.type == ElementType::GROUP && draggedGroupIds.contains(item.id); + auto isDraggedTrack = item.type == targetTrackType && + (draggedIds.contains(track_id_get(item, targetType)) || + draggedGroupIds.contains(item.groupId)); + if (isDraggedGroup || isDraggedTrack) movedItems.push_back(item); + } + if (movedItems.empty()) return; + + if (targetRow.isGroup && draggedGroupIds.contains(targetRow.id)) return; + + auto row_index_get = [&](const TimelineItemRow& row) + { + for (int i = 0; i < (int)container->children.size(); ++i) + { + auto& item = container->children[i]; + if (row.isGroup && item.type == ElementType::GROUP && item.id == row.id) return i; + if (!row.isGroup && row.type == targetType && item.type == targetTrackType && + track_id_get(item, targetType) == row.id) + return i; + } + return -1; + }; + + int targetIndex = (int)container->children.size(); + if (targetRow.isGroup || targetRow.type == targetType) + { + auto rowIndex = row_index_get(targetRow); + if (rowIndex == -1) return; + targetIndex = rowIndex + (targetType == LAYER ? !isDropAfter : isDropAfter); + } + + int removedBeforeTarget = 0; + for (int i = (int)container->children.size() - 1; i >= 0; --i) + { + auto& item = container->children[i]; + auto isDraggedGroup = item.type == ElementType::GROUP && draggedGroupIds.contains(item.id); + auto isDraggedTrack = item.type == targetTrackType && + (draggedIds.contains(track_id_get(item, targetType)) || + draggedGroupIds.contains(item.groupId)); + if (isDraggedGroup || isDraggedTrack) + { + if (i < targetIndex) ++removedBeforeTarget; + container->children.erase(container->children.begin() + i); + } + } + + auto targetGroupId = -1; + if (targetRow.isGroup && isDropAfter) + targetGroupId = targetRow.id; + else if (targetRow.type == targetType) + targetGroupId = targetRow.groupId; + + for (auto& item : movedItems) + if (item.type == targetTrackType && !draggedGroupIds.contains(item.groupId)) + item.groupId = targetGroupId; + + targetIndex -= removedBeforeTarget; + targetIndex = std::clamp(targetIndex, 0, (int)container->children.size()); + container->children.insert(container->children.begin() + targetIndex, movedItems.begin(), + movedItems.end()); + + document.items.references.clear(); + groupReferences.clear(); + for (const auto& draggedRow : draggedRows) + { + if (draggedRow.isGroup) + groupReferences.insert({draggedRow.documentIndex, draggedRow.animationIndex, + draggedRow.type, draggedRow.id}); + else + document.items.references.insert(row_item_reference_get(draggedRow)); + } + if (!document.items.references.empty()) + document.reference = *document.items.references.begin(); + else + document.reference = {animationIndex}; + frames_selection_reset_for(document); + }); }; auto fit_animation_length = [&]() { if (!animation) return; - - auto behavior = [&]() { animation->fit_length(); }; - - DOCUMENT_EDIT(document, localize.get(EDIT_FIT_ANIMATION_LENGTH), Document::ANIMATIONS, behavior()); + auto animationIndex = reference.animationIndex; + edit_command_push(EDIT_FIT_ANIMATION_LENGTH, Document::ANIMATIONS, + [=](Manager&, Document& document) + { + auto animation = command_animation_get(document, animationIndex); + if (!animation) return; + animation->frameNum = animation_length_get(*animation); + }); }; auto context_menu = [&]() @@ -462,85 +1280,106 @@ namespace anm2ed::imgui auto copy = [&]() { if (!animation) return; - if (frames.selection.empty()) return; + auto selectedFrames = frame_references_for_current_get(); + if (selectedFrames.empty()) return; - if (auto item = animation->item_get(reference.itemType, reference.itemID); item) + std::string clipboardString{}; + for (auto frameReference : selectedFrames) { - std::string clipboardString{}; - for (auto& i : frames.selection) - { - if (!vector::in_bounds(item->frames, i)) break; - clipboardString += item->frames[i].to_string(reference.itemType); - } - clipboard.set(clipboardString); + auto item = item_get(frameReference.itemType, frameReference.itemID); + auto frame = item ? track_frame_get(*item, frameReference.frameIndex) : nullptr; + if (!frame) continue; + clipboardString += element_to_string(*frame); } + if (!clipboardString.empty()) clipboard.set(clipboardString); }; auto cut = [&]() { copy(); - DOCUMENT_EDIT(document, localize.get(EDIT_CUT_FRAMES), Document::FRAMES, frames_delete()); + auto selectedFrames = frame_references_for_current_get(); + edit_command_push(EDIT_CUT_FRAMES, Document::FRAMES, + [=](Manager&, Document& document) mutable + { frames_delete_for(document, selectedFrames); }); }; auto paste = [&]() { if (clipboard.is_empty()) return; + auto targetReference = reference; + auto selectedFrames = frame_references_for_current_get(); + auto clipboardString = clipboard.get(); + auto targetHoveredTime = hoveredTime; + auto message = std::string(localize.get(EDIT_PASTE_FRAMES)); + command_push([=](Manager&, Document& document) mutable + { + document.snapshot(message); + auto animation = command_animation_get(document, targetReference.animationIndex); + if (!animation) return; + if (auto item = command_item_get(document, targetReference.animationIndex, + targetReference.itemType, targetReference.itemID)) + { + std::set indices{}; + std::string errorString{}; + int insertIndex = (int)item->children.size(); + std::set selectedIndices{}; + for (auto frameReference : selectedFrames) + if (is_same_item(frameReference, targetReference)) + selectedIndices.insert(frameReference.frameIndex); - auto behavior = [&]() - { - if (!animation) return; - if (auto item = animation->item_get(reference.itemType, reference.itemID)) - { - std::set indices{}; - std::string errorString{}; - int insertIndex = (int)item->frames.size(); - if (!frames.selection.empty()) - insertIndex = std::min((int)item->frames.size(), *frames.selection.rbegin() + 1); - else if (reference.frameIndex >= 0 && reference.frameIndex < (int)item->frames.size()) - insertIndex = reference.frameIndex + 1; + if (!selectedIndices.empty()) + insertIndex = std::min((int)item->children.size(), *selectedIndices.rbegin() + 1); + else if (targetReference.frameIndex >= 0 && + targetReference.frameIndex < (int)item->children.size()) + insertIndex = targetReference.frameIndex + 1; - auto start = reference.itemType == anm2::TRIGGER ? hoveredTime : insertIndex; - if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) - { - if (reference.itemType == anm2::LAYER && reference.itemID != -1) - { - anm2::Spritesheet* spritesheet = nullptr; - if (anm2.content.layers.contains(reference.itemID)) - { - auto& layer = anm2.content.layers.at(reference.itemID); - spritesheet = anm2.spritesheet_get(layer.spritesheetID); - } + auto start = targetReference.itemType == TRIGGER ? targetHoveredTime : insertIndex; + if (frames_deserialize(*item, clipboardString, start, indices, &errorString)) + { + if (targetReference.itemType == LAYER && targetReference.itemID != -1) + { + auto layer = command_layer_get(document, targetReference.itemID); + auto spritesheet = + layer ? command_spritesheet_get(document, layer->spritesheetId) : nullptr; - for (auto i : indices) - { - if (!vector::in_bounds(item->frames, i)) continue; - auto& frame = item->frames[i]; - if (frame.regionID == -1) continue; - if (!spritesheet || !spritesheet->regions.contains(frame.regionID)) frame.regionID = -1; - } - } + for (auto i : indices) + { + auto frame = track_frame_get(*item, i); + if (!frame || frame->regionId == -1) continue; + auto region = spritesheet ? element_child_id_get(*spritesheet, ElementType::REGION, + frame->regionId) + : nullptr; + if (!region) frame->regionId = -1; + } + } - frames.selection.clear(); - for (auto i : indices) - frames.selection.insert(i); - reference.frameIndex = *indices.begin(); - document.change(Document::FRAMES); - } - else - { - toasts.push(std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED), errorString)); - logger.error( - std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED, anm2ed::ENGLISH), errorString)); - } - } - else - { - toasts.push(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION)); - logger.warning(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION, anm2ed::ENGLISH)); - } - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_FRAMES), Document::FRAMES, behavior()); + document.reference = targetReference; + document.frames.selection.clear(); + document.frames.references.clear(); + for (auto i : indices) + { + document.frames.selection.insert(i); + document.frames.references.insert( + {targetReference.animationIndex, targetReference.itemType, targetReference.itemID, i}); + } + document.reference.frameIndex = *indices.begin(); + document.anm2_change(Document::FRAMES); + } + else + { + toasts.push(std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED), + errorString)); + logger.error(std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED, + anm2ed::ENGLISH), + errorString)); + } + } + else + { + toasts.push(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION)); + logger.warning(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION, anm2ed::ENGLISH)); + } + }); }; if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut(); @@ -552,96 +1391,171 @@ namespace anm2ed::imgui auto make_region = [&]() { - auto frame = document.frame_get(); - if (!frame || reference.itemType != anm2::LAYER || reference.itemID == -1) return; - if (frame->regionID != -1) return; - if (!anm2.content.layers.contains(reference.itemID)) return; + auto targetReference = reference; + auto frame = frame_get(); + if (!frame || targetReference.itemType != LAYER || targetReference.itemID == -1) return; + if (frame->regionId != -1) return; + auto layer = layer_get(targetReference.itemID); + if (!layer) return; - auto spritesheetID = anm2.content.layers.at(reference.itemID).spritesheetID; - if (!anm2.content.spritesheets.contains(spritesheetID)) return; + auto spritesheetID = layer->spritesheetId; + if (!spritesheet_get(spritesheetID)) return; - anm2::Spritesheet::Region region{}; - region.crop = frame->crop; - region.size = frame->size; - region.pivot = frame->pivot; - region.origin = anm2::Spritesheet::Region::CUSTOM; + auto settingsPtr = &settings; + command_push([=](Manager& manager, Document& document) + { + auto frame = command_frame_get(document, targetReference); + if (!frame || frame->regionId != -1) return; + auto layer = command_layer_get(document, targetReference.itemID); + if (!layer) return; - document.spritesheet.reference = spritesheetID; - settings.windowIsRegions = true; - manager.makeRegionSpritesheetId = spritesheetID; - manager.makeRegion = region; - manager.isMakeRegionRequested = true; + auto spritesheetID = layer->spritesheetId; + if (!command_spritesheet_get(document, spritesheetID)) return; + + auto region = element_make(ElementType::REGION); + region.crop = frame->crop; + region.size = frame->size; + region.pivot = frame->pivot; + region.origin = Origin::CUSTOM; + + document.spritesheet.reference = spritesheetID; + settingsPtr->windowIsRegions = true; + manager.makeRegionSpritesheetId = spritesheetID; + manager.makeRegion = region; + manager.isMakeRegionRequested = true; + }); }; - if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) - { - auto item = animation ? animation->item_get(reference.itemType, reference.itemID) : nullptr; - auto frame = document.frame_get(); - bool isMakeRegion = frame && reference.itemType == anm2::LAYER && reference.itemID != -1 && - frame->regionID == -1 && anm2.content.layers.contains(reference.itemID) && - anm2.content.spritesheets.contains(anm2.content.layers.at(reference.itemID).spritesheetID); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - auto label = playback.isPlaying ? localize.get(LABEL_PAUSE) : localize.get(LABEL_PLAY); - if (ImGui::MenuItem(label, settings.shortcutPlayPause.c_str())) playback.toggle(); - - if (ImGui::MenuItem(localize.get(LABEL_INSERT), settings.shortcutInsertFrame.c_str(), false, item)) - frame_insert(item); - - if (ImGui::MenuItem(localize.get(LABEL_DELETE), settings.shortcutRemove.c_str(), false, document.frame_get())) - frames_delete_action(); - - if (ImGui::MenuItem(localize.get(LABEL_BAKE), settings.shortcutBake.c_str(), false, !frames.selection.empty())) - frames_bake(); - - if (ImGui::MenuItem(localize.get(LABEL_FIT_ANIMATION_LENGTH), settings.shortcutFit.c_str(), false, - animation && animation->frameNum != animation->length())) - fit_animation_length(); - - if (ImGui::MenuItem(localize.get(LABEL_SPLIT), settings.shortcutSplit.c_str(), false, - frames.selection.size() == 1)) - frame_split(); - - if (ImGui::MenuItem(localize.get(LABEL_MAKE_REGION), nullptr, false, isMakeRegion)) make_region(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, !frames.selection.empty())) - cut(); - if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !frames.selection.empty())) - copy(); - if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) - paste(); - - ImGui::EndPopup(); - } + auto item = item_get(reference.itemType, reference.itemID); + auto frame = frame_get(); + auto selectedFrames = frame_references_for_current_get(); + auto selectedBakeFrames = selectedFrames; + std::erase_if(selectedBakeFrames, + [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); + bool isMakeRegion = frame && reference.itemType == LAYER && reference.itemID != -1 && frame->regionId == -1 && + layer_get(reference.itemID) && spritesheet_get(layer_get(reference.itemID)->spritesheetId); + Actions actions{}; + actions_undo_redo_add(actions, manager, document); + actions.separator(); + actions.add({.label = playback.isPlaying ? LABEL_PAUSE : LABEL_PLAY, + .shortcut = SHORTCUT_PLAY_PAUSE, + .isEnabled = []() { return true; }, + .run = [&]() { playback.toggle(); }}); + actions.add({.label = LABEL_INSERT, + .shortcut = SHORTCUT_INSERT_FRAME, + .isEnabled = [&]() { return item; }, + .run = [&]() { frame_insert(); }}); + actions.add({.label = LABEL_DELETE, + .shortcut = SHORTCUT_REMOVE, + .isEnabled = [=]() { return !selectedFrames.empty(); }, + .run = [&]() { frames_delete_action(); }}); + actions.add({.label = LABEL_BAKE, + .shortcut = SHORTCUT_BAKE, + .isEnabled = [=]() { return !selectedBakeFrames.empty(); }, + .run = [&]() { frames_bake(); }}); + actions.add({.label = LABEL_FIT_ANIMATION_LENGTH, + .shortcut = SHORTCUT_FIT, + .isEnabled = [&]() { return animation && animation->frameNum != animation_length_get(*animation); }, + .run = [&]() { fit_animation_length(); }}); + actions.add({.label = LABEL_SPLIT, + .shortcut = SHORTCUT_SPLIT, + .isEnabled = [=]() { return selectedBakeFrames.size() == 1; }, + .run = [&]() { frame_split(); }}); + actions.add({.label = LABEL_MAKE_REGION, + .shortcut = -1, + .isEnabled = [&]() { return isMakeRegion; }, + .run = [&]() { make_region(); }}); + actions.separator(); + actions.add(ACTION_CUT, [=]() { return !selectedFrames.empty(); }, cut); + actions.add(ACTION_COPY, [=]() { return !selectedFrames.empty(); }, copy); + actions.add(ACTION_PASTE, [&]() { return !clipboard.is_empty(); }, paste); + actions_context_window_draw("##Context Menu", actions, settings); ImGui::PopStyleVar(2); }; - auto item_base_properties_open = [&](anm2::Type type, int id) + auto item_base_properties_open = [&](int type, int id) { switch (type) { - case anm2::LAYER: + case LAYER: manager.layer_properties_open(id); break; - case anm2::NULL_: + case NULL_: manager.null_properties_open(id); default: break; }; }; + auto group_properties_close = [&]() + { + groupName.clear(); + groupAnimationIndex = -1; + groupType = NONE; + groupId = -1; + groupPropertiesPopup.close(); + }; + + auto group_properties_open = [&](const TimelineItemRow& row, const Element& group) + { + groupName = group.name.empty() ? std::string(localize.get(TEXT_NEW_GROUP)) : group.name; + groupAnimationIndex = reference.animationIndex; + groupType = row.type; + groupId = row.id; + groupPropertiesPopup.open(); + }; + + auto group_properties_update = [&]() + { + groupPropertiesPopup.trigger(); + + if (ImGui::BeginPopupModal(groupPropertiesPopup.label(), &groupPropertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto childSize = child_size_get(1); + if (ImGui::BeginChild("##Group Properties Child", childSize, ImGuiChildFlags_Borders)) + { + if (groupPropertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); + input_text_string(localize.get(BASIC_NAME), &groupName); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + shortcut(manager.chords[SHORTCUT_CONFIRM]); + if (ImGui::Button(localize.get(BASIC_CONFIRM), widgetSize)) + { + auto targetName = groupName; + auto targetAnimationIndex = groupAnimationIndex; + auto targetType = groupType; + auto targetId = groupId; + edit_command_push(EDIT_RENAME_GROUP, Document::ITEMS, + [=](Manager&, Document& document) + { + auto animation = command_animation_get(document, targetAnimationIndex); + auto container = + animation ? element_child_first_get(*animation, container_type_get(targetType)) + : nullptr; + auto group = container ? element_child_id_get(*container, ElementType::GROUP, targetId) + : nullptr; + if (!group) return; + group->name = targetName; + }); + group_properties_close(); + } + + ImGui::SameLine(); + + shortcut(manager.chords[SHORTCUT_CANCEL]); + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) group_properties_close(); + + ImGui::EndPopup(); + } + + groupPropertiesPopup.end(); + }; + auto item_context_menu = [&]() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); @@ -649,77 +1563,271 @@ namespace anm2ed::imgui auto& type = reference.itemType; auto& id = reference.itemID; - auto item = document.item_get(); + auto item = selected_item_get(); + auto selectedItems = item_references_for_current_get(); + auto selectedRows = selected_row_references_get(); + auto selectedGroupableItems = item_references_groupable_get(); + TimelineItemRow selectedGroupRow{}; + Element* selectedGroup{}; + if (selectedRows.size() == 1 && selectedRows.front().isGroup) + { + auto row = selectedRows.front(); + selectedGroupRow = {.type = row.type, .id = row.id, .index = row.index, .isGroup = true}; + selectedGroup = row_group_get(selectedGroupRow); + } + auto isRemoveAvailable = std::ranges::any_of(selectedRows, [](const TimelineRowReference& row) + { return row.type == LAYER || row.type == NULL_; }); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("##Items Context Menu"); - if (ImGui::BeginPopup("##Items Context Menu")) - { - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false, - document.is_able_to_undo())) - document.undo(); - - if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false, - document.is_able_to_redo())) - document.redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, - item && (type == anm2::LAYER || type == anm2::NULL_))) - item_base_properties_open(type, id); - - if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str(), false, animation)) - itemProperties.open(); - - if (ImGui::MenuItem(localize.get(BASIC_REMOVE), settings.shortcutRemove.c_str(), false, item)) item_remove(); - - ImGui::EndPopup(); - } + Actions actions{}; + actions_undo_redo_add(actions, manager, document); + actions.separator(); + actions.add(ACTION_PROPERTIES, [&]() { return selectedGroup || (item && (type == LAYER || type == NULL_)); }, + [&]() + { + if (selectedGroup) + group_properties_open(selectedGroupRow, *selectedGroup); + else + item_base_properties_open(type, id); + }); + actions.add(ACTION_ADD, [&]() { return animation; }, [&]() { itemProperties.open(); }); + actions.add(ACTION_REMOVE, [=]() { return isRemoveAvailable; }, [&]() { item_remove(); }); + actions.add(ACTION_GROUP, [=]() { return !selectedGroupableItems.empty(); }, [&]() { item_group(); }); + actions_popup_draw("##Items Context Menu", actions, settings); ImGui::PopStyleVar(2); }; - auto item_child = [&](anm2::Type type, int id, int index) + auto item_child = [&](const TimelineItemRow& row, int index) { ImGui::PushID(index); - auto item = animation ? animation->item_get(type, id) : nullptr; - if (type != anm2::NONE && !item) + auto type = row.type; + auto id = row.id; + if (row.isGroup) + { + auto group = row_group_get(row); + if (!group) + { + ImGui::PopID(); + return; + } + + auto label = group->name.empty() ? std::string(localize.get(TEXT_NEW_GROUP)) : group->name; + auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, rowFrameChildHeight); + auto isGroupVisible = group->isVisible; + auto colorVec = item_color_vec(type); + if (is_group_selected(row)) + { + if (isLightTheme) + colorVec = ITEM_COLOR_LIGHT_SELECTED[type_index(type)]; + else + colorVec = item_color_active_vec(type); + } + auto color = to_imvec4(isGroupVisible ? colorVec : colorVec * COLOR_HIDDEN_MULTIPLIER); + ImGui::PushStyleColor(ImGuiCol_ChildBg, color); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + + if (ImGui::BeginChild("##Group Child", itemSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse)) + { + auto cursorPos = ImGui::GetCursorPos(); + auto groupChildMin = ImGui::GetWindowPos(); + auto groupChildMax = ImVec2(groupChildMin.x + ImGui::GetWindowSize().x, + groupChildMin.y + ImGui::GetWindowSize().y); + + auto toggle_group = [&]() + { + auto targetRow = row; + auto targetAnimationIndex = reference.animationIndex; + edit_command_push(EDIT_TOGGLE_GROUP_EXPANDED, Document::ITEMS, + [=](Manager&, Document& document) mutable + { + auto animation = command_animation_get(document, targetAnimationIndex); + auto container = + animation ? element_child_first_get(*animation, container_type_get(targetRow.type)) + : nullptr; + auto group = container ? element_child_id_get(*container, ElementType::GROUP, + targetRow.id) + : nullptr; + if (!group) return; + group->isExpanded = !group->isExpanded; + }); + }; + + ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing))); + ImGui::SetNextItemAllowOverlap(); + ImGui::InvisibleButton("##Group Button", itemSize); + auto groupButtonMin = ImGui::GetItemRectMin(); + auto groupButtonMax = ImGui::GetItemRectMax(); + auto mousePos = ImGui::GetIO().MousePos; + auto is_mouse_in_rect = [](ImVec2 mouse, ImVec2 min, ImVec2 max) + { return mouse.x >= min.x && mouse.x < max.x && mouse.y >= min.y && mouse.y < max.y; }; + auto isGroupHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && + is_mouse_in_rect(mousePos, groupButtonMin, groupButtonMax); + bool isGroupTooltipDelayed{}; + if (isGroupHovered) + { + auto& imguiStyle = ImGui::GetStyle(); + auto previousTooltipDelay = imguiStyle.HoverDelayNormal; + imguiStyle.HoverDelayNormal = FRAME_TOOLTIP_HOVER_DELAY; + isGroupTooltipDelayed = + ImGui::IsItemHovered(ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayNormal | + ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoSharedDelay); + imguiStyle.HoverDelayNormal = previousTooltipDelay; + } + + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) + { + rowDragReferences = row_drag_references_get(row); + ImGui::SetDragDropPayload("Timeline Row Drag Drop", rowDragReferences.data(), + (int)rowDragReferences.size() * (int)sizeof(TimelineRowReference)); + row_drag_tooltip_draw(rowDragReferences); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload( + "Timeline Row Drag Drop", + ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) + { + auto isDropAfter = is_drop_after(groupButtonMin, groupButtonMax); + auto payloadRows = (TimelineRowReference*)payload->Data; + auto payloadCount = payload->DataSize / sizeof(TimelineRowReference); + std::vector draggedRows(payloadRows, payloadRows + payloadCount); + auto isDropIntoGroup = isDropAfter && (row.type == LAYER || row.type == NULL_); + for (auto draggedRow : draggedRows) + if (draggedRow.isGroup || draggedRow.type != row.type) isDropIntoGroup = false; + + if (isDropIntoGroup) + drop_box_draw(ImGui::GetWindowDrawList(), groupChildMin, groupChildMax); + else + drop_line_draw(ImGui::GetWindowDrawList(), groupChildMin, groupChildMax, isDropAfter); + + if (payload->IsDelivery()) rows_move_to_row(draggedRows, row, isDropAfter); + } + ImGui::EndDragDropTarget(); + } + + ImGui::SetCursorPos(cursorPos); + auto folderIcon = group->isExpanded ? icon::FOLDER_OPEN : icon::FOLDER; + ImGui::Image(resources.icons[folderIcon].id, icon_size_get()); + auto iconMin = ImGui::GetItemRectMin(); + auto iconMax = ImGui::GetItemRectMax(); + overlay_icon(resources.icons[folderIcon].id, itemIconTint); + ImGui::SameLine(); + + ImGui::PushStyleColor(ImGuiCol_Text, itemTextColor); + ImGui::TextUnformatted(label.c_str()); + ImGui::PopStyleColor(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + + ImGui::SetCursorPos( + ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + int visibleIcon = isGroupVisible ? icon::VISIBLE : icon::INVISIBLE; + if (ImGui::ImageButton("##Group Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) + { + auto targetAnimationIndex = reference.animationIndex; + auto targetType = type; + auto targetGroupId = group->id; + auto targetVisible = !isGroupVisible; + edit_command_push(EDIT_TOGGLE_ITEM_VISIBILITY, Document::FRAMES, + [=](Manager&, Document& document) + { + auto animation = command_animation_get(document, targetAnimationIndex); + auto container = + animation ? element_child_first_get(*animation, container_type_get(targetType)) + : nullptr; + if (!container) return; + auto group = element_child_id_get(*container, ElementType::GROUP, targetGroupId); + if (!group) return; + group->isVisible = targetVisible; + }); + } + auto visibleButtonMin = ImGui::GetItemRectMin(); + auto visibleButtonMax = ImGui::GetItemRectMax(); + overlay_icon(resources.icons[visibleIcon].id, itemIconTint); + ImGui::SetItemTooltip("%s", isGroupVisible ? localize.get(TOOLTIP_ITEM_VISIBILITY_SHOWN) + : localize.get(TOOLTIP_ITEM_VISIBILITY_HIDDEN)); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(3); + + auto isIconHovered = isGroupHovered && is_mouse_in_rect(mousePos, iconMin, iconMax); + auto isVisibleButtonHovered = isGroupHovered && is_mouse_in_rect(mousePos, visibleButtonMin, visibleButtonMax); + if (isGroupTooltipDelayed && !isVisibleButtonHovered) + { + ImGui::BeginTooltip(); + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); + ImGui::TextUnformatted(label.c_str()); + ImGui::PopFont(); + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(group->id)).c_str()); + auto groupItemsCount = group_items_count_get(type, group->id); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_ITEMS_COUNT), std::make_format_args(groupItemsCount)).c_str()); + ImGui::EndTooltip(); + } + if (isIconHovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left) && + !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left)) + toggle_group(); + else if (isGroupHovered && !isIconHovered && !isVisibleButtonHovered && + ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + group_properties_open(row, *group); + else if (isGroupHovered && !isIconHovered && !isVisibleButtonHovered && + ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left)) + row_selection_set(row); + } + ImGui::EndChild(); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); + ImGui::PopID(); + return; + } + + auto item = item_get(type, id); + if (type != NONE && !item) { ImGui::PopID(); return; } - auto isVisible = item ? item->isVisible : false; + auto isItemVisible = item ? item->isVisible : false; + auto isVisible = item ? item->isVisible && is_track_group_visible(type, row.groupId) : false; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; - if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; + if (isOnlyShowLayers && type != LAYER) isVisible = false; auto isReferenced = reference.itemType == type && reference.itemID == id; + auto isItemSelected = is_row_selected(row); auto label = [&]() -> std::string { - if (type == anm2::LAYER) + if (type == LAYER) { - auto it = anm2.content.layers.find(id); - if (it == anm2.content.layers.end()) return localize.get(anm2::TYPE_STRINGS[type]); - return std::vformat(localize.get(FORMAT_LAYER), - std::make_format_args(id, it->second.name, it->second.spritesheetID)); + auto layer = layer_get(id); + if (!layer) return localize.get(TYPE_STRINGS[type]); + return std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, layer->name, layer->spritesheetId)); } - if (type == anm2::NULL_) + if (type == NULL_) { - auto it = anm2.content.nulls.find(id); - if (it == anm2.content.nulls.end()) return localize.get(anm2::TYPE_STRINGS[type]); - return std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, it->second.name)); + auto null = null_get(id); + if (!null) return localize.get(TYPE_STRINGS[type]); + return std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null->name)); } - return localize.get(anm2::TYPE_STRINGS[type]); + return localize.get(TYPE_STRINGS[type]); }(); - auto icon = anm2::TYPE_ICONS[type]; - auto iconTintCurrent = isLightTheme && type == anm2::NONE ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : itemIconTint; + auto icon = TYPE_ICONS[type]; + auto iconTintCurrent = isLightTheme && type == NONE ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : itemIconTint; auto baseColorVec = item_color_vec(type); auto activeColorVec = item_color_active_vec(type); auto colorVec = baseColorVec; - if (isReferenced && type != anm2::NONE) + if (isItemSelected && type != NONE) { if (isLightTheme) colorVec = ITEM_COLOR_LIGHT_SELECTED[type_index(type)]; @@ -733,28 +1841,59 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, itemFrameChildHeight); + auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, rowFrameChildHeight); if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse)) { auto cursorPos = ImGui::GetCursorPos(); + auto itemChildMin = ImGui::GetWindowPos(); + auto itemChildMax = ImVec2(itemChildMin.x + ImGui::GetWindowSize().x, itemChildMin.y + ImGui::GetWindowSize().y); - if (type != anm2::NONE) + if (type != NONE) { ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing))); ImGui::SetNextItemAllowOverlap(); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4()); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4()); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4()); ImGui::SetNextItemStorageID(id); - if (ImGui::Selectable("##Item Button", isReferenced, ImGuiSelectableFlags_SelectOnClick, itemSize)) + ImGui::InvisibleButton("##Item Button", itemSize); + auto itemButtonMin = ImGui::GetItemRectMin(); + auto itemButtonMax = ImGui::GetItemRectMax(); + auto isItemButtonHovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + + if (type == LAYER || type == NULL_) { - reference_set_timeline_item(type, id); + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) + { + rowDragReferences = row_drag_references_get(row); + ImGui::SetDragDropPayload("Timeline Row Drag Drop", rowDragReferences.data(), + (int)rowDragReferences.size() * (int)sizeof(TimelineRowReference)); + row_drag_tooltip_draw(rowDragReferences); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload( + "Timeline Row Drag Drop", + ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) + { + auto isDropAfter = is_drop_after(itemButtonMin, itemButtonMax); + drop_line_draw(ImGui::GetWindowDrawList(), itemChildMin, itemChildMax, isDropAfter); + + auto payloadRows = (TimelineRowReference*)payload->Data; + auto payloadCount = payload->DataSize / sizeof(TimelineRowReference); + std::vector draggedRows(payloadRows, payloadRows + payloadCount); + if (payload->IsDelivery()) rows_move_to_row(draggedRows, row, isDropAfter); + } + ImGui::EndDragDropTarget(); + } } - ImGui::PopStyleColor(3); - if (ImGui::IsItemHovered()) + + if (isItemButtonHovered) { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) item_base_properties_open(type, id); + else if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && + !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left)) + row_selection_set(row); auto& imguiStyle = ImGui::GetStyle(); auto previousTooltipFlags = imguiStyle.HoverFlagsForTooltipMouse; @@ -771,11 +1910,11 @@ namespace anm2ed::imgui { auto yesNoLabel = [&](bool value) { return value ? localize.get(BASIC_YES) : localize.get(BASIC_NO); }; auto visibleLabel = yesNoLabel(isVisible); - auto framesCount = item ? (int)item->frames.size() : 0; + auto framesCount = item_frames_count(item); switch (type) { - case anm2::ROOT: + case ROOT: { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(localize.get(BASIC_ROOT)); @@ -790,18 +1929,17 @@ namespace anm2ed::imgui std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } - case anm2::LAYER: + case LAYER: { - auto it = anm2.content.layers.find(id); - if (it == anm2.content.layers.end()) break; - auto& layer = it->second; + auto layer = layer_get(id); + if (!layer) break; ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(layer.name.c_str()); + ImGui::TextUnformatted(layer->name.c_str()); ImGui::PopFont(); ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer.spritesheetID)) + std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer->spritesheetId)) .c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); @@ -809,16 +1947,15 @@ namespace anm2ed::imgui std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } - case anm2::NULL_: + case NULL_: { - auto it = anm2.content.nulls.find(id); - if (it == anm2.content.nulls.end()) break; - auto& nullInfo = it->second; + auto nullInfo = null_get(id); + if (!nullInfo) break; ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); - ImGui::TextUnformatted(nullInfo.name.c_str()); + ImGui::TextUnformatted(nullInfo->name.c_str()); ImGui::PopFont(); - auto rectLabel = yesNoLabel(nullInfo.isShowRect); + auto rectLabel = yesNoLabel(nullInfo->isShowRect); ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_RECT), std::make_format_args(rectLabel)).c_str()); @@ -828,7 +1965,7 @@ namespace anm2ed::imgui std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } - case anm2::TRIGGER: + case TRIGGER: { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(localize.get(BASIC_TRIGGERS)); @@ -848,35 +1985,9 @@ namespace anm2ed::imgui } } - if (type == anm2::LAYER) - { - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) - { - ImGui::SetDragDropPayload("Layer Animation Drag Drop", &id, sizeof(int)); - ImGui::EndDragDropSource(); - } - - if (ImGui::BeginDragDropTarget()) - { - if (auto payload = ImGui::AcceptDragDropPayload("Layer Animation Drag Drop")) - { - auto droppedID = *(int*)payload->Data; - - auto layer_order_move = [&]() - { - int source = vector::find_index(animation->layerOrder, droppedID); - int destination = vector::find_index(animation->layerOrder, id); - - if (source != -1 && destination != -1) vector::move_index(animation->layerOrder, source, destination); - }; - - DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_LAYER_ANIMATION), Document::ITEMS, layer_order_move()); - } - ImGui::EndDragDropTarget(); - } - } - - ImGui::SetCursorPos(cursorPos); + auto contentCursorPos = cursorPos; + contentCursorPos.x += (float)row.depth * ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetCursorPos(contentCursorPos); ImGui::Image(resources.icons[icon].id, icon_size_get()); overlay_icon(resources.icons[icon].id, iconTintCurrent); @@ -896,27 +2007,46 @@ namespace anm2ed::imgui 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 = isItemVisible ? icon::VISIBLE : icon::INVISIBLE; if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) - DOCUMENT_EDIT(document, localize.get(EDIT_TOGGLE_ITEM_VISIBILITY), Document::FRAMES, - item->isVisible = !item->isVisible); - overlay_icon(resources.icons[visibleIcon].id, iconTintCurrent); - ImGui::SetItemTooltip("%s", isVisible ? localize.get(TOOLTIP_ITEM_VISIBILITY_SHOWN) - : localize.get(TOOLTIP_ITEM_VISIBILITY_HIDDEN)); - - if (type == anm2::NULL_) { - if (auto it = anm2.content.nulls.find(id); it != anm2.content.nulls.end()) + auto animationIndex = reference.animationIndex; + auto targetType = type; + auto targetID = id; + edit_command_push(EDIT_TOGGLE_ITEM_VISIBILITY, Document::FRAMES, + [=](Manager&, Document& document) + { + auto item = command_item_get(document, animationIndex, targetType, targetID); + if (!item) return; + item->isVisible = !item->isVisible; + }); + } + overlay_icon(resources.icons[visibleIcon].id, iconTintCurrent); + ImGui::SetItemTooltip("%s", isItemVisible ? localize.get(TOOLTIP_ITEM_VISIBILITY_SHOWN) + : localize.get(TOOLTIP_ITEM_VISIBILITY_HIDDEN)); + + if (type == NULL_) + { + if (auto null = null_get(id)) { - auto& null = it->second; - auto& isShowRect = null.isShowRect; + auto& isShowRect = null->isShowRect; auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT; ImGui::SetCursorPos( ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get())) - DOCUMENT_EDIT(document, localize.get(EDIT_TOGGLE_NULL_RECT), Document::FRAMES, - null.isShowRect = !null.isShowRect); + { + auto nullID = id; + edit_command_push(EDIT_TOGGLE_NULL_RECT, Document::FRAMES, + [=](Manager&, Document& document) + { + auto nulls = document.anm2.element_get(ElementType::NULLS); + auto null = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, nullID) + : nullptr; + if (!null) return; + null->isShowRect = !null->isShowRect; + }); + } overlay_icon(resources.icons[rectIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", isShowRect ? localize.get(TOOLTIP_NULL_RECT_SHOWN) : localize.get(TOOLTIP_NULL_RECT_HIDDEN)); @@ -965,13 +2095,12 @@ namespace anm2ed::imgui ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::Text("(?)"); ImGui::PopStyleColor(); - auto tooltipShortcuts = - std::vformat(localize.get(TOOLTIP_TIMELINE_SHORTCUTS), - std::make_format_args(settings.shortcutMovePlayheadBack, - settings.shortcutMovePlayheadForward, settings.shortcutShortenFrame, - settings.shortcutExtendFrame, settings.shortcutPreviousFrame, - settings.shortcutNextFrame, settings.shortcutPreviousItem, - settings.shortcutNextItem)); + auto tooltipShortcuts = std::vformat( + localize.get(TOOLTIP_TIMELINE_SHORTCUTS), + std::make_format_args(settings.shortcutMovePlayheadBack, settings.shortcutMovePlayheadForward, + settings.shortcutShortenFrame, settings.shortcutExtendFrame, + settings.shortcutPreviousFrame, settings.shortcutNextFrame, + settings.shortcutPreviousItem, settings.shortcutNextItem)); ImGui::SetItemTooltip("%s", tooltipShortcuts.c_str()); ImGui::EndDisabled(); } @@ -1001,6 +2130,24 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Items List Child", itemsListChildSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) { + if (animation && ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + auto rowReferences = timeline_row_references_get(); + row_selection_clear(); + for (auto rowReference : rowReferences) + row_selection_insert(rowReference); + if (!rowReferences.empty()) + { + rowSelectionAnchor = rowReferences.front(); + isRowSelectionAnchorSet = true; + } + frames_selection_reset_for(document); + } + + if (animation && shortcut(manager.chords[SHORTCUT_GROUP], shortcut::FOCUSED) && + !item_references_groupable_get().empty()) + item_group(); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 0.0f); if (ImGui::BeginTable("##Item Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) @@ -1011,36 +2158,18 @@ namespace anm2ed::imgui ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("##Items"); - auto item_child_row = [&](anm2::Type type, int id = -1, int index = 0) + auto item_child_row = [&](const TimelineItemRow& row, int index) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - item_child(type, id, index); + item_child(row, index); }; - item_child_row(anm2::NONE); - int index{}; + item_child_row({.type = NONE}, index++); if (animation) - { - item_child_row(anm2::ROOT, -1, index++); - - for (auto& id : animation->layerOrder | std::views::reverse) - { - auto item = animation->item_get(anm2::LAYER, id); - if (!item) continue; - if (!settings.timelineIsShowUnused && item->frames.empty()) continue; - item_child_row(anm2::LAYER, id, index++); - } - - for (auto& [id, nullAnimation] : animation->nullAnimations) - { - if (!settings.timelineIsShowUnused && nullAnimation.frames.empty()) continue; - item_child_row(anm2::NULL_, id, index++); - } - - item_child_row(anm2::TRIGGER); - } + for (const auto& row : timeline_item_rows_get()) + item_child_row(row, index++); if (isHorizontalScroll && ImGui::GetCurrentWindow()->ScrollbarY) { @@ -1066,12 +2195,14 @@ namespace anm2ed::imgui ImGui::BeginDisabled(!animation); { shortcut(manager.chords[SHORTCUT_ADD]); - if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) - itemProperties.open(); + if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) itemProperties.open(); set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_ITEM), settings.shortcutAdd); ImGui::SameLine(); - ImGui::BeginDisabled(!document.item_get()); + auto selectedRows = selected_row_references_get(); + auto isRemoveAvailable = std::ranges::any_of(selectedRows, [](const TimelineRowReference& row) + { return row.type == LAYER || row.type == NULL_; }); + ImGui::BeginDisabled(!isRemoveAvailable); shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize)) item_remove(); set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_ITEMS), settings.shortcutRemove); @@ -1084,7 +2215,7 @@ namespace anm2ed::imgui ImGui::EndChild(); }; - anm2::Type frameMoveDropType = anm2::NONE; + int frameMoveDropType = NONE; int frameMoveDropItemID = -1; int frameMoveDropIndex = -1; bool isFrameMoveDropTarget = false; @@ -1095,97 +2226,140 @@ namespace anm2ed::imgui frameSelectionLocked.clear(); }; - auto time_from_index = [](anm2::Item* target, int index) - { - if (!target || target->frames.empty()) return 0.0f; - index = std::clamp(index, 0, (int)target->frames.size()); - float timeAccum = 0.0f; - for (int n = 0; n < index && n < (int)target->frames.size(); ++n) - timeAccum += target->frames[n].duration; - return timeAccum; - }; - - auto frames_move_to = [&](anm2::Type targetType, int targetID, int insertIndex) + auto frames_move_to = [&](int targetType, int targetID, int insertIndex) { if (!frameMoveDrag.isActive || !animation || frameMoveDrag.animationIndex != reference.animationIndex) return; - if (frameMoveDrag.type == anm2::TRIGGER || targetType == anm2::TRIGGER) return; + if (targetType == TRIGGER) return; - auto sourceItem = animation->item_get(frameMoveDrag.type, frameMoveDrag.itemID); - auto targetItem = animation->item_get(targetType, targetID); - if (!sourceItem || !targetItem) return; + auto drag = frameMoveDrag; + std::erase_if(drag.references, [](const Reference& frameReference) + { return frameReference.itemType == TRIGGER || frameReference.frameIndex < 0; }); + if (drag.references.empty() && drag.frameIndex >= 0) + drag.references.push_back({drag.animationIndex, drag.type, drag.itemID, drag.frameIndex}); + std::erase_if(drag.references, [](const Reference& frameReference) + { return frameReference.itemType == TRIGGER || frameReference.frameIndex < 0; }); + if (drag.references.empty()) return; - std::vector indices = frameMoveDrag.indices; - if (indices.empty() && frameMoveDrag.frameIndex >= 0) indices.push_back(frameMoveDrag.frameIndex); - std::sort(indices.begin(), indices.end()); - indices.erase(std::unique(indices.begin(), indices.end()), indices.end()); - indices.erase(std::remove_if(indices.begin(), indices.end(), - [&](int i) { return i < 0 || i >= (int)sourceItem->frames.size(); }), - indices.end()); - if (indices.empty()) return; + edit_command_push(EDIT_MOVE_FRAMES, Document::FRAMES, + [=, this](Manager&, Document& document) mutable + { + auto targetItem = command_item_get(document, drag.animationIndex, targetType, targetID); + if (!targetItem) return; - int insertPosResult = -1; - int insertedCount = 0; - DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_FRAMES), Document::FRAMES, { - std::vector movedFrames; - movedFrames.reserve(indices.size()); + std::map> groupedFrames{}; + for (auto frameReference : drag.references) + { + auto itemReference = item_reference_from_frame_get(frameReference); + groupedFrames[itemReference].insert(frameReference.frameIndex); + } - for (int i : indices) - movedFrames.push_back(std::move(sourceItem->frames[i])); + int removedBeforeTarget = 0; + std::vector movedFrames; + for (auto& [itemReference, indices] : groupedFrames) + { + auto sourceItem = command_item_get(document, itemReference.animationIndex, + itemReference.itemType, itemReference.itemID); + if (!sourceItem) continue; - for (auto it = indices.rbegin(); it != indices.rend(); ++it) - sourceItem->frames.erase(sourceItem->frames.begin() + *it); + for (auto i : indices) + { + if (i < 0 || i >= (int)sourceItem->children.size()) continue; + movedFrames.push_back(std::move(sourceItem->children[i])); + if (itemReference.itemType == targetType && itemReference.itemID == targetID && + i < insertIndex) + ++removedBeforeTarget; + } - int desired = std::clamp(insertIndex, 0, (int)targetItem->frames.size()); - if (sourceItem == targetItem) - { - int removedBefore = 0; - for (int i : indices) - if (i < desired) ++removedBefore; - desired -= removedBefore; - } - desired = std::clamp(desired, 0, (int)targetItem->frames.size()); + for (auto it = indices.rbegin(); it != indices.rend(); ++it) + { + auto i = *it; + if (i >= 0 && i < (int)sourceItem->children.size()) + sourceItem->children.erase(sourceItem->children.begin() + i); + } + } - insertPosResult = desired; - insertedCount = (int)movedFrames.size(); - targetItem->frames.insert(targetItem->frames.begin() + insertPosResult, - std::make_move_iterator(movedFrames.begin()), - std::make_move_iterator(movedFrames.end())); - }); + if (movedFrames.empty()) return; - if (insertedCount > 0) - { - frames.selection.clear(); - for (int offset = 0; offset < insertedCount; ++offset) - frames.selection.insert(insertPosResult + offset); + int desired = std::clamp(insertIndex, 0, (int)targetItem->children.size()); + desired -= removedBeforeTarget; + desired = std::clamp(desired, 0, (int)targetItem->children.size()); - reference = {reference.animationIndex, targetType, targetID, insertPosResult}; - document.frameTime = time_from_index(targetItem, reference.frameIndex); - frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); - frameSelectionSnapshotReference = reference; - frameSelectionLocked.clear(); - isFrameSelectionLocked = false; - frameFocusIndex = reference.frameIndex; - frameFocusRequested = true; - if (targetType == anm2::LAYER) - { - if (auto it = anm2.content.layers.find(targetID); it != anm2.content.layers.end()) - document.spritesheet.reference = it->second.spritesheetID; - } - } + auto insertPosResult = desired; + auto insertedCount = (int)movedFrames.size(); + targetItem->children.insert(targetItem->children.begin() + insertPosResult, + std::make_move_iterator(movedFrames.begin()), + std::make_move_iterator(movedFrames.end())); + + if (insertedCount <= 0) return; + + document.frames.selection.clear(); + document.frames.references.clear(); + for (int offset = 0; offset < insertedCount; ++offset) + { + document.frames.selection.insert(insertPosResult + offset); + document.frames.references.insert( + {drag.animationIndex, targetType, targetID, insertPosResult + offset}); + } + + document.reference = {drag.animationIndex, targetType, targetID, insertPosResult}; + document.frameTime = frame_time_from_index_get(*targetItem, document.reference.frameIndex); + frameSelectionSnapshot.assign(document.frames.selection.begin(), + document.frames.selection.end()); + frameSelectionSnapshotReference = document.reference; + frameSelectionLocked.clear(); + isFrameSelectionLocked = false; + frameFocusIndex = document.reference.frameIndex; + frameFocusRequested = true; + if (targetType == LAYER) + if (auto layer = command_layer_get(document, targetID)) + document.spritesheet.reference = layer->spritesheetId; + }); }; float playheadLineCenterX{}; float playheadLineTopY{}; bool isPlayheadLineSet{}; - - auto frame_child = [&](anm2::Type type, int id, int& index, float width) + ImVec2 frameBoxClipMin{}; + ImVec2 frameBoxClipMax{}; + bool isFrameBoxClipSet{}; + auto frame_box_content_point_get = [&]() { - auto item = animation ? animation->item_get(type, id) : nullptr; - if (type != anm2::NONE && !item) return; + auto mousePos = ImGui::GetIO().MousePos; + return ImVec2(mousePos.x + scroll.x - frameBoxClipMin.x, mousePos.y + scroll.y - frameBoxClipMin.y); + }; + auto frame_box_screen_point_get = [&](ImVec2 point) + { return ImVec2(point.x - scroll.x + frameBoxClipMin.x, point.y - scroll.y + frameBoxClipMin.y); }; + auto is_frame_box_overlapping = [](ImVec2 leftMin, ImVec2 leftMax, ImVec2 rightMin, ImVec2 rightMax) + { + return leftMin.x <= rightMax.x && leftMax.x >= rightMin.x && leftMin.y <= rightMax.y && + leftMax.y >= rightMin.y; + }; - auto isVisible = item ? item->isVisible : false; + auto frame_child = [&](const TimelineItemRow& row, int& index, float width) + { + auto type = row.type; + auto id = row.id; + auto childSize = ImVec2(width, rowFrameChildHeight); + if (row.isGroup) + { + auto group = row_group_get(row); + if (!group) return; + + ImGui::PushID(index); + ImGui::BeginChild("##Frames Group Child", childSize, ImGuiChildFlags_None, + ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar); + ImGui::EndChild(); + index++; + ImGui::PopID(); + return; + } + + auto item = item_get(type, id); + if (type != NONE && !item) return; + + auto isVisible = item ? item->isVisible && is_track_group_visible(type, row.groupId) : false; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; - if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; + if (isOnlyShowLayers && type != LAYER) isVisible = false; auto colorVec = type_color_base_vec(type); auto colorActiveVec = type_color_active_vec(type); @@ -1200,14 +2374,12 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - auto childSize = ImVec2(width, itemFrameChildHeight); - ImGui::PopStyleVar(2); ImGui::PushID(index); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); - bool isDefaultChild = type == anm2::NONE; + bool isDefaultChild = type == NONE; if (isLightTheme && isDefaultChild) ImGui::PushStyleColor(ImGuiCol_ChildBg, TIMELINE_CHILD_BG_COLOR_LIGHT); if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) @@ -1215,12 +2387,12 @@ namespace anm2ed::imgui if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) { - if (!frames.selection.empty()) + if (!frames.references.empty() || !frames.selection.empty()) { reference.frameIndex = -1; - frames_selection_set_reference(); + frames_selection_reset_for(document); } - else if (reference.itemType != anm2::NONE || reference.itemID != -1) + else if (reference.itemType != NONE || reference.itemID != -1) reference_clear(); } @@ -1234,10 +2406,10 @@ namespace anm2ed::imgui auto border = glm::max(0.5f, ImGui::GetStyle().FrameBorderSize * 0.5f); auto borderLineLength = frameSize.y / 5; auto frameMin = std::max(0, (int)std::floor(scroll.x / frameSize.x) - 1); - auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil((scroll.x + clipMax.x) / frameSize.x) + 1); + auto frameMax = std::min(FRAME_NUM_MAX, (int)std::ceil((scroll.x + clipMax.x) / frameSize.x) + 1); pickerLineDrawList = drawList; - if (type == anm2::NONE) + if (type == NONE) { if (length > 0) { @@ -1299,11 +2471,11 @@ namespace anm2ed::imgui if (isDragging) { playback.time = hoveredTime; - playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX); + playback.clamp(settings.playbackIsClamp ? length : FRAME_NUM_MAX); document.frameTime = playback.time; } - if (!playback.isPlaying) playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX); + if (!playback.isPlaying) playback.clamp(settings.playbackIsClamp ? length : FRAME_NUM_MAX); if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) isDragging = false; @@ -1323,7 +2495,7 @@ namespace anm2ed::imgui { float frameTime{}; - if (!frameMoveDrag.isActive && ImGui::IsWindowHovered() && + if (!isFrameBoxSelecting && !frameMoveDrag.isActive && ImGui::IsWindowHovered() && (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right)) && !ImGui::IsAnyItemHovered()) reference_set_item(type, id); @@ -1339,8 +2511,16 @@ namespace anm2ed::imgui drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(frameMultipleOverlayColor)); } - bool isFrameSelectionStarted = type != anm2::TRIGGER && !frameMoveDrag.isActive; - if (isFrameSelectionStarted) frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); + if (!frameMoveDrag.isActive && !ImGui::IsKeyDown(ImGuiMod_Shift) && + ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && + ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsAnyItemHovered()) + { + isFrameBoxSelecting = true; + isFrameBoxAdditive = ImGui::IsKeyDown(ImGuiMod_Ctrl); + frameBoxStart = frame_box_content_point_get(); + frameBoxEnd = frameBoxStart; + frameBoxSelection.clear(); + } bool isFrameMovePreview = false; ImVec2 frameMovePreviewMin{}; @@ -1349,7 +2529,7 @@ namespace anm2ed::imgui ImVec2 frameMoveHoveredFrameMin{}; ImVec2 frameMoveHoveredFrameMax{}; - if (frameMoveDrag.isActive && type != anm2::TRIGGER) + if (frameMoveDrag.isActive && type != TRIGGER) { auto mousePos = ImGui::GetIO().MousePos; auto rowMin = cursorScreenPos; @@ -1358,11 +2538,11 @@ namespace anm2ed::imgui { auto mouseX = mousePos.x - cursorScreenPos.x; auto targetTime = glm::max(0.0f, mouseX / frameSize.x); - int dropIndex = (int)item->frames.size(); + int dropIndex = (int)item->children.size(); float dropFrameTime{}; float frameTime{}; - for (auto [i, frame] : std::views::enumerate(item->frames)) + for (auto [i, frame] : std::views::enumerate(item->children)) { auto frameStart = frameTime; auto frameEnd = frameStart + frame.duration; @@ -1370,8 +2550,8 @@ namespace anm2ed::imgui { isFrameMoveHoveredFrame = true; frameMoveHoveredFrameMin = ImVec2(cursorScreenPos.x + frameStart * frameSize.x, cursorScreenPos.y); - frameMoveHoveredFrameMax = ImVec2(cursorScreenPos.x + frameEnd * frameSize.x, - cursorScreenPos.y + frameSize.y); + frameMoveHoveredFrameMax = + ImVec2(cursorScreenPos.x + frameEnd * frameSize.x, cursorScreenPos.y + frameSize.y); } auto midpoint = frameStart + ((float)frame.duration * 0.5f); @@ -1399,25 +2579,35 @@ namespace anm2ed::imgui } } - for (auto [i, frame] : std::views::enumerate(item->frames)) + for (auto [i, frame] : std::views::enumerate(item->children)) { ImGui::PushID((int)i); - auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)i}; + auto frameReference = Reference{reference.animationIndex, type, id, (int)i}; auto isFrameVisible = isVisible && frame.isVisible; auto isReferenced = reference == frameReference; - auto isSelected = - (frames.selection.contains((int)i) && reference.itemType == type && reference.itemID == id); + auto isSelected = frames.references.contains(frameReference) || + (frames.references.empty() && frames.selection.contains((int)i) && + reference.itemType == type && reference.itemID == id); - if (type == anm2::TRIGGER) frameTime = frame.atFrame; + if (type == TRIGGER) frameTime = frame.atFrame; - auto buttonSize = - type == anm2::TRIGGER ? frameSize : to_imvec2(vec2(frameSize.x * frame.duration, frameSize.y)); - auto frameStart = type == anm2::TRIGGER ? frame.atFrame : frameTime; - auto frameEnd = type == anm2::TRIGGER ? frameStart + 1.0f : frameStart + frame.duration; + auto buttonSize = type == TRIGGER ? frameSize : to_imvec2(vec2(frameSize.x * frame.duration, frameSize.y)); + auto frameStart = type == TRIGGER ? frame.atFrame : frameTime; + auto frameEnd = type == TRIGGER ? frameStart + 1.0f : frameStart + frame.duration; + if (isFrameBoxSelecting && isFrameBoxClipSet) + { + auto boxMin = ImVec2(std::min(frameBoxStart.x, frameBoxEnd.x), std::min(frameBoxStart.y, frameBoxEnd.y)); + auto boxMax = ImVec2(std::max(frameBoxStart.x, frameBoxEnd.x), std::max(frameBoxStart.y, frameBoxEnd.y)); + auto rowMinY = cursorScreenPos.y + scroll.y - frameBoxClipMin.y; + auto frameContentMin = ImVec2(frameStart * frameSize.x, rowMinY); + auto frameContentMax = ImVec2(frameEnd * frameSize.x, rowMinY + frameSize.y); + if (is_frame_box_overlapping(frameContentMin, frameContentMax, boxMin, boxMax)) + frameBoxSelection.insert(frameReference); + } if (frameEnd <= (float)frameMin || frameStart >= (float)frameMax) { - if (type != anm2::TRIGGER) frameTime += frame.duration; + if (type != TRIGGER) frameTime += frame.duration; ImGui::PopID(); continue; } @@ -1446,26 +2636,57 @@ namespace anm2ed::imgui bool isDifferentItem = reference.itemType != type || reference.itemID != id; if (ImGui::Selectable("##Frame Button", isSelected, ImGuiSelectableFlags_None, buttonSize)) { - if (type == anm2::LAYER) - if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end()) - document.spritesheet.reference = it->second.spritesheetID; + if (type == LAYER) + if (auto layer = layer_get(id)) document.spritesheet.reference = layer->spritesheetId; - if (type != anm2::TRIGGER) + if (type != TRIGGER) { if (ImGui::IsKeyDown(ImGuiMod_Alt)) - DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_INTERPOLATION), Document::FRAMES, - frame.interpolation = frame.interpolation == anm2::Frame::Interpolation::NONE - ? anm2::Frame::Interpolation::LINEAR - : anm2::Frame::Interpolation::NONE); + { + auto targetReference = frameReference; + edit_command_push(EDIT_FRAME_INTERPOLATION, Document::FRAMES, + [=](Manager&, Document& document) + { + auto frame = command_frame_get(document, targetReference); + if (!frame) return; + frame->interpolation = frame->interpolation == Interpolation::NONE + ? Interpolation::LINEAR + : Interpolation::NONE; + }); + } document.frameTime = frameTime; } + auto isCtrlDown = ImGui::IsKeyDown(ImGuiMod_Ctrl); + auto isShiftDown = ImGui::IsKeyDown(ImGuiMod_Shift); + if (isShiftDown) + { + auto isHadAnchor = isFrameSelectionAnchorSet; + auto anchorReference = isHadAnchor ? frameSelectionAnchor : frameReference; + auto isRangeSelected = + frame_selection_range_set_for(document, anchorReference, frameReference, isCtrlDown); + if (!isRangeSelected) frame_selection_set_for(document, frameReference); + if (!isHadAnchor || !isRangeSelected) frameSelectionAnchor = frameReference; + isFrameSelectionAnchorSet = true; + } + else if (isCtrlDown) + { + frame_selection_toggle_for(document, frameReference); + frameSelectionAnchor = frameReference; + isFrameSelectionAnchorSet = true; + } + else + { + frame_selection_set_for(document, frameReference); + frameSelectionAnchor = frameReference; + isFrameSelectionAnchorSet = true; + } reference = frameReference; isReferenced = true; region.reference = -1; region.selection.clear(); - if (isDifferentItem) frames_selection_set_reference(); + if (isDifferentItem) frames_selection_sync_for(document); } ImGui::PopStyleVar(); @@ -1475,62 +2696,36 @@ namespace anm2ed::imgui { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - if (type == anm2::TRIGGER || ImGui::IsKeyDown(ImGuiMod_Ctrl)) + if (type == TRIGGER || ImGui::IsKeyDown(ImGuiMod_Ctrl)) { - draggedFrame = &frame; + isDraggedFrameActive = true; + draggedFrameReference = frameReference; draggedFrameType = type; draggedFrameIndex = (int)i; draggedFrameStart = hoveredTime; - if (type != anm2::TRIGGER) draggedFrameStartDuration = draggedFrame->duration; + if (type != TRIGGER) draggedFrameStartDuration = frame.duration; } } } - if (type != anm2::TRIGGER) + if (type != TRIGGER) { - if (!draggedFrame && !frameMoveDrag.isActive && ImGui::IsItemActive() && + if (!isDraggedFrameActive && !frameMoveDrag.isActive && ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { - frameSelectionLocked.clear(); - - auto append_valid_indices = [&](const auto& container) - { - for (auto idx : container) - if (idx >= 0 && idx < (int)item->frames.size()) frameSelectionLocked.push_back(idx); - }; - - if (frameSelectionSnapshotReference.animationIndex == reference.animationIndex && - frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id && - std::find(frameSelectionSnapshot.begin(), frameSelectionSnapshot.end(), (int)i) != - frameSelectionSnapshot.end()) - append_valid_indices(frameSelectionSnapshot); - else if (isReferenced) - append_valid_indices(frames.selection); - - auto contains_index = [&](const std::vector& container, int index) - { return std::find(container.begin(), container.end(), index) != container.end(); }; - - if ((!contains_index(frameSelectionLocked, (int)i) || frameSelectionLocked.size() <= 1) && - frameSelectionSnapshotReference.animationIndex == reference.animationIndex && - frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id && - contains_index(frameSelectionSnapshot, (int)i)) - { - frameSelectionLocked = frameSelectionSnapshot; - frames.selection.clear(); - for (int idx : frameSelectionSnapshot) - if (idx >= 0 && idx < (int)item->frames.size()) frames.selection.insert(idx); - isFrameSelectionLocked = true; - } - - if (frameSelectionLocked.empty()) frameSelectionLocked.push_back((int)i); - - std::sort(frameSelectionLocked.begin(), frameSelectionLocked.end()); - frameSelectionLocked.erase(std::unique(frameSelectionLocked.begin(), frameSelectionLocked.end()), - frameSelectionLocked.end()); - + auto selectedReferences = frame_references_for_current_get(); + if (!selectedReferences.contains(frameReference)) selectedReferences = {frameReference}; + std::erase_if(selectedReferences, [](const Reference& selectedReference) + { return selectedReference.itemType == TRIGGER; }); + if (selectedReferences.empty()) selectedReferences = {frameReference}; int dragDuration = 0; - for (int idx : frameSelectionLocked) - if (idx >= 0 && idx < (int)item->frames.size()) dragDuration += item->frames[idx].duration; + for (auto selectedReference : selectedReferences) + { + auto selectedItem = item_get(selectedReference.itemType, selectedReference.itemID); + auto selectedFrame = + selectedItem ? track_frame_get(*selectedItem, selectedReference.frameIndex) : nullptr; + if (selectedFrame) dragDuration += selectedFrame->duration; + } dragDuration = glm::max(1, dragDuration); frameMoveDrag = { @@ -1539,7 +2734,8 @@ namespace anm2ed::imgui .animationIndex = reference.animationIndex, .frameIndex = (int)i, .duration = dragDuration, - .indices = frameSelectionLocked, + .indices = {}, + .references = {selectedReferences.begin(), selectedReferences.end()}, .isActive = true, }; } @@ -1552,25 +2748,25 @@ namespace anm2ed::imgui drawList->AddRect(rectMin, rectMax, ImGui::GetColorU32(borderColor), FRAME_ROUNDING, 0, borderThickness); auto icon = icon::UNINTERPOLATED; - if (type == anm2::TRIGGER) + if (type == TRIGGER) icon = icon::TRIGGER; else { switch (frame.interpolation) { - case anm2::Frame::Interpolation::NONE: + case Interpolation::NONE: icon = icon::UNINTERPOLATED; break; - case anm2::Frame::Interpolation::LINEAR: + case Interpolation::LINEAR: icon = icon::INTERPOLATED; break; - case anm2::Frame::Interpolation::EASE_IN: + case Interpolation::EASE_IN: icon = icon::EASE_IN; break; - case anm2::Frame::Interpolation::EASE_OUT: + case Interpolation::EASE_OUT: icon = icon::EASE_OUT; break; - case anm2::Frame::Interpolation::EASE_IN_OUT: + case Interpolation::EASE_IN_OUT: icon = icon::EASE_IN_OUT; break; default: @@ -1584,7 +2780,7 @@ namespace anm2ed::imgui ImGui::Image(resources.icons[icon].id, icon_size_get()); overlay_icon(resources.icons[icon].id, iconTintDefault); - if (type != anm2::TRIGGER) frameTime += frame.duration; + if (type != TRIGGER) frameTime += frame.duration; ImGui::PopID(); } @@ -1602,13 +2798,15 @@ namespace anm2ed::imgui ImGui::GetColorU32(ImGuiCol_DragDropTarget), FRAME_ROUNDING, 0, ImGui::GetStyle().DragDropTargetBorderSize * 1.5f); - if (isFrameSelectionStarted) frames.selection.finish(); - if (isFrameSelectionLocked) { frames.selection.clear(); + frames.references.clear(); for (int idx : frameSelectionLocked) + { frames.selection.insert(idx); + frames.references.insert({reference.animationIndex, type, id, idx}); + } isFrameSelectionLocked = false; frameSelectionLocked.clear(); } @@ -1620,45 +2818,72 @@ namespace anm2ed::imgui } } - if (draggedFrame) + if (isDraggedFrameActive) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (!isDraggedFrameSnapshot && hoveredTime != draggedFrameStart) { isDraggedFrameSnapshot = true; - document.snapshot(draggedFrameType == anm2::TRIGGER ? localize.get(EDIT_TRIGGER_AT_FRAME) - : localize.get(EDIT_FRAME_DURATION)); + snapshot_command_push(draggedFrameType == TRIGGER ? EDIT_TRIGGER_AT_FRAME : EDIT_FRAME_DURATION); } - if (draggedFrameType == anm2::TRIGGER) + if (isDraggedFrameSnapshot) { - draggedFrame->atFrame = - glm::clamp(hoveredTime, 0, settings.playbackIsClamp ? animation->frameNum - 1 : anm2::FRAME_NUM_MAX - 1); + auto targetReference = draggedFrameReference; + auto targetType = draggedFrameType; + auto targetIndex = draggedFrameIndex; + auto targetStart = draggedFrameStart; + auto targetStartDuration = draggedFrameStartDuration; + auto targetHoveredTime = hoveredTime; + auto isPlaybackClamp = settings.playbackIsClamp; + auto animationLength = animation ? animation->frameNum : FRAME_NUM_MAX; + command_push([=](Manager&, Document& document) + { + auto item = command_item_get(document, targetReference.animationIndex, + targetReference.itemType, targetReference.itemID); + auto frame = command_frame_get(document, targetReference); + if (!item || !frame) return; - for (auto [i, trigger] : std::views::enumerate(animation->triggers.frames)) - { - if ((int)i == draggedFrameIndex) continue; - if (trigger.atFrame == draggedFrame->atFrame) draggedFrame->atFrame--; - } - } - else - { - draggedFrame->duration = glm::clamp(draggedFrameStartDuration + (hoveredTime - draggedFrameStart), - anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX); + if (targetType == TRIGGER) + { + frame->atFrame = + glm::clamp(targetHoveredTime, 0, + isPlaybackClamp ? animationLength - 1 : FRAME_NUM_MAX - 1); + + for (auto [i, trigger] : std::views::enumerate(item->children)) + { + if ((int)i == targetIndex) continue; + if (trigger.atFrame == frame->atFrame) frame->atFrame--; + } + } + else + { + frame->duration = glm::clamp(targetStartDuration + (targetHoveredTime - targetStart), + FRAME_DURATION_MIN, FRAME_DURATION_MAX); + } + }); } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - document.change(Document::FRAMES); - draggedFrame = nullptr; - draggedFrameType = anm2::NONE; + auto targetReference = draggedFrameReference; + auto targetType = draggedFrameType; + command_push([=](Manager&, Document& document) + { + auto item = command_item_get(document, targetReference.animationIndex, + targetReference.itemType, targetReference.itemID); + if (targetType == TRIGGER && item) frames_sort_by_at_frame(*item); + document.anm2_change(Document::FRAMES); + }); + isDraggedFrameActive = false; + draggedFrameReference = {}; + draggedFrameType = NONE; draggedFrameIndex = -1; draggedFrameStart = -1; draggedFrameStartDuration = -1; isDraggedFrameSnapshot = false; frameSelectionLocked.clear(); - if (type == anm2::TRIGGER) item->frames_sort_by_at_frame(); } } @@ -1685,8 +2910,16 @@ namespace anm2ed::imgui ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - style.ItemSpacing.y * 2); - auto childWidth = anm2.animations.length() * ImGui::GetTextLineHeight(); - if (animation && animation->frameNum > anm2.animations.length()) + auto animationsLength = [&]() + { + int length{}; + if (auto animations = anm2.element_get(ElementType::ANIMATIONS)) + for (auto& item : animations->children) + if (item.type == ElementType::ANIMATION) length = std::max(length, animation_length_get(item)); + return length; + }(); + auto childWidth = animationsLength * ImGui::GetTextLineHeight(); + if (animation && animation->frameNum > animationsLength) childWidth = animation->frameNum * ImGui::GetTextLineHeight(); childWidth = std::max(childWidth, ImGui::GetContentRegionAvail().x); @@ -1698,6 +2931,17 @@ namespace anm2ed::imgui playheadLineCenterX = 0.0f; playheadLineTopY = 0.0f; isPlayheadLineSet = false; + isFrameBoxClipSet = false; + if (animation && ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + group_selection_reset(); + document.frames.references = all_frame_references_for_items_get(); + if (!document.frames.references.empty()) + { + reference = *document.frames.references.begin(); + frames_selection_sync_for(document); + } + } ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); if (ImGui::BeginTable("##Frames List Table", 1, @@ -1723,43 +2967,49 @@ namespace anm2ed::imgui ImGui::SetScrollX(scroll.x); ImGui::SetScrollY(scroll.y); + auto frameBoxDrawList = ImGui::GetWindowDrawList(); + frameBoxClipMin = frameBoxDrawList->GetClipRectMin(); + frameBoxClipMax = frameBoxDrawList->GetClipRectMax(); + isFrameBoxClipSet = true; + if (isFrameBoxSelecting) + { + auto& io = ImGui::GetIO(); + auto edgeSize = ImGui::GetTextLineHeightWithSpacing(); + auto scrollStep = edgeSize * 0.5f; + if (io.MousePos.x < frameBoxClipMin.x + edgeSize) + scroll.x -= scrollStep; + else if (io.MousePos.x > frameBoxClipMax.x - edgeSize) + scroll.x += scrollStep; + if (io.MousePos.y < frameBoxClipMin.y + edgeSize) + scroll.y -= scrollStep; + else if (io.MousePos.y > frameBoxClipMax.y - edgeSize) + scroll.y += scrollStep; + ImGui::SetScrollX(scroll.x); + ImGui::SetScrollY(scroll.y); + frameBoxEnd = frame_box_content_point_get(); + frameBoxSelection.clear(); + } + int index{}; ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("##Frames"); - auto frames_child_row = [&](anm2::Type type, int id = -1) + auto frames_child_row = [&](const TimelineItemRow& row) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - frame_child(type, id, index, childWidth); + frame_child(row, index, childWidth); }; - frames_child_row(anm2::NONE); + frames_child_row({.type = NONE}); if (animation) { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); - frames_child_row(anm2::ROOT); - - for (auto& id : animation->layerOrder | std::views::reverse) - { - if (auto item = animation->item_get(anm2::LAYER, id); item) - if (!settings.timelineIsShowUnused && item->frames.empty()) continue; - - frames_child_row(anm2::LAYER, id); - } - - for (const auto& entry : animation->nullAnimations) - { - auto id = entry.first; - if (auto item = animation->item_get(anm2::NULL_, id); item) - if (!settings.timelineIsShowUnused && item->frames.empty()) continue; - frames_child_row(anm2::NULL_, id); - } - - frames_child_row(anm2::TRIGGER); + for (const auto& row : timeline_item_rows_get()) + frames_child_row(row); ImGui::PopStyleVar(); } @@ -1767,22 +3017,55 @@ namespace anm2ed::imgui } ImDrawList* windowDrawList = ImGui::GetWindowDrawList(); - + ImDrawList* foregroundDrawList = ImGui::GetForegroundDrawList(); auto lineBottomY = ImGui::GetWindowPos().y + ImGui::GetWindowSize().y; if (isHorizontalScroll) lineBottomY -= ImGui::GetStyle().ScrollbarSize; auto rectMin = windowDrawList->GetClipRectMin(); auto rectMax = windowDrawList->GetClipRectMax(); + if (isFrameBoxSelecting && isFrameBoxClipSet) + { + auto boxContentMin = + ImVec2(std::min(frameBoxStart.x, frameBoxEnd.x), std::min(frameBoxStart.y, frameBoxEnd.y)); + auto boxContentMax = + ImVec2(std::max(frameBoxStart.x, frameBoxEnd.x), std::max(frameBoxStart.y, frameBoxEnd.y)); + auto boxMin = frame_box_screen_point_get(boxContentMin); + auto boxMax = frame_box_screen_point_get(boxContentMax); + auto boxClipMin = rectMin; + if (isPlayheadLineSet) boxClipMin.y = std::max(boxClipMin.y, playheadLineTopY); + foregroundDrawList->PushClipRect(boxClipMin, rectMax, true); + foregroundDrawList->AddRectFilled(boxMin, boxMax, ImGui::GetColorU32(ImGuiCol_DragDropTargetBg)); + foregroundDrawList->AddRect(boxMin, boxMax, ImGui::GetColorU32(ImGuiCol_DragDropTarget)); + foregroundDrawList->PopClipRect(); + } + if (pickerLineDrawList && isPlayheadLineSet && lineBottomY > playheadLineTopY) { - auto linePos = - ImVec2(playheadLineCenterX - (PLAYHEAD_LINE_THICKNESS * 0.5f), playheadLineTopY); - auto lineSize = - ImVec2((PLAYHEAD_LINE_THICKNESS / 2.0f), std::max(0.0f, lineBottomY - playheadLineTopY)); - pickerLineDrawList->PushClipRect(rectMin, rectMax); - pickerLineDrawList->AddRectFilled(linePos, ImVec2(linePos.x + lineSize.x, linePos.y + lineSize.y), + auto linePos = ImVec2(playheadLineCenterX - (PLAYHEAD_LINE_THICKNESS * 0.5f), playheadLineTopY); + auto lineSize = ImVec2((PLAYHEAD_LINE_THICKNESS / 2.0f), std::max(0.0f, lineBottomY - playheadLineTopY)); + foregroundDrawList->PushClipRect(rectMin, rectMax, true); + foregroundDrawList->AddRectFilled(linePos, ImVec2(linePos.x + lineSize.x, linePos.y + lineSize.y), ImGui::GetColorU32(playheadLineColor)); - pickerLineDrawList->PopClipRect(); + foregroundDrawList->PopClipRect(); + } + + if (isFrameBoxSelecting) + { + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) + { + group_selection_reset(); + if (isFrameBoxAdditive) + document.frames.references.insert(frameBoxSelection.begin(), frameBoxSelection.end()); + else + document.frames.references = frameBoxSelection; + document.items.references.clear(); + for (auto frameReference : document.frames.references) + document.items.references.insert(item_reference_from_frame_get(frameReference)); + if (!document.frames.references.empty()) reference = *document.frames.references.begin(); + frames_selection_sync_for(document); + isFrameBoxSelecting = false; + frameBoxSelection.clear(); + } } ImGui::PopStyleVar(); @@ -1809,17 +3092,21 @@ namespace anm2ed::imgui ImGui::SameLine(); - auto item = animation ? animation->item_get(reference.itemType, reference.itemID) : nullptr; + auto item = selected_item_get(); + auto selectedFrames = frame_references_for_current_get(); + auto selectedBakeFrames = selectedFrames; + std::erase_if(selectedBakeFrames, [](const Reference& frameReference) + { return frameReference.itemType == TRIGGER; }); ImGui::BeginDisabled(!item); { shortcut(manager.chords[SHORTCUT_INSERT_FRAME]); - if (ImGui::Button(localize.get(LABEL_INSERT), widgetSize)) frame_insert(item); + if (ImGui::Button(localize.get(LABEL_INSERT), widgetSize)) frame_insert(); set_item_tooltip_shortcut(localize.get(TOOLTIP_INSERT_FRAME), settings.shortcutInsertFrame); ImGui::SameLine(); - ImGui::BeginDisabled(!document.frame_get()); + ImGui::BeginDisabled(selectedFrames.empty()); { shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button(localize.get(LABEL_DELETE), widgetSize)) frames_delete_action(); @@ -1827,8 +3114,10 @@ namespace anm2ed::imgui ImGui::SameLine(); + ImGui::BeginDisabled(selectedBakeFrames.empty()); if (ImGui::Button(localize.get(LABEL_BAKE), widgetSize)) bakePopup.open(); set_item_tooltip_shortcut(localize.get(TOOLTIP_BAKE_FRAMES), settings.shortcutBake); + ImGui::EndDisabled(); } ImGui::EndDisabled(); } @@ -1836,7 +3125,7 @@ namespace anm2ed::imgui ImGui::SameLine(); - ImGui::BeginDisabled(!animation || animation->frameNum == animation->length()); + ImGui::BeginDisabled(!animation || animation->frameNum == animation_length_get(*animation)); shortcut(manager.chords[SHORTCUT_FIT]); if (ImGui::Button(localize.get(LABEL_FIT_ANIMATION_LENGTH), widgetSize)) fit_animation_length(); set_item_tooltip_shortcut(localize.get(TOOLTIP_FIT_ANIMATION_LENGTH), settings.shortcutFit); @@ -1846,11 +3135,19 @@ namespace anm2ed::imgui auto frameNum = animation ? animation->frameNum : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); - if (input_int_range(localize.get(LABEL_ANIMATION_LENGTH), frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX, - STEP, STEP_FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) && + if (input_int_range(localize.get(LABEL_ANIMATION_LENGTH), frameNum, FRAME_NUM_MIN, FRAME_NUM_MAX, STEP, + STEP_FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) && animation) - DOCUMENT_EDIT(document, localize.get(EDIT_ANIMATION_LENGTH), Document::ANIMATIONS, - animation->frameNum = frameNum); + { + auto animationIndex = reference.animationIndex; + edit_command_push(EDIT_ANIMATION_LENGTH, Document::ANIMATIONS, + [=](Manager&, Document& document) + { + auto animation = command_animation_get(document, animationIndex); + if (!animation) return; + animation->frameNum = frameNum; + }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ANIMATION_LENGTH)); ImGui::SameLine(); @@ -1858,25 +3155,60 @@ namespace anm2ed::imgui auto isLoop = animation ? animation->isLoop : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); if (ImGui::Checkbox(localize.get(LABEL_LOOP), &isLoop) && animation) - DOCUMENT_EDIT(document, localize.get(EDIT_LOOP), Document::ANIMATIONS, animation->isLoop = isLoop); + { + auto animationIndex = reference.animationIndex; + edit_command_push(EDIT_LOOP, Document::ANIMATIONS, + [=](Manager&, Document& document) + { + auto animation = command_animation_get(document, animationIndex); + if (!animation) return; + animation->isLoop = isLoop; + }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LOOP_ANIMATION)); } ImGui::EndDisabled(); ImGui::SameLine(); - auto fps = anm2.info.fps; + auto info = info_get(); + auto fps = info ? info->fps : 30; ImGui::SetNextItemWidth(widgetSize.x); - if (input_int_range(localize.get(LABEL_FPS), fps, anm2::FPS_MIN, anm2::FPS_MAX)) - DOCUMENT_EDIT(document, localize.get(EDIT_FPS), Document::ANIMATIONS, anm2.info.fps = fps); + if (input_int_range(localize.get(LABEL_FPS), fps, FPS_MIN, FPS_MAX)) + { + edit_command_push(EDIT_FPS, Document::INFO, + [=](Manager&, Document& document) + { + auto info = command_info_get(document); + if (!info) + { + document.anm2.root.children.push_back(element_make(ElementType::INFO)); + info = &document.anm2.root.children.back(); + } + info->fps = fps; + }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FPS)); ImGui::SameLine(); - auto createdBy = anm2.info.createdBy; + info = info_get(); + auto createdBy = info ? info->createdBy : std::string{}; ImGui::SetNextItemWidth(widgetSize.x); if (input_text_string(localize.get(LABEL_AUTHOR), &createdBy)) - DOCUMENT_EDIT(document, localize.get(EDIT_AUTHOR), Document::ANIMATIONS, anm2.info.createdBy = createdBy); + { + edit_command_push(EDIT_AUTHOR, Document::INFO, + [=](Manager&, Document& document) + { + auto info = command_info_get(document); + if (!info) + { + document.anm2.root.children.push_back(element_make(ElementType::INFO)); + info = &document.anm2.root.children.back(); + } + info->createdBy = createdBy; + }); + } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTHOR)); ImGui::SameLine(); @@ -1908,7 +3240,8 @@ namespace anm2ed::imgui ImGui::PopStyleVar(); ImGui::End(); - itemProperties.update(manager, settings, document, animation, reference, reference_set_item); + itemProperties.update(manager, settings, document, reference, reference_set_item); + group_properties_update(); bakePopup.trigger(); @@ -1918,10 +3251,10 @@ namespace anm2ed::imgui auto& isRoundRotation = settings.bakeIsRoundRotation; auto& isRoundScale = settings.bakeIsRoundScale; - auto frame = document.frame_get(); + auto frame = frame_get(); - input_int_range(localize.get(LABEL_INTERVAL), interval, anm2::FRAME_DURATION_MIN, - frame ? frame->duration : anm2::FRAME_DURATION_MIN); + input_int_range(localize.get(LABEL_INTERVAL), interval, FRAME_DURATION_MIN, + frame ? frame->duration : FRAME_DURATION_MIN); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_INTERVAL)); ImGui::Checkbox(localize.get(LABEL_ROUND_ROTATION), &isRoundRotation); @@ -1956,42 +3289,64 @@ namespace anm2ed::imgui if (shortcut(manager.chords[SHORTCUT_MOVE_PLAYHEAD_BACK], shortcut::GLOBAL)) { playback_stop(); - playback.decrement(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); + playback.decrement(settings.playbackIsClamp ? animation->frameNum : FRAME_NUM_MAX); document.frameTime = playback.time; } if (shortcut(manager.chords[SHORTCUT_MOVE_PLAYHEAD_FORWARD], shortcut::GLOBAL)) { playback_stop(); - playback.increment(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); + playback.increment(settings.playbackIsClamp ? animation->frameNum : FRAME_NUM_MAX); document.frameTime = playback.time; } static bool isShortenChordHeld = false; auto isShortenFrame = shortcut(manager.chords[SHORTCUT_SHORTEN_FRAME], shortcut::GLOBAL); - if (isShortenFrame && !isShortenChordHeld) document.snapshot(localize.get(EDIT_SHORTEN_FRAME)); + if (isShortenFrame && !isShortenChordHeld) snapshot_command_push(EDIT_SHORTEN_FRAME); if (isShortenFrame) { - if (auto frame = document.frame_get()) + auto selectedFrames = frame_references_for_current_get(); + std::erase_if(selectedFrames, + [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); + if (!selectedFrames.empty()) { - frame->shorten(); - document.change(Document::FRAMES); + command_push([=](Manager&, Document& document) + { + for (auto frameReference : selectedFrames) + { + auto frame = command_frame_get(document, frameReference); + if (!frame) continue; + frame->duration = std::max(FRAME_DURATION_MIN, frame->duration - 1); + } + document.anm2_change(Document::FRAMES); + }); } } isShortenChordHeld = isShortenFrame; static bool isExtendChordHeld = false; auto isExtendFrame = shortcut(manager.chords[SHORTCUT_EXTEND_FRAME], shortcut::GLOBAL); - if (isExtendFrame && !isExtendChordHeld) document.snapshot(localize.get(EDIT_EXTEND_FRAME)); + if (isExtendFrame && !isExtendChordHeld) snapshot_command_push(EDIT_EXTEND_FRAME); if (isExtendFrame) { - if (auto frame = document.frame_get()) + auto selectedFrames = frame_references_for_current_get(); + std::erase_if(selectedFrames, + [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); + if (!selectedFrames.empty()) { - frame->extend(); - document.change(Document::FRAMES); + command_push([=](Manager&, Document& document) + { + for (auto frameReference : selectedFrames) + { + auto frame = command_frame_get(document, frameReference); + if (!frame) continue; + frame->duration = std::min(FRAME_DURATION_MAX, frame->duration + 1); + } + document.anm2_change(Document::FRAMES); + }); } } isExtendChordHeld = isExtendFrame; @@ -2002,19 +3357,19 @@ namespace anm2ed::imgui auto isNextItem = shortcut(manager.chords[SHORTCUT_NEXT_ITEM], shortcut::GLOBAL); if (isPreviousFrame) - if (auto item = document.item_get(); item && !item->frames.empty()) - reference.frameIndex = glm::clamp(--reference.frameIndex, 0, (int)item->frames.size() - 1); + if (auto item = selected_item_get(); item && !item->children.empty()) + reference.frameIndex = glm::clamp(--reference.frameIndex, 0, (int)item->children.size() - 1); if (isNextFrame) - if (auto item = document.item_get(); item && !item->frames.empty()) - reference.frameIndex = glm::clamp(++reference.frameIndex, 0, (int)item->frames.size() - 1); + if (auto item = selected_item_get(); item && !item->children.empty()) + reference.frameIndex = glm::clamp(++reference.frameIndex, 0, (int)item->children.size() - 1); if (isPreviousFrame || isNextFrame) { - if (auto item = document.item_get(); item && !item->frames.empty()) + if (auto item = selected_item_get(); item && !item->children.empty()) { frames_selection_set_reference(); - document.frameTime = item->frame_time_from_index_get(reference.frameIndex); + document.frameTime = frame_time_from_index_get(*item, reference.frameIndex); } } diff --git a/src/imgui/window/timeline.hpp b/src/imgui/window/timeline.hpp index 2ba736b..47e254d 100644 --- a/src/imgui/window/timeline.hpp +++ b/src/imgui/window/timeline.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "clipboard.hpp" @@ -8,17 +10,41 @@ #include "resources.hpp" #include "settings.hpp" #include "strings.hpp" +#include "util/imgui/popup.hpp" namespace anm2ed::imgui { + struct TimelineGroupReference + { + int documentIndex{-1}; + int animationIndex{-1}; + int type{NONE}; + int id{-1}; + + auto operator<=>(const TimelineGroupReference&) const = default; + }; + + struct TimelineRowReference + { + int documentIndex{-1}; + int animationIndex{-1}; + int type{NONE}; + int id{-1}; + int index{-1}; + bool isGroup{}; + + auto operator<=>(const TimelineRowReference&) const = default; + }; + struct FrameMoveDrag { - anm2::Type type{anm2::NONE}; + int type{NONE}; int itemID{-1}; int animationIndex{-1}; int frameIndex{-1}; int duration{1}; std::vector indices{}; + std::vector references{}; bool isActive{}; }; @@ -30,8 +56,22 @@ namespace anm2ed::imgui popup::ItemProperties itemProperties{}; PopupHelper bakePopup{PopupHelper(LABEL_TIMELINE_BAKE_POPUP, POPUP_SMALL_NO_HEIGHT)}; int hoveredTime{}; - anm2::Frame* draggedFrame{}; - anm2::Type draggedFrameType{}; + bool isFrameBoxSelecting{}; + bool isFrameBoxAdditive{}; + ImVec2 frameBoxStart{}; + ImVec2 frameBoxEnd{}; + std::set frameBoxSelection{}; + std::set groupReferences{}; + TimelineRowReference rowSelectionAnchor{}; + bool isRowSelectionAnchorSet{}; + PopupHelper groupPropertiesPopup{PopupHelper(LABEL_GROUP_PROPERTIES, POPUP_SMALL_NO_HEIGHT)}; + std::string groupName{}; + int groupAnimationIndex{-1}; + int groupType{NONE}; + int groupId{-1}; + Reference draggedFrameReference{}; + bool isDraggedFrameActive{}; + int draggedFrameType{}; int draggedFrameIndex{-1}; int draggedFrameStart{-1}; int draggedFrameStartDuration{-1}; @@ -42,7 +82,10 @@ namespace anm2ed::imgui std::vector frameSelectionSnapshot{}; std::vector frameSelectionLocked{}; bool isFrameSelectionLocked{}; - anm2::Reference frameSelectionSnapshotReference{}; + Reference frameSelectionSnapshotReference{}; + Reference frameSelectionAnchor{}; + bool isFrameSelectionAnchorSet{}; + std::vector rowDragReferences{}; glm::vec2 scroll{}; ImDrawList* pickerLineDrawList{}; ImGuiStyle style{}; diff --git a/src/imgui/window/tools.cpp b/src/imgui/window/tools.cpp index 3432da0..f935342 100644 --- a/src/imgui/window/tools.cpp +++ b/src/imgui/window/tools.cpp @@ -6,6 +6,7 @@ #include "strings.hpp" #include "tool.hpp" #include "types.hpp" +#include "util/imgui/imgui.hpp" using namespace anm2ed::resource; using namespace anm2ed::types; @@ -30,10 +31,12 @@ namespace anm2ed::imgui switch (type) { case tool::UNDO: - if (document.is_able_to_undo()) document.undo(); + if (document.is_able_to_undo()) + manager.command_push({manager.selected, [](Manager&, Document& document) { document.undo(); }}); break; case tool::REDO: - if (document.is_able_to_redo()) document.redo(); + if (document.is_able_to_redo()) + manager.command_push({manager.selected, [](Manager&, Document& document) { document.redo(); }}); break; case tool::COLOR: colorEditPopup.open(); diff --git a/src/imgui/window/welcome.cpp b/src/imgui/window/welcome.cpp index b5383e3..f193e85 100644 --- a/src/imgui/window/welcome.cpp +++ b/src/imgui/window/welcome.cpp @@ -3,7 +3,7 @@ #include #include -#include "path_.hpp" +#include "path.hpp" #include "strings.hpp" using namespace anm2ed::util; @@ -15,8 +15,7 @@ namespace anm2ed::imgui { auto viewport = ImGui::GetMainViewport(); auto windowHeight = viewport->Size.y - taskbar.height - documents.height; - if (windowHeight < 1.0f) - windowHeight = 1.0f; + if (windowHeight < 1.0f) windowHeight = 1.0f; ImGui::SetNextWindowViewport(viewport->ID); ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height)); @@ -37,10 +36,10 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button(localize.get(BASIC_NEW), widgetSize)) - dialog.file_save(Dialog::ANM2_NEW); // handled in taskbar.cpp + dialog.file_save(Dialog::ANM2_CREATE); // handled in taskbar.cpp ImGui::SameLine(); if (ImGui::Button(localize.get(BASIC_OPEN), widgetSize)) - dialog.file_open(Dialog::ANM2_OPEN); // handled in taskbar.cpp + dialog.file_open(Dialog::ANM2_OPEN, true); // handled in taskbar.cpp if (ImGui::BeginChild("##Recent Files Child", {}, ImGuiChildFlags_Borders)) { @@ -53,7 +52,7 @@ namespace anm2ed::imgui if (ImGui::Selectable(label.c_str())) { - manager.open(file); + manager.command_push({.runManager = [file](Manager& manager) { manager.open(file); }}); ImGui::PopID(); break; } diff --git a/src/imgui/window/window.cpp b/src/imgui/window/window.cpp new file mode 100644 index 0000000..d2f7508 --- /dev/null +++ b/src/imgui/window/window.cpp @@ -0,0 +1,3134 @@ +#include "window.hpp" + +#include +#include +#include +#include +#include +#include + +#include "actions.hpp" +#include "log.hpp" +#include "math.hpp" +#include "path.hpp" +#include "strings.hpp" +#include "toast.hpp" +#include "vector.hpp" +#include "working_directory.hpp" + +using namespace anm2ed::resource; +using namespace anm2ed::types; +using namespace anm2ed::util; +using namespace glm; + +namespace anm2ed::imgui +{ + static constexpr auto PADDING_MAX = 100; + static constexpr auto MERGE_APPEND_RIGHT = 0; + static constexpr auto MERGE_APPEND_BOTTOM = 1; + + struct SpritesheetMergeOptions + { + bool isAppendRight{}; + bool isMakeRegions{}; + bool isMakePrimaryRegion{}; + origin::Type regionOrigin{}; + }; + + struct AnimationMergeOptions + { + std::set selection{}; + int reference{-1}; + merge::Type type{}; + bool isDeleteAnimationsAfter{}; + }; + + int window_element_count_get(const Window& window, const Element* container) + { + int count{}; + if (!container) return count; + for (const auto& element : container->children) + if (element.type == window.elementType) ++count; + return count; + } + + Element* window_container_get(const Window& window, Anm2& anm2) { return anm2.element_get(window.containerType); } + + Element* window_element_get(const Window& window, Anm2& anm2, int key) + { + if (window.element_get) return window.element_get(anm2, key); + auto container = window_container_get(window, anm2); + return container ? element_child_id_get(*container, window.elementType, key) : nullptr; + } + + int window_track_id_get(const Element& item, ElementType trackType) + { + return trackType == ElementType::LAYER_ANIMATION ? item.layerId : item.nullId; + } + + void window_track_shell_push(Element& destination, std::set& copiedTrackIds, const Element& source, + ElementType trackType, int groupId) + { + if (source.type != trackType) return; + auto trackId = window_track_id_get(source, trackType); + if (copiedTrackIds.contains(trackId)) return; + copiedTrackIds.insert(trackId); + + auto item = element_make(trackType); + item.layerId = source.layerId; + item.nullId = source.nullId; + item.groupId = source.groupId != -1 ? source.groupId : groupId; + item.isVisible = source.isVisible; + destination.children.push_back(item); + } + + Element window_track_container_shell_copy(const Element* source, ElementType containerType, ElementType trackType) + { + auto destination = element_make(containerType); + if (!source) return destination; + + auto nextGroupId = element_child_next_id_get(*source, ElementType::GROUP); + std::set copiedTrackIds{}; + for (const auto& sourceItem : source->children) + { + if (sourceItem.type != ElementType::GROUP) + { + window_track_shell_push(destination, copiedTrackIds, sourceItem, trackType, -1); + continue; + } + + auto group = element_make(ElementType::GROUP); + group.id = sourceItem.id >= 0 ? sourceItem.id : nextGroupId++; + group.name = sourceItem.name; + group.isExpanded = sourceItem.isExpanded; + group.isVisible = sourceItem.isVisible; + destination.children.push_back(group); + + for (const auto& child : sourceItem.children) + window_track_shell_push(destination, copiedTrackIds, child, trackType, group.id); + } + return destination; + } + + void window_edit(Window& window, Document& document, const std::string& message, auto behavior) + { + document.snapshot(message); + behavior(); + document.anm2_change(window.changeType); + } + + void window_edit(Document& document, Document::ChangeType changeType, const std::string& message, auto behavior) + { + document.snapshot(message); + behavior(); + document.anm2_change(changeType); + } + + void window_rename_finish(Window& window, Manager& manager, int key, int count, const std::string& name) + { + auto changeType = window.changeType; + auto containerType = window.containerType; + auto elementType = window.elementType; + auto elementGet = window.element_get; + auto renameEdit = window.renameEdit; + auto renameFinish = window.rename_finish; + manager.command_push({manager.selected, + [changeType, containerType, elementType, elementGet, renameEdit, renameFinish, key, count, + name](Manager&, Document& document) mutable + { + auto element = elementGet ? elementGet(document.anm2, key) : nullptr; + if (!element) + if (auto container = document.anm2.element_get(containerType)) + element = element_child_id_get(*container, elementType, key); + if (!element || element->name == name) return; + + document.snapshot(localize.get(renameEdit)); + element->name = name; + if (renameFinish) renameFinish(document, *element, key, count); + document.anm2_change(changeType); + }}); + } + + void window_command_push(Window& window, Manager& manager, Settings& settings, Clipboard& clipboard, + const Window::Command& command) + { + if (!command) return; + auto queuedCommand = command; + manager.command_push({manager.selected, + [&window, &settings, &clipboard, queuedCommand](Manager& manager, Document& document) mutable + { queuedCommand(window, manager, settings, document, clipboard); }}); + } + + void window_copy(Window& window, Manager& manager, Settings& settings, Document& document, Clipboard& clipboard) + { + if (window.copy) + { + window_command_push(window, manager, settings, clipboard, window.copy); + return; + } + + auto& selection = window.storage_get(document).selection; + if (selection.empty()) return; + + std::string clipboardText{}; + for (auto key : selection) + if (auto element = window_element_get(window, document.anm2, key)) clipboardText += element_to_string(*element); + clipboard.set(clipboardText); + } + + void window_paste(Window& window, Manager& manager, Settings& settings, Document&, Clipboard& clipboard) + { + if (window.paste) + { + window_command_push(window, manager, settings, clipboard, window.paste); + return; + } + + if (clipboard.is_empty()) return; + + auto paste = [](Window& window, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + auto storage = &window.storage_get(document); + auto container = window_container_get(window, document.anm2); + auto maxIdBefore = container ? element_child_max_id_get(*container, window.elementType) : -1; + auto pasted = document.anm2; + std::string errorString{}; + + if (pasted.deserialize(window.elementType, clipboard.get(), true, &errorString, document.directory_get())) + { + document.snapshot(localize.get(window.pasteEdit)); + document.anm2 = std::move(pasted); + container = window_container_get(window, document.anm2); + auto maxIdAfter = container ? element_child_max_id_get(*container, window.elementType) : -1; + if (maxIdAfter > maxIdBefore) + { + window.newElementId = maxIdAfter; + storage->selection = {maxIdAfter}; + storage->reference = maxIdAfter; + } + document.anm2_change(window.changeType); + return; + } + + if (window.deserializeFailedToast) + { + toasts.push(std::vformat(localize.get(window.deserializeFailedToast), std::make_format_args(errorString))); + logger.error(std::vformat(localize.get(window.deserializeFailedToast, anm2ed::ENGLISH), + std::make_format_args(errorString))); + } + }; + + window_command_push(window, manager, settings, clipboard, paste); + } + + void window_remove_unused(Window& window, Manager& manager, Settings& settings, Document&, Clipboard& clipboard) + { + auto removeUnused = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto unused = document.anm2.element_unused(window.elementType); + if (unused.empty()) return; + + auto behavior = [&]() + { + auto container = window_container_get(window, document.anm2); + if (!container) return; + for (auto id : unused) + element_child_id_erase(*container, window.elementType, id); + }; + + window_edit(window, document, localize.get(window.removeUnusedEdit), behavior); + }; + + window_command_push(window, manager, settings, clipboard, removeUnused); + } + + void window_add(Window& window, Manager& manager, Settings& settings, Document&, Clipboard& clipboard) + { + if (window.add) + { + window_command_push(window, manager, settings, clipboard, window.add); + return; + } + + if (window.properties_open) window.properties_open(manager, -1); + } + + void window_command_run(Window& window, Manager& manager, Settings& settings, Document&, Clipboard& clipboard, + const Window::Command& command) + { + window_command_push(window, manager, settings, clipboard, command); + } + + void window_properties(Window& window, Manager& manager, int id) + { + if (window.properties_open) window.properties_open(manager, id); + } + + void window_scroll_to_item(float rowHeight, bool isTarget) + { + if (!isTarget) return; + auto windowHeight = ImGui::GetWindowHeight(); + auto targetTop = ImGui::GetCursorPosY(); + auto targetBottom = targetTop + rowHeight; + auto visibleTop = ImGui::GetScrollY(); + auto visibleBottom = visibleTop + windowHeight; + if (targetTop < visibleTop) + ImGui::SetScrollY(targetTop); + else if (targetBottom > visibleBottom) + ImGui::SetScrollY(targetBottom - windowHeight); + } + + int window_arrow_selection_get(const std::vector& ids, int reference, const std::set& selection) + { + if (ids.empty()) return -1; + auto current = reference; + if (current == -1 && !selection.empty()) current = *selection.begin(); + auto it = std::find(ids.begin(), ids.end(), current); + auto index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it); + auto delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1; + index = std::clamp(index + delta, 0, (int)ids.size() - 1); + return ids[index]; + } + + std::filesystem::path window_asset_path_get(Document& document, const std::filesystem::path& path) + { + WorkingDirectory workingDirectory(document.directory_get()); + return path::lower_case_backslash_handle(path::make_relative(path)); + } + + void window_directory_open(Dialog& dialog, Document& document, const std::filesystem::path& path) + { + if (path.empty()) return; + std::error_code ec{}; + auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / path, ec); + if (ec) absolutePath = document.directory_get() / path; + auto target = std::filesystem::is_directory(absolutePath) ? absolutePath + : std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path() + : document.directory_get(); + dialog.file_explorer_open(target); + } + + int window_animation_count_get(Anm2& anm2) + { + int count{}; + auto animations = anm2.element_get(ElementType::ANIMATIONS); + if (!animations) return count; + for (auto& animation : animations->children) + if (animation.type == ElementType::ANIMATION) ++count; + return count; + } + + int window_animations_merge(Document& document, const AnimationMergeOptions& options) + { + auto& anm2 = document.anm2; + auto& selection = document.animation.selection; + auto& reference = document.reference; + auto& overlayIndex = document.overlayIndex; + int merged{-1}; + + document.snapshot(localize.get(EDIT_MERGE_ANIMATIONS)); + if (options.selection.empty()) + { + if (selection.contains(overlayIndex)) overlayIndex = -1; + + if (selection.size() > 1) + merged = anm2.animations_merge(*selection.begin(), selection); + else if (selection.size() == 1 && *selection.begin() != window_animation_count_get(anm2) - 1) + { + auto start = *selection.begin(); + auto next = *selection.begin() + 1; + std::set animationSet{}; + animationSet.insert(start); + animationSet.insert(next); + merged = anm2.animations_merge(start, animationSet); + } + else + return -1; + } + else + { + if (options.selection.contains(overlayIndex)) overlayIndex = -1; + auto mergeSelection = options.selection; + merged = anm2.animations_merge(options.reference, mergeSelection, options.type, options.isDeleteAnimationsAfter); + } + + if (merged == -1) return -1; + selection = {merged}; + reference = {merged}; + document.anm2_change(Document::ANIMATIONS); + return merged; + } + + void window_spritesheets_merge(Document& document, const std::set& ids, + const SpritesheetMergeOptions& options) + { + if (ids.size() <= 1) return; + + auto behavior = [&]() + { + auto baseID = *ids.begin(); + if (document.spritesheets_merge(ids, options.isAppendRight, options.isMakeRegions, options.isMakePrimaryRegion, + options.regionOrigin)) + { + document.spritesheet.selection = {baseID}; + document.spritesheet.reference = baseID; + document.region.reference = -1; + document.region.selection.clear(); + toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS)); + logger.info(localize.get(TOAST_MERGE_SPRITESHEETS, anm2ed::ENGLISH)); + } + else + { + toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED)); + logger.error(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED, anm2ed::ENGLISH)); + } + }; + + document.snapshot(localize.get(EDIT_MERGE_SPRITESHEETS)); + behavior(); + document.change(Document::ALL); + } + + void window_spritesheet_pack(Document& document, int id, int padding) + { + if (id == -1) return; + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) return; + bool isRegionsEmpty = true; + for (auto& child : spritesheet->children) + if (child.type == ElementType::REGION) isRegionsEmpty = false; + if (isRegionsEmpty) return; + + auto behavior = [&]() + { + if (document.spritesheet_pack(id, std::max(0, padding))) + { + toasts.push(localize.get(TOAST_PACK_SPRITESHEET)); + logger.info(localize.get(TOAST_PACK_SPRITESHEET, anm2ed::ENGLISH)); + } + else + { + toasts.push(localize.get(TOAST_PACK_SPRITESHEET_FAILED)); + logger.error(localize.get(TOAST_PACK_SPRITESHEET_FAILED, anm2ed::ENGLISH)); + } + }; + + document.snapshot(localize.get(EDIT_PACK_SPRITESHEET)); + behavior(); + document.change(Document::SPRITESHEETS); + } + + void window_spritesheets_save(Document& document, const std::set& ids) + { + if (ids.empty()) return; + + for (auto& id : ids) + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id); + auto texture = document.texture_get(id); + if (!spritesheet || !texture) continue; + auto pathString = path::to_utf8(spritesheet->path); + WorkingDirectory workingDirectory(document.directory_get()); + path::ensure_directory(spritesheet->path.parent_path()); + if (texture->write_png(spritesheet->path)) + { + document.spritesheet_hash_set_saved(id); + toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString))); + logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + } + else + { + toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString))); + logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + } + } + } + + int window_footer_button_count_get(const Window& window) + { + int count{}; + if (window_flag_has(window.flags, WINDOW_ADD)) ++count; + if (window_flag_has(window.flags, WINDOW_DUPLICATE)) ++count; + if (window_flag_has(window.flags, WINDOW_MERGE)) ++count; + if (window_flag_has(window.flags, WINDOW_REMOVE)) ++count; + if (window_flag_has(window.flags, WINDOW_REMOVE_UNUSED)) ++count; + if (window_flag_has(window.flags, WINDOW_DEFAULT)) ++count; + return count; + } + + Actions window_footer_actions_get(Window& window, Manager& manager, Settings& settings, Document& document, + Clipboard& clipboard) + { + Actions actions{}; + auto& selection = window.storage_get(document).selection; + + if (window_flag_has(window.flags, WINDOW_ADD)) + actions.add(ACTION_ADD, []() { return true; }, + [&]() { window_add(window, manager, settings, document, clipboard); }, window.addTooltip); + + if (window_flag_has(window.flags, WINDOW_DUPLICATE)) + actions.add(ACTION_DUPLICATE, [&]() { return !selection.empty(); }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.duplicate); }, + window.duplicateTooltip); + + if (window_flag_has(window.flags, WINDOW_MERGE)) + actions.add(ACTION_MERGE, [&]() { return selection.size() == 1; }, + [&]() + { + window_command_run(window, manager, settings, document, clipboard, + window.merge_open ? window.merge_open : window.merge); + }, + window.mergeTooltip); + + if (window_flag_has(window.flags, WINDOW_REMOVE)) + actions.add(ACTION_REMOVE, [&]() { return !selection.empty(); }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.remove); }, + window.removeTooltip); + + if (window_flag_has(window.flags, WINDOW_REMOVE_UNUSED)) + actions.add(ACTION_REMOVE_UNUSED, []() { return true; }, + [&]() { window_remove_unused(window, manager, settings, document, clipboard); }, + window.removeUnusedTooltip); + + if (window_flag_has(window.flags, WINDOW_DEFAULT)) + actions.add(ACTION_DEFAULT, [&]() { return selection.size() == 1; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.default_set); }, + window.defaultTooltip); + + return actions; + } + + Actions window_context_actions_get(Window& window, Manager& manager, Settings& settings, Document& document, + Clipboard& clipboard, bool isUndoRedoIncluded = true) + { + Actions actions{}; + auto& selection = window.storage_get(document).selection; + + if (isUndoRedoIncluded) + { + actions_undo_redo_add(actions, manager, document); + actions.separator(); + } + + if (window_flag_has(window.flags, WINDOW_RENAME)) + actions.add(ACTION_RENAME, [&]() { return selection.size() == 1; }, + [&]() { window.renameQueued = *selection.begin(); }); + if (window_flag_has(window.flags, WINDOW_PROPERTIES)) + actions.add(ACTION_PROPERTIES, [&]() { return selection.size() == 1; }, + [&]() { window_properties(window, manager, *selection.begin()); }); + if (window_flag_has(window.flags, WINDOW_ADD)) + actions.add(ACTION_ADD, []() { return true; }, + [&]() { window_add(window, manager, settings, document, clipboard); }); + if (window_flag_has(window.flags, WINDOW_DUPLICATE)) + actions.add(ACTION_DUPLICATE, [&]() { return !selection.empty(); }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.duplicate); }); + if (window_flag_has(window.flags, WINDOW_MERGE)) + actions.add(ACTION_MERGE, [&]() { return !selection.empty(); }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.merge); }); + if (window_flag_has(window.flags, WINDOW_REMOVE)) + actions.add(ACTION_REMOVE, [&]() { return !selection.empty(); }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.remove); }); + if (window_flag_has(window.flags, WINDOW_REMOVE_UNUSED)) + actions.add(ACTION_REMOVE_UNUSED, []() { return true; }, + [&]() { window_remove_unused(window, manager, settings, document, clipboard); }); + if (window_flag_has(window.flags, WINDOW_DEFAULT)) + actions.add(ACTION_DEFAULT, [&]() { return selection.size() == 1; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.default_set); }); + + actions.separator(); + + if (window_flag_has(window.flags, WINDOW_CUT)) + actions.add(ACTION_CUT, [&]() { return !selection.empty(); }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.cut); }); + if (window_flag_has(window.flags, WINDOW_COPY)) + actions.add(ACTION_COPY, [&]() { return !selection.empty(); }, + [&]() { window_copy(window, manager, settings, document, clipboard); }); + if (window_flag_has(window.flags, WINDOW_PASTE)) + actions.add(ACTION_PASTE, [&]() { return !clipboard.is_empty(); }, + [&]() { window_paste(window, manager, settings, document, clipboard); }); + + return actions; + } + + void window_footer_draw(Window& window, Manager& manager, Settings& settings, Document& document, + Clipboard& clipboard) + { + auto actions = window_footer_actions_get(window, manager, settings, document, clipboard); + auto buttonCount = (int)actions.items.size(); + if (buttonCount == 0) return; + + auto widgetSize = widget_size_with_row_get(buttonCount); + bool isSameLine{}; + for (auto& action : actions.items) + action_button_draw(action, manager, settings, widgetSize, isSameLine); + } + + void window_context_menu_draw(Window& window, Manager& manager, Settings& settings, Document& document, + Clipboard& clipboard) + { + auto actions = window_context_actions_get(window, manager, settings, document, clipboard); + actions_context_window_draw("##Context Menu", actions, settings); + } + + std::string window_rename_format_get(Window& window, Manager& manager, int id) + { + return std::format("###Document #{} Window #{} Element #{}", manager.selected, (int)window.elementType, id); + } + + void window_rows_draw(Window& window, Manager& manager, Resources& resources, Document& document) + { + auto container = window_container_get(window, document.anm2); + auto& storage = window.storage_get(document); + auto& selection = storage.selection; + auto count = window_element_count_get(window, container); + int index{}; + + selection.start(count); + + if (container) + { + for (auto& element : container->children) + { + if (element.type != window.elementType) continue; + auto key = window.element_key_get ? window.element_key_get(element, index) : element.id; + auto isSelected = selection.contains(key); + auto isReferenced = window_flag_has(window.flags, WINDOW_REFERENCE_ITALIC) && storage.reference == key; + auto font = window.row_font_get ? window.row_font_get(document, element, key) + : isReferenced ? resource::font::ITALICS + : resource::font::REGULAR; + auto isFontPushed = font != resource::font::REGULAR; + + ImGui::PushID(key); + ImGui::SetNextItemSelectionUserData(key); + auto label = window.row_label_get ? window.row_label_get(document, element) : element.name; + if (isFontPushed) ImGui::PushFont(resources.fonts[font].get(), resource::font::SIZE); + + if (window_flag_has(window.flags, WINDOW_RENAME)) + { + if (window.newElementId == key || window.renameQueued == key) + { + window.renameState = RENAME_FORCE_EDIT; + window.renameQueued = -1; + } + + auto isRenaming = window.renameId == key; + auto& name = isRenaming ? window.renameText : element.name; + if (selectable_input_text(label, window_rename_format_get(window, manager, key), name, isSelected, + ImGuiSelectableFlags_None, window.renameState)) + { + if (window.row_select) window.row_select(window, document, key); + if (window.renameState == RENAME_BEGIN) + { + window.renameId = key; + window.renameText = element.name; + } + else if (window.renameState == RENAME_FINISHED) + { + if (isRenaming) window_rename_finish(window, manager, key, count, window.renameText); + window.renameId = -1; + window.renameText.clear(); + } + } + } + else + { + if (ImGui::Selectable(label.c_str(), isSelected)) + if (window.row_select) window.row_select(window, document, key); + } + + if (isFontPushed) ImGui::PopFont(); + + if (window.newElementId == key || window.scrollQueued == key) + { + ImGui::SetScrollHereY(0.5f); + if (window.newElementId == key) window.newElementId = -1; + if (window.scrollQueued == key) window.scrollQueued = -1; + } + + if (window_flag_has(window.flags, WINDOW_PROPERTIES) && ImGui::IsItemHovered() && + ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + window_properties(window, manager, key); + + if (window.row_drag_drop_update && window.row_drag_drop_update(window, manager, document, element, key)) + { + ImGui::PopID(); + break; + } + + if (window.tooltip_draw) + { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, window.tooltipItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, window.tooltipWindowPadding); + if (ImGui::BeginItemTooltip()) + { + window.tooltip_draw(document, resources, element); + ImGui::EndTooltip(); + } + ImGui::PopStyleVar(2); + } + + ImGui::PopID(); + ++index; + } + } + + selection.finish(); + } + + void window_update(Window& window, Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, + Clipboard& clipboard) + { + auto document = manager.get(); + if (!document || !window.isOpen || !window.storage_get) return; + window.dialog = &dialog; + + if (ImGui::Begin(localize.get(window.title), &(settings.*window.isOpen))) + { + if (window.begin_update) window.begin_update(window, manager, settings, resources, clipboard, *document); + + auto isAvailable = !window.is_available || window.is_available(*document); + if (!isAvailable) + { + if (window.unavailableText != STRING_UNDEFINED) ImGui::TextUnformatted(localize.get(window.unavailableText)); + } + else + { + auto footerRows = window.footerRows != -1 ? window.footerRows + : window_footer_button_count_get(window) > 0 ? 1 + : 0; + auto childSize = size_without_footer_get(footerRows); + auto style = ImGui::GetStyle(); + window.tooltipWindowPadding = style.WindowPadding; + window.tooltipItemSpacing = style.ItemSpacing; + + if (window.isChildPaddingZero) ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); + auto isChildOpen = ImGui::BeginChild(window.childLabel, childSize, true); + if (isChildOpen) + { + if (window.rows_update) + window.rows_update(window, manager, settings, resources, clipboard, *document, childSize); + else + window_rows_draw(window, manager, resources, *document); + + auto actions = window_context_actions_get(window, manager, settings, *document, clipboard, false); + actions_shortcuts_update(actions, manager); + + if (!window.context_update) window_context_menu_draw(window, manager, settings, *document, clipboard); + } + ImGui::EndChild(); + if (window.isChildPaddingZero) ImGui::PopStyleVar(); + + if (window.context_update) window.context_update(window, manager, settings, resources, clipboard, *document); + + if (window.footer_update) + window.footer_update(window, manager, settings, resources, clipboard, *document); + else + window_footer_draw(window, manager, settings, *document, clipboard); + if (window.body_update) window.body_update(window, manager, settings, resources, clipboard, *document); + } + } + ImGui::End(); + + if (window.popup_update) window.popup_update(window, manager, settings, resources, clipboard, *document); + if (window.post_update) window.post_update(window, manager, settings, resources, clipboard, *document); + } + + Window animations_window_register() + { + Window window{}; + window.title = LABEL_ANIMATIONS_WINDOW; + window.isOpen = &Settings::windowIsAnimations; + window.changeType = Document::ANIMATIONS; + window.containerType = ElementType::ANIMATIONS; + window.elementType = ElementType::ANIMATION; + window.childLabel = "##Animations Child"; + window.addTooltip = TOOLTIP_ADD_ANIMATION; + window.duplicateTooltip = TOOLTIP_DUPLICATE_ANIMATION; + window.mergeTooltip = TOOLTIP_OPEN_MERGE_POPUP; + window.removeTooltip = TOOLTIP_REMOVE_ANIMATION; + window.defaultTooltip = TOOLTIP_SET_DEFAULT_ANIMATION; + window.renameEdit = SNAPSHOT_RENAME_ANIMATION; + window.pasteEdit = EDIT_PASTE_ANIMATIONS; + window.deserializeFailedToast = TOAST_DESERIALIZE_ANIMATIONS_FAILED; + window.flags = WINDOW_ADD | WINDOW_DUPLICATE | WINDOW_MERGE | WINDOW_REMOVE | WINDOW_DEFAULT | WINDOW_CUT | + WINDOW_COPY | WINDOW_PASTE | WINDOW_RENAME; + window.popup = PopupHelper(LABEL_ANIMATIONS_MERGE_POPUP); + window.storage_get = [](Document& document) -> Storage& { return document.animation; }; + window.element_get = [](Anm2& anm2, int index) { return anm2.element_get(ElementType::ANIMATION, index); }; + window.element_key_get = [](const Element&, int index) { return index; }; + window.row_font_get = [](Document& document, const Element& animation, int index) + { + auto animations = document.anm2.element_get(ElementType::ANIMATIONS); + auto isDefault = animations && animations->defaultAnimation == animation.name; + auto isReferenced = document.reference.animationIndex == index; + return isDefault && isReferenced ? resource::font::BOLD_ITALICS + : isDefault ? resource::font::BOLD + : isReferenced ? resource::font::ITALICS + : resource::font::REGULAR; + }; + window.row_select = [](Window&, Document& document, int index) + { + document.reference = {index}; + document.frames.clear(); + }; + window.rename_finish = [](Document& document, Element& animation, int, int count) + { + auto animations = document.anm2.element_get(ElementType::ANIMATIONS); + if (animations && count == 1) animations->defaultAnimation = animation.name; + }; + window.tooltip_draw = [](Document& document, Resources& resources, const Element& animation) + { + auto animations = document.anm2.element_get(ElementType::ANIMATIONS); + auto isDefault = animations && animations->defaultAnimation == animation.name; + + ImGui::PushFont(resources.fonts[resource::font::BOLD].get(), resource::font::SIZE); + ImGui::TextUnformatted(animation.name.c_str()); + ImGui::PopFont(); + + if (isDefault) + { + ImGui::PushFont(resources.fonts[resource::font::ITALICS].get(), resource::font::SIZE); + ImGui::TextUnformatted(localize.get(BASIC_DEFAULT)); + ImGui::PopFont(); + } + + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_LENGTH), std::make_format_args(animation.frameNum)).c_str()); + auto loopLabel = animation.isLoop ? localize.get(BASIC_YES) : localize.get(BASIC_NO); + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_LOOP), std::make_format_args(loopLabel)).c_str()); + }; + window.row_drag_drop_update = [](Window& window, Manager& manager, Document& document, const Element&, int index) + { + auto& anm2 = document.anm2; + auto& selection = document.animation.selection; + + if (ImGui::BeginDragDropSource()) + { + static std::vector dragDropSelection{}; + dragDropSelection.assign(selection.begin(), selection.end()); + ImGui::SetDragDropPayload("Animation Drag Drop", dragDropSelection.data(), + dragDropSelection.size() * sizeof(int)); + for (auto& dragIndex : dragDropSelection) + if (auto dragAnimation = anm2.element_get(ElementType::ANIMATION, dragIndex)) + ImGui::Text("%s", dragAnimation->name.c_str()); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload( + "Animation Drag Drop", + ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) + { + auto itemMin = ImGui::GetItemRectMin(); + auto itemMax = ImGui::GetItemRectMax(); + auto isDropAfter = is_drop_after(itemMin, itemMax); + drop_line_draw(ImGui::GetWindowDrawList(), itemMin, itemMax, isDropAfter); + + auto payloadIndices = (int*)(payload->Data); + auto payloadCount = payload->DataSize / sizeof(int); + std::vector indices(payloadIndices, payloadIndices + payloadCount); + std::sort(indices.begin(), indices.end()); + if (payload->IsDelivery()) + { + auto targetIndex = index + (isDropAfter ? 1 : 0); + manager.command_push({manager.selected, [&window, indices, targetIndex](Manager&, Document& document) mutable + { + auto move = [&]() + { + auto items = window_container_get(window, document.anm2); + if (items) + document.animation.selection = + anm2ed::util::vector::move_indices_to_position(items->children, indices, + targetIndex); + }; + window_edit(window, document, localize.get(EDIT_MOVE_ANIMATIONS), move); + }}); + ImGui::EndDragDropTarget(); + return true; + } + } + ImGui::EndDragDropTarget(); + } + + return false; + }; + window.add = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& anm2 = document.anm2; + auto& reference = document.reference; + auto& selection = document.animation.selection; + + auto animations = [&]() { return anm2.element_get(ElementType::ANIMATIONS); }; + auto animation_count = [&]() { return window_element_count_get(window, animations()); }; + + auto behavior = [&]() + { + auto items = animations(); + if (!items) return; + + auto animation = element_make(ElementType::ANIMATION); + animation.name = localize.get(TEXT_NEW_ANIMATION); + animation.frameNum = 1; + animation.isLoop = true; + + auto rootAnimation = element_make(ElementType::ROOT_ANIMATION); + rootAnimation.children.push_back(element_make(ElementType::FRAME)); + animation.children.push_back(rootAnimation); + + const Element* referenceLayerAnimations{}; + const Element* referenceNullAnimations{}; + if (auto referenceAnimation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex)) + { + referenceLayerAnimations = element_child_first_get(*referenceAnimation, ElementType::LAYER_ANIMATIONS); + referenceNullAnimations = element_child_first_get(*referenceAnimation, ElementType::NULL_ANIMATIONS); + } + + animation.children.push_back(window_track_container_shell_copy(referenceLayerAnimations, + ElementType::LAYER_ANIMATIONS, + ElementType::LAYER_ANIMATION)); + animation.children.push_back(window_track_container_shell_copy(referenceNullAnimations, + ElementType::NULL_ANIMATIONS, + ElementType::NULL_ANIMATION)); + animation.children.push_back(element_make(ElementType::TRIGGERS)); + + auto count = animation_count(); + auto index = count; + if (!selection.empty()) index = std::min(*selection.rbegin() + 1, count); + if (count == 0) items->defaultAnimation = animation.name; + + items->children.insert(items->children.begin() + index, animation); + selection = {index}; + reference = {index}; + window.newElementId = index; + window.scrollQueued = index; + }; + + window_edit(window, document, localize.get(EDIT_ADD_ANIMATION), behavior); + }; + window.remove = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.animation.selection; + auto& reference = document.reference; + auto& overlayIndex = document.overlayIndex; + + auto behavior = [&]() + { + auto items = document.anm2.element_get(ElementType::ANIMATIONS); + if (!items || selection.empty()) return; + for (auto it = selection.rbegin(); it != selection.rend(); ++it) + { + auto i = *it; + if (i < 0 || i >= (int)items->children.size()) continue; + if (overlayIndex == i) overlayIndex = -1; + if (reference.animationIndex == i) reference.animationIndex = -1; + items->children.erase(items->children.begin() + i); + } + selection.clear(); + }; + + window_edit(window, document, localize.get(EDIT_REMOVE_ANIMATIONS), behavior); + }; + window.duplicate = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.animation.selection; + + auto behavior = [&]() + { + auto items = document.anm2.element_get(ElementType::ANIMATIONS); + if (!items || selection.empty()) return; + auto duplicated = selection; + auto end = *duplicated.rbegin(); + for (auto& id : duplicated) + { + if (id < 0 || id >= (int)items->children.size()) continue; + items->children.insert(items->children.begin() + end, items->children[id]); + selection.insert(++end); + selection.erase(id); + } + }; + + window_edit(window, document, localize.get(EDIT_DUPLICATE_ANIMATIONS), behavior); + }; + window.merge = [](Window& window, Manager&, Settings& settings, Document& document, Clipboard&) + { + auto& mergeSelection = document.merge.selection; + auto& mergeReference = document.merge.reference; + auto merged = window_animations_merge(document, {.selection = mergeSelection, + .reference = mergeReference, + .type = (merge::Type)settings.mergeType, + .isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter}); + if (merged != -1) window.scrollQueued = merged; + mergeSelection.clear(); + }; + window.merge_open = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.animation.selection; + if (selection.empty()) return; + window.popup.open(); + document.merge.selection.clear(); + document.merge.reference = *selection.begin(); + }; + window.default_set = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.animation.selection; + + auto behavior = [&]() + { + auto items = document.anm2.element_get(ElementType::ANIMATIONS); + auto animation = document.anm2.element_get(ElementType::ANIMATION, *selection.begin()); + if (!items || !animation) return; + items->defaultAnimation = animation->name; + }; + + window_edit(window, document, localize.get(EDIT_DEFAULT_ANIMATION), behavior); + }; + window.copy = [](Window&, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + auto& selection = document.animation.selection; + if (selection.empty()) return; + + std::string clipboardText{}; + for (auto& i : selection) + if (auto animation = document.anm2.element_get(ElementType::ANIMATION, i)) + clipboardText += element_to_string(*animation); + clipboard.set(clipboardText); + }; + window.cut = [](Window& window, Manager& manager, Settings& settings, Document& document, Clipboard& clipboard) + { + if (window.copy) window.copy(window, manager, settings, document, clipboard); + + auto& selection = document.animation.selection; + auto behavior = [&]() + { + auto items = document.anm2.element_get(ElementType::ANIMATIONS); + if (!items) return; + for (auto it = selection.rbegin(); it != selection.rend(); ++it) + { + auto i = *it; + if (i < 0 || i >= (int)items->children.size()) continue; + items->children.erase(items->children.begin() + i); + } + selection.clear(); + }; + + window_edit(window, document, localize.get(EDIT_CUT_ANIMATIONS), behavior); + }; + window.paste = [](Window& window, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + if (clipboard.is_empty()) return; + + auto& anm2 = document.anm2; + auto& selection = document.animation.selection; + auto& reference = document.reference; + auto animation_count = [&]() + { return window_element_count_get(window, anm2.element_get(ElementType::ANIMATIONS)); }; + + auto behavior = [&]() + { + auto clipboardText = clipboard.get(); + auto start = selection.empty() ? animation_count() : *selection.rbegin() + 1; + std::set indices{}; + std::string errorString{}; + if (anm2.animations_deserialize(clipboardText, start, indices, &errorString)) + { + if (!indices.empty()) + { + auto index = *indices.rbegin(); + selection = {index}; + reference = {index}; + window.newElementId = index; + window.scrollQueued = index; + } + } + else + { + toasts.push( + std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED), std::make_format_args(errorString))); + logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED, anm2ed::ENGLISH), + std::make_format_args(errorString))); + } + }; + + window_edit(window, document, localize.get(EDIT_PASTE_ANIMATIONS), behavior); + }; + window.begin_update = [](Window&, Manager&, Settings&, Resources&, Clipboard&, Document& document) + { + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::IsKeyPressed(ImGuiKey_Escape)) + document.reference = {}; + }; + window.body_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard&, Document& document) + { + auto animations = [&]() { return document.anm2.element_get(ElementType::ANIMATIONS); }; + auto animation_count = [&]() { return window_element_count_get(window, animations()); }; + auto& mergeSelection = document.merge.selection; + auto& mergeReference = document.merge.reference; + + window.popup.trigger(); + + if (ImGui::BeginPopupModal(window.popup.label(), &window.popup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto close = [&]() + { + mergeSelection.clear(); + window.popup.close(); + }; + + auto& type = settings.mergeType; + auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter; + + 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)); + + if (ImGui::BeginChild(localize.get(LABEL_ANIMATIONS_CHILD), animationsSize, ImGuiChildFlags_Borders)) + { + auto items = animations(); + mergeSelection.start(animation_count()); + + if (items) + { + for (int i = 0; i < (int)items->children.size(); i++) + { + if (i == mergeReference) continue; + + auto& animation = items->children[i]; + if (animation.type != ElementType::ANIMATION) continue; + + ImGui::PushID(i); + ImGui::SetNextItemSelectionUserData(i); + ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i)); + ImGui::PopID(); + } + } + + mergeSelection.finish(); + } + ImGui::EndChild(); + + if (ImGui::BeginChild("##Merge Options", optionsSize, ImGuiChildFlags_Borders)) + { + auto size = ImVec2(optionsSize.x * 0.5f, optionsSize.y - ImGui::GetStyle().WindowPadding.y * 2); + + if (ImGui::BeginChild("##Merge Options 1", size)) + { + ImGui::RadioButton(localize.get(LABEL_APPEND_FRAMES), &type, merge::APPEND); + ImGui::RadioButton(localize.get(LABEL_PREPEND_FRAMES), &type, merge::PREPEND); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("##Merge Options 2", size)) + { + ImGui::RadioButton(localize.get(LABEL_REPLACE_FRAMES), &type, merge::REPLACE); + ImGui::RadioButton(localize.get(LABEL_IGNORE_FRAMES), &type, merge::IGNORE); + } + ImGui::EndChild(); + } + ImGui::EndChild(); + + if (ImGui::BeginChild("##Merge Delete After", deleteAfterSize, ImGuiChildFlags_Borders)) + ImGui::Checkbox(localize.get(LABEL_DELETE_ANIMATIONS_AFTER), &isDeleteAnimationsAfter); + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + ImGui::BeginDisabled(mergeSelection.empty()); + if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize)) + { + auto queuedSelection = mergeSelection; + auto queuedReference = mergeReference; + AnimationMergeOptions options{.selection = queuedSelection, + .reference = queuedReference, + .type = (merge::Type)type, + .isDeleteAnimationsAfter = isDeleteAnimationsAfter}; + manager.command_push({manager.selected, [options](Manager&, Document& document) mutable + { window_animations_merge(document, options); }}); + close(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) close(); + + ImGui::EndPopup(); + } + }; + window.post_update = [](Window& window, Manager& manager, Settings&, Resources&, Clipboard&, Document& document) + { + auto isNextAnimation = shortcut(manager.chords[SHORTCUT_NEXT_ANIMATION], shortcut::GLOBAL); + auto isPreviousAnimation = shortcut(manager.chords[SHORTCUT_PREVIOUS_ANIMATION], shortcut::GLOBAL); + auto count = window_element_count_get(window, window_container_get(window, document.anm2)); + + if ((isPreviousAnimation || isNextAnimation) && count > 0) + { + auto& reference = document.reference; + if (isPreviousAnimation) reference.animationIndex = std::clamp(reference.animationIndex - 1, 0, count - 1); + if (isNextAnimation) reference.animationIndex = std::clamp(reference.animationIndex + 1, 0, count - 1); + + window.storage_get(document).selection = {reference.animationIndex}; + window.scrollQueued = reference.animationIndex; + } + }; + return window; + } + + Window regions_window_register() + { + Window window{}; + window.title = LABEL_REGIONS_WINDOW; + window.isOpen = &Settings::windowIsRegions; + window.changeType = Document::SPRITESHEETS; + window.elementType = ElementType::REGION; + window.childLabel = "##Regions Child"; + window.footerRows = 1; + window.flags = 0; + window.isChildPaddingZero = true; + window.unavailableText = TEXT_SELECT_SPRITESHEET; + window.editElement = element_make(ElementType::REGION); + window.popup = PopupHelper(LABEL_REGION_PROPERTIES, POPUP_SMALL_NO_HEIGHT); + window.storage_get = [](Document& document) -> Storage& { return document.region; }; + window.is_available = [](Document& document) + { return document.anm2.element_get(ElementType::SPRITESHEET, document.spritesheet.reference); }; + window.add = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + document.region.reference = -1; + window.editElement = element_make(ElementType::REGION); + window.popup.open(); + }; + window.properties = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& reference = document.region.reference; + auto& selection = document.region.selection; + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, document.spritesheet.reference); + if (!spritesheet || selection.size() != 1) return; + auto id = *selection.begin(); + if (!element_child_id_get(*spritesheet, ElementType::REGION, id)) return; + reference = id; + window.popup.open(); + }; + window.remove = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& anm2 = document.anm2; + auto spritesheetReference = document.spritesheet.reference; + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, spritesheetReference); + if (!spritesheet) return; + + auto unused = anm2.element_unused(ElementType::REGION, spritesheetReference); + if (unused.empty()) return; + + auto behavior = [&]() + { + auto target = anm2.element_get(ElementType::SPRITESHEET, spritesheetReference); + if (!target) return; + for (auto& id : unused) + { + if (auto animations = anm2.element_get(ElementType::ANIMATIONS)) + for (auto& animation : animations->children) + { + if (animation.type != ElementType::ANIMATION) continue; + auto layerAnimations = element_child_first_get(animation, ElementType::LAYER_ANIMATIONS); + if (!layerAnimations) continue; + auto region_clear = [&](auto&& self, Element& layerAnimation) -> void + { + if (layerAnimation.type == ElementType::GROUP) + { + for (auto& child : layerAnimation.children) + self(self, child); + return; + } + if (layerAnimation.type != ElementType::LAYER_ANIMATION) return; + for (auto& frame : layerAnimation.children) + if (frame.type == ElementType::FRAME && frame.regionId == id) frame.regionId = -1; + }; + for (auto& layerAnimation : layerAnimations->children) + region_clear(region_clear, layerAnimation); + } + + element_child_id_erase(*target, ElementType::REGION, id); + } + }; + + window_edit(window, document, localize.get(EDIT_REMOVE_UNUSED_REGIONS), behavior); + }; + window.trim = [](Window&, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.region.selection; + auto& reference = document.region.reference; + auto& frame = document.frames; + auto spritesheetReference = document.spritesheet.reference; + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, spritesheetReference); + if (!spritesheet || selection.empty()) return; + + auto behavior = [&]() + { + if (document.regions_trim(spritesheetReference, selection)) + { + if (reference != -1 && !selection.contains(reference)) reference = *selection.begin(); + document.reference = {document.reference.animationIndex}; + frame.reference = -1; + frame.selection.clear(); + } + }; + + document.snapshot(localize.get(EDIT_TRIM_REGIONS)); + behavior(); + document.change(Document::SPRITESHEETS); + }; + window.copy = [](Window&, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, document.spritesheet.reference); + auto& selection = document.region.selection; + if (!spritesheet || selection.empty()) return; + + std::string clipboardText{}; + for (auto& id : selection) + if (auto region = element_child_id_get(*spritesheet, ElementType::REGION, id)) + clipboardText += element_to_string(*region); + clipboard.set(clipboardText); + }; + window.paste = [](Window& window, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + auto spritesheetReference = document.spritesheet.reference; + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, spritesheetReference); + if (!spritesheet || clipboard.is_empty()) return; + + auto& anm2 = document.anm2; + auto& selection = document.region.selection; + auto& reference = document.region.reference; + auto maxRegionIdBefore = element_child_max_id_get(*spritesheet, ElementType::REGION); + auto pasted = anm2; + std::string errorString{}; + if (pasted.regions_deserialize(spritesheetReference, clipboard.get(), true, &errorString)) + { + document.snapshot(localize.get(EDIT_PASTE_REGIONS)); + anm2 = std::move(pasted); + if (auto pastedSpritesheet = anm2.element_get(ElementType::SPRITESHEET, spritesheetReference)) + { + auto maxRegionIdAfter = element_child_max_id_get(*pastedSpritesheet, ElementType::REGION); + if (maxRegionIdAfter > maxRegionIdBefore) + { + window.newElementId = maxRegionIdAfter; + selection = {maxRegionIdAfter}; + reference = maxRegionIdAfter; + } + } + document.anm2_change(Document::SPRITESHEETS); + } + else + { + toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED), std::make_format_args(errorString))); + logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED, anm2ed::ENGLISH), + std::make_format_args(errorString))); + } + }; + window.begin_update = [](Window& window, Manager& manager, Settings&, Resources&, Clipboard&, Document& document) + { + if (!manager.isMakeRegionRequested) return; + document.spritesheet.reference = manager.makeRegionSpritesheetId; + document.region.reference = -1; + window.editElement = manager.makeRegion; + window.isPreserveEditElementOnOpen = true; + window.popup.open(); + manager.isMakeRegionRequested = false; + }; + window.rows_update = [](Window& window, Manager& manager, Settings& settings, Resources& resources, + Clipboard& clipboard, Document& document, ImVec2) + { + auto& anm2 = document.anm2; + auto& selection = document.region.selection; + auto& frame = document.frames; + auto& spritesheetReference = document.spritesheet.reference; + auto& reference = document.region.reference; + auto style = ImGui::GetStyle(); + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, spritesheetReference); + if (!spritesheet) + { + ImGui::TextUnformatted(localize.get(TEXT_SELECT_SPRITESHEET)); + return; + } + + auto regionChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); + + window.order.clear(); + window.order.reserve(spritesheet->children.size()); + for (auto& region : spritesheet->children) + if (region.type == ElementType::REGION) window.order.push_back(region.id); + + selection.set_index_map(&window.order); + selection.start(window.order.size()); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + for (auto& id : window.order) + selection.insert(id); + } + if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + reference = -1; + document.reference = {document.reference.animationIndex}; + frame.reference = -1; + frame.selection.clear(); + } + + int scrollTargetId = -1; + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + (ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true))) + { + auto nextId = window_arrow_selection_get(window.order, reference, selection); + if (nextId != -1) + { + selection = {nextId}; + reference = nextId; + document.reference = {document.reference.animationIndex}; + frame.reference = -1; + frame.selection.clear(); + scrollTargetId = nextId; + } + } + + auto textureInfo = document.texture_get(spritesheetReference); + bool isValid = textureInfo && textureInfo->is_valid(); + auto& texture = isValid ? *textureInfo : resources.icons[icon::NONE]; + auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + bool isRegionDragDropSourceSubmitted{}; + bool isRegionMoved{}; + + for (int i = 0; i < (int)window.order.size(); i++) + { + int id = window.order[i]; + auto region = element_child_id_get(*spritesheet, ElementType::REGION, id); + if (!region) continue; + auto isNewRegion = window.newElementId == id; + auto nameCStr = region->name.c_str(); + auto isSelected = selection.contains(id); + auto isReferenced = id == reference; + + ImGui::PushID(id); + + window_scroll_to_item(regionChildSize.y, scrollTargetId == id); + + if (ImGui::BeginChild("##Region Child", regionChildSize, ImGuiChildFlags_Borders)) + { + auto cursorPos = ImGui::GetCursorPos(); + auto regionChildMin = ImGui::GetWindowPos(); + auto regionChildMax = ImVec2(regionChildMin.x + ImGui::GetWindowSize().x, + regionChildMin.y + ImGui::GetWindowSize().y); + + ImGui::SetNextItemSelectionUserData(i); + ImGui::SetNextItemStorageID(id); + if (ImGui::Selectable("##Region Selectable", isSelected, 0, regionChildSize)) + { + reference = id; + document.reference = {document.reference.animationIndex}; + frame.reference = -1; + frame.selection.clear(); + } + auto regionRowMin = ImGui::GetItemRectMin(); + auto regionRowMax = ImGui::GetItemRectMax(); + if (scrollTargetId == id) ImGui::SetItemDefaultFocus(); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) window.popup.open(); + + auto viewport = ImGui::GetMainViewport(); + auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f; + vec2 regionSize = glm::max(region->size, vec2(1.0f)); + vec2 previewSize = regionSize; + if (previewSize.x > maxPreviewSize.x || previewSize.y > maxPreviewSize.y) + { + auto scale = glm::min(maxPreviewSize.x / previewSize.x, maxPreviewSize.y / previewSize.y); + previewSize *= scale; + } + vec2 uvMin{}; + vec2 uvMax{1.0f, 1.0f}; + if (isValid) + { + uvMin = region->crop / vec2(texture.size); + uvMax = (region->crop + region->size) / vec2(texture.size); + } + + auto textWidth = ImGui::CalcTextSize(nameCStr).x; + auto tooltipPadding = window.tooltipWindowPadding.x * 4.0f; + auto minWidth = previewSize.x + window.tooltipItemSpacing.x + textWidth + tooltipPadding; + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, window.tooltipItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, window.tooltipWindowPadding); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && !ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + { + ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); + auto childFlags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; + auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; + + if (ImGui::BeginChild("##Region Tooltip Image Child", to_imvec2(previewSize), childFlags, noScrollFlags)) + ImGui::ImageWithBg(texture.id, to_imvec2(previewSize), to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(), + tintColor); + ImGui::EndChild(); + ImGui::PopStyleVar(); + + ImGui::SameLine(); + + auto infoChildFlags = ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; + if (ImGui::BeginChild("##Region Info Tooltip Child", ImVec2(), infoChildFlags, noScrollFlags)) + { + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); + ImGui::TextUnformatted(nameCStr); + ImGui::PopFont(); + + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region->crop.x, region->crop.y)) + .c_str()); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region->size.x, region->size.y)) + .c_str()); + if (region->origin == Origin::CUSTOM) + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region->pivot.x, region->pivot.y)) + .c_str()); + else + { + StringType originString = LABEL_REGION_ORIGIN_CENTER; + if (region->origin == Origin::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT; + auto originLabel = localize.get(originString); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str()); + } + } + ImGui::EndChild(); + ImGui::EndTooltip(); + } + } + ImGui::PopStyleVar(2); + + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + isRegionDragDropSourceSubmitted = true; + if (selection.contains(id)) + window.dragSelection.assign(selection.begin(), selection.end()); + else + window.dragSelection = {id}; + + ImGui::SetDragDropPayload("Region Drag Drop", window.dragSelection.data(), + window.dragSelection.size() * sizeof(int)); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload( + "Region Drag Drop", + ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) + { + auto isDropAfter = is_drop_after(regionRowMin, regionRowMax); + drop_line_draw(ImGui::GetWindowDrawList(), regionChildMin, regionChildMax, isDropAfter); + + auto payloadIds = (int*)(payload->Data); + int payloadCount = (int)(payload->DataSize / sizeof(int)); + std::vector indices{}; + indices.reserve(payloadCount); + for (int payloadIndex = 0; payloadIndex < payloadCount; payloadIndex++) + { + int payloadId = payloadIds[payloadIndex]; + int index = vector::find_index(window.order, payloadId); + if (index != -1) indices.push_back(index); + } + if (!indices.empty() && payload->IsDelivery()) + { + std::sort(indices.begin(), indices.end()); + auto movedIds = window.dragSelection; + auto targetIndex = i + (isDropAfter ? 1 : 0); + auto targetSpritesheetReference = spritesheetReference; + manager.command_push( + {manager.selected, [&window, indices, movedIds, targetIndex, + targetSpritesheetReference](Manager&, Document& document) mutable + { + auto spritesheet = + document.anm2.element_get(ElementType::SPRITESHEET, targetSpritesheetReference); + if (!spritesheet) return; + + auto move = [&]() + { + vector::move_indices_to_position(spritesheet->children, indices, targetIndex); + document.region.selection.clear(); + for (auto id : movedIds) + document.region.selection.insert(id); + }; + window_edit(window, document, localize.get(EDIT_MOVE_REGIONS), move); + }}); + isRegionMoved = true; + } + } + ImGui::EndDragDropTarget(); + } + + auto imageSize = to_imvec2(vec2(regionChildSize.y)); + auto aspectRatio = region->size.y != 0.0f ? (float)region->size.x / region->size.y : 1.0f; + + if (imageSize.x / imageSize.y > aspectRatio) + imageSize.x = imageSize.y * aspectRatio; + else + imageSize.y = imageSize.x / aspectRatio; + + ImGui::SetCursorPos(cursorPos); + ImGui::ImageWithBg(texture.id, imageSize, to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(), tintColor); + + ImGui::SetCursorPos(ImVec2(regionChildSize.y + style.ItemSpacing.x, + regionChildSize.y - regionChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); + + if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); + ImGui::TextUnformatted(nameCStr); + if (isReferenced) ImGui::PopFont(); + } + + ImGui::EndChild(); + + if (isNewRegion) + { + ImGui::SetScrollHereY(0.5f); + window.newElementId = -1; + } + + ImGui::PopID(); + if (isRegionMoved) break; + } + + auto regionDragPayload = isRegionMoved ? nullptr : ImGui::GetDragDropPayload(); + bool isRegionDragActive = regionDragPayload && regionDragPayload->IsDataType("Region Drag Drop"); + if (isRegionDragActive && !isRegionDragDropSourceSubmitted && !window.dragSelection.empty() && + ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceExtern | ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + ImGui::SetDragDropPayload("Region Drag Drop", window.dragSelection.data(), + window.dragSelection.size() * sizeof(int)); + ImGui::EndDragDropSource(); + } + + if (isRegionDragActive && !window.dragSelection.empty()) + { + auto mousePos = ImGui::GetIO().MousePos; + auto tooltipOffset = ImVec2(16.0f, 16.0f); + ImGui::SetNextWindowPos(ImVec2(mousePos.x + tooltipOffset.x, mousePos.y + tooltipOffset.y)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, window.tooltipWindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, window.tooltipItemSpacing); + if (ImGui::BeginTooltip()) + { + for (auto regionId : window.dragSelection) + { + auto drag = element_child_id_get(*spritesheet, ElementType::REGION, regionId); + if (drag) ImGui::TextUnformatted(drag->name.c_str()); + } + ImGui::EndTooltip(); + } + ImGui::PopStyleVar(2); + } + + ImGui::PopStyleVar(); + selection.finish(); + selection.set_index_map(nullptr); + + if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED) && window.add) + window_command_run(window, manager, settings, document, clipboard, window.add); + if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED) && window.remove) + window_command_run(window, manager, settings, document, clipboard, window.remove); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED) && window.copy) + window_command_run(window, manager, settings, document, clipboard, window.copy); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED) && window.paste) + window_command_run(window, manager, settings, document, clipboard, window.paste); + }; + window.context_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard& clipboard, Document& document) + { + auto& selection = document.region.selection; + auto style = ImGui::GetStyle(); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && + ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + ImGui::OpenPopup("##Region Context Menu"); + + Actions actions{}; + actions_undo_redo_add(actions, manager, document); + actions.separator(); + actions.add(ACTION_PROPERTIES, [&]() { return selection.size() == 1 && (bool)window.properties; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.properties); }); + actions.add(ACTION_ADD, [&]() { return (bool)window.add; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.add); }); + actions.add(ACTION_REMOVE_UNUSED, [&]() { return (bool)window.remove; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.remove); }); + actions.add(ACTION_TRIM, [&]() { return !selection.empty() && (bool)window.trim; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.trim); }, + TOOLTIP_TRIM_REGIONS); + actions.separator(); + actions.add(ACTION_COPY, [&]() { return !selection.empty() && (bool)window.copy; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.copy); }); + actions.add(ACTION_PASTE, [&]() { return !clipboard.is_empty() && (bool)window.paste; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.paste); }); + actions_popup_draw("##Region Context Menu", actions, settings); + ImGui::PopStyleVar(2); + }; + window.footer_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard& clipboard, Document& document) + { + auto rowOneWidgetSize = widget_size_with_row_get(2); + + shortcut(manager.chords[SHORTCUT_ADD]); + if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize) && window.add) + window_command_run(window, manager, settings, document, clipboard, window.add); + set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_REGION), settings.shortcutAdd); + + ImGui::SameLine(); + + shortcut(manager.chords[SHORTCUT_REMOVE]); + if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowOneWidgetSize) && window.remove) + window_command_run(window, manager, settings, document, clipboard, window.remove); + set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_REGIONS), settings.shortcutAdd); + }; + window.popup_update = [](Window& window, Manager& manager, Settings&, Resources&, Clipboard&, Document& document) + { + auto& anm2 = document.anm2; + auto& spritesheetReference = document.spritesheet.reference; + auto& reference = document.region.reference; + auto spritesheet_get = [&]() { return anm2.element_get(ElementType::SPRITESHEET, spritesheetReference); }; + auto region_get = [&](int id) + { + auto spritesheet = spritesheet_get(); + return spritesheet ? element_child_id_get(*spritesheet, ElementType::REGION, id) : nullptr; + }; + + window.popup.trigger(); + + if (ImGui::BeginPopupModal(window.popup.label(), &window.popup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto spritesheet = spritesheet_get(); + auto targetRegion = reference == -1 ? nullptr : region_get(reference); + if (!spritesheet || (reference != -1 && !targetRegion)) + { + window.popup.close(); + ImGui::EndPopup(); + return; + } + + auto childSize = child_size_get(5); + + if (window.popup.isJustOpened) + { + if (!window.isPreserveEditElementOnOpen) + window.editElement = targetRegion ? *targetRegion : element_make(ElementType::REGION); + window.isPreserveEditElementOnOpen = false; + } + auto& region = window.editElement; + + if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders)) + { + const char* originOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT), + localize.get(LABEL_REGION_ORIGIN_CENTER), + localize.get(LABEL_REGION_ORIGIN_CUSTOM)}; + + if (window.popup.isJustOpened) ImGui::SetKeyboardFocusHere(); + input_text_string(localize.get(BASIC_NAME), ®ion.name); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); + ImGui::DragFloat2(localize.get(BASIC_CROP), value_ptr(region.crop), DRAG_SPEED, 0.0f, 0.0f, + math::vec2_format_get(region.crop)); + ImGui::DragFloat2(localize.get(BASIC_SIZE), value_ptr(region.size), DRAG_SPEED, 0.0f, 0.0f, + math::vec2_format_get(region.size)); + ImGui::BeginDisabled(region.origin != Origin::CUSTOM); + ImGui::DragFloat2(localize.get(BASIC_PIVOT), value_ptr(region.pivot), DRAG_SPEED, 0.0f, 0.0f, + math::vec2_format_get(region.pivot)); + ImGui::EndDisabled(); + + int originIndex = region.origin == Origin::TOP_LEFT ? 0 : region.origin == Origin::CENTER ? 1 : 2; + if (ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), &originIndex, originOptions, + IM_ARRAYSIZE(originOptions))) + { + region.origin = originIndex == 0 ? Origin::TOP_LEFT : originIndex == 1 ? Origin::CENTER : Origin::CUSTOM; + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION_PROPERTIES_ORIGIN)); + } + + if (region.origin == Origin::TOP_LEFT) + region.pivot = {}; + else if (region.origin == Origin::CENTER) + region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)}; + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + shortcut(manager.chords[SHORTCUT_CONFIRM]); + if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) + { + auto editedRegion = region; + auto editedReference = reference; + auto editedSpritesheetReference = spritesheetReference; + manager.command_push({manager.selected, [&window, editedRegion, editedReference, + editedSpritesheetReference](Manager&, Document& document) mutable + { + auto spritesheet = + document.anm2.element_get(ElementType::SPRITESHEET, editedSpritesheetReference); + if (!spritesheet) return; + + auto& selection = document.region.selection; + auto& reference = document.region.reference; + auto& frame = document.frames; + + if (editedReference == -1) + { + auto add = [&]() + { + auto added = editedRegion; + auto id = element_child_next_id_get(*spritesheet, ElementType::REGION); + added.type = ElementType::REGION; + added.tag = "Region"; + added.id = id; + spritesheet->children.push_back(added); + selection = {id}; + reference = id; + window.newElementId = id; + }; + + window_edit(window, document, localize.get(EDIT_ADD_REGION), add); + } + else + { + auto set = [&]() + { + auto target = + element_child_id_get(*spritesheet, ElementType::REGION, editedReference); + if (!target) return; + auto changed = editedRegion; + changed.type = ElementType::REGION; + changed.tag = "Region"; + changed.id = editedReference; + *target = changed; + selection = {editedReference}; + }; + + window_edit(window, document, localize.get(EDIT_SET_REGION_PROPERTIES), set); + } + + frame.reference = -1; + frame.selection.clear(); + }}); + + window.popup.close(); + } + + ImGui::SameLine(); + + shortcut(manager.chords[SHORTCUT_CANCEL]); + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) window.popup.close(); + + ImGui::EndPopup(); + } + + window.popup.end(); + }; + return window; + } + + Window spritesheets_window_register() + { + Window window{}; + window.title = LABEL_SPRITESHEETS_WINDOW; + window.isOpen = &Settings::windowIsSpritesheets; + window.changeType = Document::SPRITESHEETS; + window.containerType = ElementType::SPRITESHEETS; + window.elementType = ElementType::SPRITESHEET; + window.childLabel = "##Spritesheets Child"; + window.footerRows = 2; + window.flags = 0; + window.isChildPaddingZero = true; + window.popup = PopupHelper(LABEL_SPRITESHEETS_MERGE_POPUP, POPUP_SMALL_NO_HEIGHT); + window.popup2 = PopupHelper(LABEL_SPRITESHEETS_PACK_POPUP, POPUP_SMALL_NO_HEIGHT); + window.popup3 = PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, POPUP_SMALL_NO_HEIGHT); + window.storage_get = [](Document& document) -> Storage& { return document.spritesheet; }; + window.add = [](Window& window, Manager&, Settings&, Document&, Clipboard&) + { + if (window.dialog) window.dialog->file_open(Dialog::SPRITESHEET_OPEN, true); + }; + window.replace = [](Window& window, Manager&, Settings&, Document&, Clipboard&) + { + if (window.dialog) window.dialog->file_open(Dialog::SPRITESHEET_REPLACE); + }; + window.path_set = [](Window& window, Manager&, Settings&, Document&, Clipboard&) + { + if (window.dialog) window.dialog->file_save(Dialog::SPRITESHEET_PATH_SET); + }; + window.open = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.spritesheet.selection; + if (selection.size() != 1 || !window.dialog) return; + if (auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, *selection.begin())) + window_directory_open(*window.dialog, document, spritesheet->path); + }; + window.remove = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& anm2 = document.anm2; + auto unused = anm2.element_unused(ElementType::SPRITESHEET); + if (unused.empty()) return; + + auto behavior = [&]() + { + auto items = anm2.element_get(ElementType::SPRITESHEETS); + if (!items) return; + for (auto& id : unused) + { + auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) continue; + auto pathString = path::to_utf8(spritesheet->path); + toasts.push(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET), std::make_format_args(id, pathString))); + logger.info(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + element_child_id_erase(*items, ElementType::SPRITESHEET, id); + } + }; + + window_edit(window, document, localize.get(EDIT_REMOVE_UNUSED_SPRITESHEETS), behavior); + }; + window.reload = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto selected = document.spritesheet.selection; + window_edit(window, document, localize.get(EDIT_RELOAD_SPRITESHEETS), []() {}); + + for (auto& id : selected) + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) continue; + document.spritesheet_hash_set_saved(id); + auto pathString = path::to_utf8(spritesheet->path); + toasts.push(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET), std::make_format_args(id, pathString))); + logger.info(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + } + }; + window.save = [](Window&, Manager&, Settings&, Document& document, Clipboard&) + { + window_spritesheets_save(document, document.spritesheet.selection); + }; + window.merge_open = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.spritesheet.selection; + if (selection.size() <= 1) return; + window.selection = selection; + window.popup.open(); + }; + window.merge = [](Window& window, Manager&, Settings& settings, Document& document, Clipboard&) + { + auto ids = window.selection.empty() ? document.spritesheet.selection : window.selection; + window_spritesheets_merge(document, ids, + {.isAppendRight = settings.mergeSpritesheetsOrigin == MERGE_APPEND_RIGHT, + .isMakeRegions = settings.mergeSpritesheetsIsMakeRegions, + .isMakePrimaryRegion = settings.mergeSpritesheetsIsMakePrimaryRegion, + .regionOrigin = (origin::Type)settings.mergeSpritesheetsRegionOrigin}); + window.selection.clear(); + }; + window.pack = [](Window& window, Manager&, Settings& settings, Document& document, Clipboard&) + { + auto id = window.editId != -1 ? window.editId + : document.spritesheet.selection.size() == 1 ? *document.spritesheet.selection.begin() + : -1; + window_spritesheet_pack(document, id, settings.packPadding); + window.editId = -1; + }; + window.copy = [](Window&, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + auto& selection = document.spritesheet.selection; + if (selection.empty()) return; + + std::string clipboardText{}; + for (auto& id : selection) + if (auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id)) + clipboardText += element_to_string(*spritesheet); + clipboard.set(clipboardText); + }; + window.paste = [](Window& window, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + if (clipboard.is_empty()) return; + + auto& anm2 = document.anm2; + auto& selection = document.spritesheet.selection; + auto& reference = document.spritesheet.reference; + auto& region = document.region; + auto items = anm2.element_get(ElementType::SPRITESHEETS); + auto maxSpritesheetIdBefore = items ? element_child_max_id_get(*items, ElementType::SPRITESHEET) : -1; + auto pasted = anm2; + std::string errorString{}; + if (pasted.deserialize(ElementType::SPRITESHEET, clipboard.get(), true, &errorString, document.directory_get())) + { + document.snapshot(localize.get(EDIT_PASTE_SPRITESHEETS)); + anm2 = std::move(pasted); + if (auto pastedItems = anm2.element_get(ElementType::SPRITESHEETS)) + { + auto maxSpritesheetIdAfter = element_child_max_id_get(*pastedItems, ElementType::SPRITESHEET); + if (maxSpritesheetIdAfter > maxSpritesheetIdBefore) + { + window.newElementId = maxSpritesheetIdAfter; + selection = {maxSpritesheetIdAfter}; + reference = maxSpritesheetIdAfter; + region.reference = -1; + region.selection.clear(); + } + } + document.anm2_change(Document::SPRITESHEETS); + } + else + { + toasts.push( + std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED), std::make_format_args(errorString))); + logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED, anm2ed::ENGLISH), + std::make_format_args(errorString))); + } + }; + window.rows_update = + [](Window& window, Manager&, Settings&, Resources& resources, Clipboard&, Document& document, ImVec2) + { + auto& selection = document.spritesheet.selection; + auto& reference = document.spritesheet.reference; + auto& region = document.region; + auto style = ImGui::GetStyle(); + auto spritesheetChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 4); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); + + auto items = document.anm2.element_get(ElementType::SPRITESHEETS); + int spritesheetCount{}; + std::vector ids{}; + if (items) + for (auto& spritesheet : items->children) + if (spritesheet.type == ElementType::SPRITESHEET) + { + ++spritesheetCount; + ids.push_back(spritesheet.id); + } + + selection.start(spritesheetCount); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + for (auto& id : ids) + selection.insert(id); + } + if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + reference = -1; + region.reference = -1; + region.selection.clear(); + } + + int scrollTargetId = -1; + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + (ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true))) + { + auto nextId = window_arrow_selection_get(ids, reference, selection); + if (nextId != -1) + { + selection = {nextId}; + reference = nextId; + region.reference = -1; + region.selection.clear(); + scrollTargetId = nextId; + } + } + + if (items) + { + for (auto& spritesheet : items->children) + { + if (spritesheet.type != ElementType::SPRITESHEET) continue; + auto id = spritesheet.id; + auto isNewSpritesheet = window.newElementId == id; + ImGui::PushID(id); + + window_scroll_to_item(spritesheetChildSize.y, scrollTargetId == id); + + if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders)) + { + auto isSelected = selection.contains(id); + auto isReferenced = id == reference; + auto cursorPos = ImGui::GetCursorPos(); + auto textureInfo = document.texture_get(id); + bool isValid = textureInfo && textureInfo->is_valid(); + auto& texture = isValid ? *textureInfo : resources.icons[icon::NONE]; + auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + auto pathString = path::to_utf8(spritesheet.path); + auto pathCStr = pathString.c_str(); + + ImGui::SetNextItemSelectionUserData(id); + ImGui::SetNextItemStorageID(id); + if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize)) + { + reference = id; + region.reference = -1; + region.selection.clear(); + } + if (scrollTargetId == id) ImGui::SetItemDefaultFocus(); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && window.dialog) + window_directory_open(*window.dialog, document, spritesheet.path); + + auto viewport = ImGui::GetMainViewport(); + auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f; + vec2 textureSize = vec2(glm::max(texture.size.x, 1), glm::max(texture.size.y, 1)); + if (textureSize.x > maxPreviewSize.x || textureSize.y > maxPreviewSize.y) + { + auto scale = glm::min(maxPreviewSize.x / textureSize.x, maxPreviewSize.y / textureSize.y); + textureSize *= scale; + } + + auto textWidth = ImGui::CalcTextSize(pathCStr).x; + auto tooltipPadding = window.tooltipWindowPadding.x * 4.0f; + auto minWidth = textureSize.x + window.tooltipItemSpacing.x + textWidth + tooltipPadding; + + ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, window.tooltipItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, window.tooltipWindowPadding); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); + auto childFlags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; + auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; + if (ImGui::BeginChild("##Spritesheet Tooltip Image Child", to_imvec2(textureSize), childFlags, + noScrollFlags)) + ImGui::ImageWithBg(texture.id, to_imvec2(textureSize), ImVec2(), ImVec2(1, 1), ImVec4(), tintColor); + ImGui::EndChild(); + ImGui::PopStyleVar(); + + ImGui::SameLine(); + + auto infoChildFlags = ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY; + if (ImGui::BeginChild("##Spritesheet Info Tooltip Child", ImVec2(), infoChildFlags, noScrollFlags)) + { + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); + ImGui::TextUnformatted(pathCStr); + ImGui::PopFont(); + + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); + + if (!isValid) + ImGui::TextUnformatted(localize.get(TOOLTIP_SPRITESHEET_INVALID)); + else + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_TEXTURE_SIZE), + std::make_format_args(texture.size.x, texture.size.y)) + .c_str()); + + ImGui::TextUnformatted(localize.get(TEXT_OPEN_DIRECTORY)); + } + ImGui::EndChild(); + + ImGui::EndTooltip(); + } + ImGui::PopStyleVar(2); + + auto imageSize = to_imvec2(vec2(spritesheetChildSize.y)); + auto aspectRatio = texture.size.y != 0 ? (float)texture.size.x / texture.size.y : 1.0f; + + if (imageSize.x / imageSize.y > aspectRatio) + imageSize.x = imageSize.y * aspectRatio; + else + imageSize.y = imageSize.x / aspectRatio; + + ImGui::SetCursorPos(cursorPos); + ImGui::ImageWithBg(texture.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor); + + ImGui::SetCursorPos( + ImVec2(spritesheetChildSize.y + style.ItemSpacing.x, + spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); + + if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); + auto spritesheetLabel = std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathCStr)); + if (document.spritesheet_is_dirty(id)) + spritesheetLabel = + std::vformat(localize.get(FORMAT_SPRITESHEET_NOT_SAVED), std::make_format_args(spritesheetLabel)); + ImGui::TextUnformatted(spritesheetLabel.c_str()); + if (isReferenced) ImGui::PopFont(); + } + + ImGui::EndChild(); + + if (isNewSpritesheet) + { + ImGui::SetScrollHereY(0.5f); + window.newElementId = -1; + } + + ImGui::PopID(); + } + } + + ImGui::PopStyleVar(); + selection.finish(); + }; + window.context_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard& clipboard, Document& document) + { + auto& selection = document.spritesheet.selection; + auto style = ImGui::GetStyle(); + auto is_regions_empty = [&](int id) + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) return true; + for (auto& child : spritesheet->children) + if (child.type == ElementType::REGION) return false; + return true; + }; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && + ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + ImGui::OpenPopup("##Spritesheet Context Menu"); + + auto isPackable = selection.size() == 1 && + document.anm2.element_get(ElementType::SPRITESHEET, *selection.begin()) && + !is_regions_empty(*selection.begin()); + Actions actions{}; + actions_undo_redo_add(actions, manager, document); + actions.separator(); + actions.add(ACTION_OPEN_DIRECTORY, [&]() { return selection.size() == 1 && (bool)window.open; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.open); }); + actions.add(ACTION_SET_FILE_PATH, [&]() { return selection.size() == 1 && (bool)window.path_set; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.path_set); }); + actions.add(ACTION_ADD, [&]() { return (bool)window.add; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.add); }); + actions.add(ACTION_REMOVE_UNUSED, [&]() { return (bool)window.remove; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.remove); }); + actions.add(ACTION_RELOAD, [&]() { return !selection.empty() && (bool)window.reload; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.reload); }); + actions.add(ACTION_REPLACE, [&]() { return selection.size() == 1 && (bool)window.replace; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.replace); }); + actions.add(ACTION_MERGE, [&]() { return selection.size() > 1 && (bool)window.merge_open; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.merge_open); }); + actions.add(ACTION_PACK, [&]() { return isPackable; }, + [&]() + { + window.editId = *selection.begin(); + window.popup2.open(); + }, + TOOLTIP_PACK_SPRITESHEET); + actions.add(ACTION_SAVE, [&]() { return !selection.empty(); }, + [&]() + { + if (settings.fileIsWarnOverwrite) + { + window.selection2 = selection; + window.popup3.open(); + } + else if (window.save) + window_command_run(window, manager, settings, document, clipboard, window.save); + }, + TOOLTIP_SAVE_SPRITESHEETS); + actions.separator(); + actions.add(ACTION_COPY, [&]() { return !selection.empty() && (bool)window.copy; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.copy); }); + actions.add(ACTION_PASTE, [&]() { return !clipboard.is_empty() && (bool)window.paste; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.paste); }); + actions_popup_draw("##Spritesheet Context Menu", actions, settings); + ImGui::PopStyleVar(2); + }; + window.footer_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard& clipboard, Document& document) + { + auto& selection = document.spritesheet.selection; + + auto rowOneWidgetSize = widget_size_with_row_get(3); + Actions rowOneActions{}; + rowOneActions.add(ACTION_ADD, [&]() { return (bool)window.add; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.add); }, + TOOLTIP_ADD_SPRITESHEET); + rowOneActions.add(ACTION_RELOAD, [&]() { return !selection.empty() && (bool)window.reload; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.reload); }, + TOOLTIP_RELOAD_SPRITESHEETS); + rowOneActions.add(ACTION_REPLACE, [&]() { return selection.size() == 1 && (bool)window.replace; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.replace); }, + TOOLTIP_REPLACE_SPRITESHEET); + + bool isSameLine{}; + for (auto& action : rowOneActions.items) + action_button_draw(action, manager, settings, rowOneWidgetSize, isSameLine); + + if (window.dialog && window.dialog->is_selected(Dialog::SPRITESHEET_OPEN)) + { + auto paths = window.dialog->paths; + manager.command_push({manager.selected, [&window, paths](Manager&, Document& document) + { + document.spritesheets_add(paths); + document.region.reference = -1; + document.region.selection.clear(); + window.newElementId = document.spritesheet.reference; + }}); + window.dialog->reset(); + } + + if (window.dialog && window.dialog->is_selected(Dialog::SPRITESHEET_REPLACE)) + { + if (selection.size() == 1 && !window.dialog->path.empty()) + { + auto id = *selection.begin(); + auto dialogPath = window.dialog->path; + manager.command_push({manager.selected, [id, dialogPath](Manager&, Document& document) + { + auto behavior = [&]() + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) return; + spritesheet->path = window_asset_path_get(document, dialogPath); + }; + + window_edit(document, Document::SPRITESHEETS, localize.get(EDIT_REPLACE_SPRITESHEET), + behavior); + if (auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id)) + { + document.spritesheet_hash_set_saved(id); + auto pathString = path::to_utf8(spritesheet->path); + toasts.push(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET), + std::make_format_args(id, pathString))); + logger.info(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + } + }}); + } + window.dialog->reset(); + } + + if (window.dialog && window.dialog->is_selected(Dialog::SPRITESHEET_PATH_SET)) + { + if (selection.size() == 1 && !window.dialog->path.empty()) + { + auto id = *selection.begin(); + auto dialogPath = window.dialog->path; + manager.command_push({manager.selected, [id, dialogPath](Manager&, Document& document) + { + auto behavior = [&]() + { + auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET, id); + if (!spritesheet) return; + spritesheet->path = window_asset_path_get(document, dialogPath); + }; + + window_edit(document, Document::SPRITESHEETS, + localize.get(EDIT_SET_SPRITESHEET_FILE_PATH), behavior); + }}); + } + window.dialog->reset(); + } + + auto rowTwoWidgetSize = widget_size_with_row_get(2); + Actions rowTwoActions{}; + rowTwoActions.add(ACTION_REMOVE_UNUSED, [&]() { return (bool)window.remove; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.remove); }, + TOOLTIP_REMOVE_UNUSED_SPRITESHEETS); + rowTwoActions.add(ACTION_SAVE, [&]() { return !selection.empty(); }, + [&]() + { + if (settings.fileIsWarnOverwrite) + { + window.selection2 = selection; + window.popup3.open(); + } + else if (window.save) + window_command_run(window, manager, settings, document, clipboard, window.save); + }, + TOOLTIP_SAVE_SPRITESHEETS); + + isSameLine = false; + for (auto& action : rowTwoActions.items) + action_button_draw(action, manager, settings, rowTwoWidgetSize, isSameLine); + + if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED) && window.add) + window_command_run(window, manager, settings, document, clipboard, window.add); + if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED) && window.remove) + window_command_run(window, manager, settings, document, clipboard, window.remove); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED) && window.copy) + window_command_run(window, manager, settings, document, clipboard, window.copy); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED) && window.paste) + window_command_run(window, manager, settings, document, clipboard, window.paste); + if (shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED) && selection.size() > 1 && window.merge_open) + window_command_run(window, manager, settings, document, clipboard, window.merge_open); + }; + window.popup_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard&, Document& document) + { + window.popup.trigger(); + if (ImGui::BeginPopupModal(window.popup.label(), &window.popup.isOpen, ImGuiWindowFlags_NoResize)) + { + settings.mergeSpritesheetsRegionOrigin = + glm::clamp(settings.mergeSpritesheetsRegionOrigin, (int)origin::TOP_LEFT, (int)origin::ORIGIN_CENTER); + + auto close = [&]() + { + window.selection.clear(); + window.popup.close(); + }; + + auto optionsSize = child_size_get(6); + if (ImGui::BeginChild("##Merge Spritesheets Options", optionsSize, ImGuiChildFlags_Borders)) + { + ImGui::SeparatorText(localize.get(LABEL_REGION_PROPERTIES_ORIGIN)); + ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_BOTTOM), &settings.mergeSpritesheetsOrigin, + MERGE_APPEND_BOTTOM); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_BOTTOM_LEFT)); + ImGui::SameLine(); + ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_RIGHT), &settings.mergeSpritesheetsOrigin, + MERGE_APPEND_RIGHT); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_TOP_RIGHT)); + + ImGui::SeparatorText(localize.get(LABEL_OPTIONS)); + ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_SPRITESHEET_REGIONS), &settings.mergeSpritesheetsIsMakeRegions); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_SPRITESHEET_REGIONS)); + + ImGui::BeginDisabled(!settings.mergeSpritesheetsIsMakeRegions); + ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION), + &settings.mergeSpritesheetsIsMakePrimaryRegion); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION)); + + const char* regionOriginOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT), + localize.get(LABEL_REGION_ORIGIN_CENTER)}; + ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), &settings.mergeSpritesheetsRegionOrigin, + regionOriginOptions, IM_ARRAYSIZE(regionOriginOptions)); + ImGui::EndDisabled(); + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + shortcut(manager.chords[SHORTCUT_CONFIRM]); + ImGui::BeginDisabled(window.selection.size() <= 1); + if (ImGui::Button(localize.get(BASIC_MERGE), widgetSize)) + { + auto queuedSelection = window.selection; + SpritesheetMergeOptions options{.isAppendRight = settings.mergeSpritesheetsOrigin == MERGE_APPEND_RIGHT, + .isMakeRegions = settings.mergeSpritesheetsIsMakeRegions, + .isMakePrimaryRegion = settings.mergeSpritesheetsIsMakePrimaryRegion, + .regionOrigin = (origin::Type)settings.mergeSpritesheetsRegionOrigin}; + manager.command_push({manager.selected, [queuedSelection, options](Manager&, Document& document) mutable + { + window_spritesheets_merge(document, queuedSelection, options); + }}); + close(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close(); + + ImGui::EndPopup(); + } + window.popup.end(); + + window.popup2.trigger(); + if (ImGui::BeginPopupModal(window.popup2.label(), &window.popup2.isOpen, ImGuiWindowFlags_NoResize)) + { + settings.packPadding = std::max(0, settings.packPadding); + + auto close = [&]() + { + window.editId = -1; + window.popup2.close(); + }; + + auto optionsSize = child_size_get(1); + if (ImGui::BeginChild("##Pack Spritesheet Options", optionsSize, ImGuiChildFlags_Borders)) + ImGui::DragInt(localize.get(LABEL_PACK_PADDING), &settings.packPadding, DRAG_SPEED, 0, PADDING_MAX); + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + shortcut(manager.chords[SHORTCUT_CONFIRM]); + auto spritesheet = + window.editId != -1 ? document.anm2.element_get(ElementType::SPRITESHEET, window.editId) : nullptr; + bool isPackable{}; + if (spritesheet) + for (auto& child : spritesheet->children) + if (child.type == ElementType::REGION) isPackable = true; + ImGui::BeginDisabled(!isPackable); + if (ImGui::Button(localize.get(BASIC_PACK), widgetSize)) + { + auto queuedEditId = window.editId; + auto queuedPadding = settings.packPadding; + manager.command_push({manager.selected, [queuedEditId, queuedPadding](Manager&, Document& document) mutable + { window_spritesheet_pack(document, queuedEditId, queuedPadding); }}); + close(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close(); + + ImGui::EndPopup(); + } + window.popup2.end(); + + window.popup3.trigger(); + if (ImGui::BeginPopupModal(window.popup3.label(), &window.popup3.isOpen, ImGuiWindowFlags_NoResize)) + { + ImGui::TextUnformatted(localize.get(LABEL_OVERWRITE_CONFIRMATION)); + + auto widgetSize = widget_size_with_row_get(2); + + if (ImGui::Button(localize.get(BASIC_YES), widgetSize)) + { + auto queuedSelection = window.selection2; + manager.command_push({manager.selected, [queuedSelection](Manager&, Document& document) mutable + { window_spritesheets_save(document, queuedSelection); }}); + window.selection2.clear(); + window.popup3.close(); + } + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(BASIC_NO), widgetSize)) + { + window.selection2.clear(); + window.popup3.close(); + } + + ImGui::EndPopup(); + } + window.popup3.end(); + }; + return window; + } + + Window sounds_window_register() + { + Window window{}; + window.title = LABEL_SOUNDS_WINDOW; + window.isOpen = &Settings::windowIsSounds; + window.changeType = Document::SOUNDS; + window.containerType = ElementType::SOUNDS; + window.elementType = ElementType::SOUND_ELEMENT; + window.childLabel = "##Sounds Child"; + window.footerRows = 1; + window.flags = 0; + window.isChildPaddingZero = true; + window.storage_get = [](Document& document) -> Storage& { return document.sound; }; + window.add = [](Window& window, Manager&, Settings&, Document&, Clipboard&) + { + if (window.dialog) window.dialog->file_open(Dialog::SOUND_OPEN, true); + }; + window.remove = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto unused = document.anm2.element_unused(ElementType::SOUND_ELEMENT); + if (unused.empty()) return; + + auto behavior = [&]() + { + auto sounds = document.anm2.element_get(ElementType::SOUNDS); + if (!sounds) return; + for (auto& id : unused) + element_child_id_erase(*sounds, ElementType::SOUND_ELEMENT, id); + }; + + window_edit(window, document, localize.get(EDIT_REMOVE_UNUSED_SOUNDS), behavior); + }; + window.reload = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.sound.selection; + auto behavior = [&]() + { + for (auto& id : selection) + { + auto sound = window_element_get(window, document.anm2, id); + if (!sound) continue; + auto pathString = path::to_utf8(sound->path); + toasts.push(std::vformat(localize.get(TOAST_RELOAD_SOUND), std::make_format_args(id, pathString))); + logger.info( + std::vformat(localize.get(TOAST_RELOAD_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString))); + } + }; + + window_edit(window, document, localize.get(EDIT_RELOAD_SOUNDS), behavior); + }; + window.replace = [](Window& window, Manager&, Settings&, Document&, Clipboard&) + { + if (window.dialog) window.dialog->file_open(Dialog::SOUND_REPLACE); + }; + window.open = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto& selection = document.sound.selection; + if (selection.size() != 1 || !window.dialog) return; + if (auto sound = window_element_get(window, document.anm2, *selection.begin())) + window_directory_open(*window.dialog, document, sound->path); + }; + window.copy = [](Window&, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + auto& selection = document.sound.selection; + if (selection.empty()) return; + + std::string clipboardText{}; + auto sounds = document.anm2.element_get(ElementType::SOUNDS); + if (!sounds) return; + for (auto& id : selection) + if (auto sound = element_child_id_get(*sounds, ElementType::SOUND_ELEMENT, id)) + clipboardText += element_to_string(*sound); + clipboard.set(clipboardText); + }; + window.paste = [](Window& window, Manager&, Settings&, Document& document, Clipboard& clipboard) + { + if (clipboard.is_empty()) return; + + auto& anm2 = document.anm2; + auto& reference = document.sound.reference; + auto& selection = document.sound.selection; + auto sounds = anm2.element_get(ElementType::SOUNDS); + auto maxSoundIdBefore = sounds ? element_child_max_id_get(*sounds, ElementType::SOUND_ELEMENT) : -1; + auto pasted = anm2; + std::string errorString{}; + if (pasted.deserialize(ElementType::SOUND_ELEMENT, clipboard.get(), true, &errorString, document.directory_get())) + { + document.snapshot(localize.get(TOAST_SOUNDS_PASTE)); + anm2 = std::move(pasted); + if (auto pastedSounds = anm2.element_get(ElementType::SOUNDS)) + { + auto maxSoundIdAfter = element_child_max_id_get(*pastedSounds, ElementType::SOUND_ELEMENT); + if (maxSoundIdAfter > maxSoundIdBefore) + { + window.newElementId = maxSoundIdAfter; + selection = {maxSoundIdAfter}; + reference = maxSoundIdAfter; + } + } + document.anm2_change(Document::SOUNDS); + } + else + { + toasts.push(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR), std::make_format_args(errorString))); + logger.error(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR, anm2ed::ENGLISH), + std::make_format_args(errorString))); + } + }; + window.rows_update = [](Window& window, Manager& manager, Settings& settings, Resources& resources, + Clipboard& clipboard, Document& document, ImVec2) + { + auto& reference = document.sound.reference; + auto& selection = document.sound.selection; + auto style = ImGui::GetStyle(); + auto soundChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2); + auto sounds = document.anm2.element_get(ElementType::SOUNDS); + int soundCount{}; + std::vector ids{}; + if (sounds) + for (auto& sound : sounds->children) + if (sound.type == ElementType::SOUND_ELEMENT) + { + ++soundCount; + ids.push_back(sound.id); + } + + auto play = [&](int id) + { + if (auto audio = document.sound_get(id)) audio->play(); + }; + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); + selection.start(soundCount); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + for (auto& id : ids) + selection.insert(id); + } + if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear(); + + int scrollTargetId = -1; + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + (ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true))) + { + auto nextId = window_arrow_selection_get(ids, reference, selection); + if (nextId != -1) + { + selection = {nextId}; + reference = nextId; + scrollTargetId = nextId; + } + } + + if (sounds) + { + for (auto& sound : sounds->children) + { + if (sound.type != ElementType::SOUND_ELEMENT) continue; + auto id = sound.id; + auto isNewSound = window.newElementId == id; + ImGui::PushID(id); + + window_scroll_to_item(soundChildSize.y, scrollTargetId == id); + + if (ImGui::BeginChild("##Sound Child", soundChildSize, ImGuiChildFlags_Borders)) + { + auto isSelected = selection.contains(id); + auto cursorPos = ImGui::GetCursorPos(); + auto audio = document.sound_get(id); + bool isValid = audio && audio->is_valid(); + auto& soundIcon = isValid ? resources.icons[icon::SOUND] : resources.icons[icon::NONE]; + auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + auto pathString = path::to_utf8(sound.path); + + ImGui::SetNextItemSelectionUserData(id); + ImGui::SetNextItemStorageID(id); + if (ImGui::Selectable("##Sound Selectable", isSelected, 0, soundChildSize)) + { + reference = id; + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) play(id); + } + if (scrollTargetId == id) ImGui::SetItemDefaultFocus(); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && window.dialog) + window_directory_open(*window.dialog, document, sound.path); + + auto textWidth = ImGui::CalcTextSize(pathString.c_str()).x; + auto tooltipPadding = window.tooltipWindowPadding.x * 4.0f; + auto minWidth = textWidth + window.tooltipItemSpacing.x + tooltipPadding; + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, window.tooltipItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, window.tooltipWindowPadding); + ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); + ImGui::TextUnformatted(pathString.c_str()); + ImGui::PopFont(); + ImGui::Text("%s: %d", localize.get(BASIC_ID), id); + if (!isValid) + { + ImGui::Spacing(); + ImGui::TextWrapped("%s", localize.get(TOOLTIP_SOUND_INVALID)); + } + else + { + ImGui::Text("%s", localize.get(TEXT_SOUND_PLAY)); + ImGui::Text("%s", localize.get(TEXT_OPEN_DIRECTORY)); + } + ImGui::EndTooltip(); + } + ImGui::PopStyleVar(2); + + ImGui::SetCursorPos(cursorPos); + auto imageSize = to_imvec2(vec2(soundChildSize.y)); + ImGui::ImageWithBg(soundIcon.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor); + + ImGui::SetCursorPos(ImVec2(soundChildSize.y + style.ItemSpacing.x, + soundChildSize.y - soundChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); + + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString)).c_str()); + } + + ImGui::EndChild(); + + if (isNewSound) + { + ImGui::SetScrollHereY(0.5f); + window.newElementId = -1; + } + + ImGui::PopID(); + } + } + + ImGui::PopStyleVar(); + selection.finish(); + + if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED) && window.add) + window_command_run(window, manager, settings, document, clipboard, window.add); + if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED) && window.remove) + window_command_run(window, manager, settings, document, clipboard, window.remove); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED) && window.copy) + window_command_run(window, manager, settings, document, clipboard, window.copy); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED) && window.paste) + window_command_run(window, manager, settings, document, clipboard, window.paste); + }; + window.context_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard& clipboard, Document& document) + { + auto& selection = document.sound.selection; + auto style = ImGui::GetStyle(); + auto play = [&](int id) + { + if (auto audio = document.sound_get(id)) audio->play(); + }; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && + ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + ImGui::OpenPopup("##Sound Context Menu"); + + Actions actions{}; + actions_undo_redo_add(actions, manager, document); + actions.separator(); + actions.add(ACTION_PLAY, [&]() { return selection.size() == 1; }, [&]() { play(*selection.begin()); }, + STRING_UNDEFINED, -1); + actions.add(ACTION_OPEN_DIRECTORY, [&]() { return selection.size() == 1 && (bool)window.open; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.open); }); + actions.add(ACTION_ADD, [&]() { return (bool)window.add; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.add); }); + actions.add(ACTION_REMOVE_UNUSED, [&]() { return (bool)window.remove; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.remove); }); + actions.add(ACTION_RELOAD, [&]() { return !selection.empty() && (bool)window.reload; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.reload); }); + actions.add(ACTION_REPLACE, [&]() { return selection.size() == 1 && (bool)window.replace; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.replace); }); + actions.separator(); + actions.add(ACTION_COPY, [&]() { return !selection.empty() && (bool)window.copy; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.copy); }); + actions.add(ACTION_PASTE, [&]() { return !clipboard.is_empty() && (bool)window.paste; }, + [&]() { window_command_run(window, manager, settings, document, clipboard, window.paste); }); + actions_popup_draw("##Sound Context Menu", actions, settings); + ImGui::PopStyleVar(2); + }; + window.footer_update = + [](Window& window, Manager& manager, Settings& settings, Resources&, Clipboard& clipboard, Document& document) + { + auto& selection = document.sound.selection; + auto widgetSize = widget_size_with_row_get(4); + + shortcut(manager.chords[SHORTCUT_ADD]); + if (ImGui::Button(localize.get(BASIC_ADD), widgetSize) && window.add) + window_command_run(window, manager, settings, document, clipboard, window.add); + set_item_tooltip_shortcut(localize.get(TOOLTIP_SOUND_ADD), settings.shortcutAdd); + + if (window.dialog && window.dialog->is_selected(Dialog::SOUND_OPEN)) + { + auto paths = window.dialog->paths; + manager.command_push({manager.selected, [&window, paths](Manager&, Document& document) + { + document.sounds_add(paths); + window.newElementId = document.sound.reference; + }}); + window.dialog->reset(); + } + + ImGui::SameLine(); + + shortcut(manager.chords[SHORTCUT_REMOVE]); + if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize) && window.remove) + window_command_run(window, manager, settings, document, clipboard, window.remove); + set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SOUNDS), settings.shortcutRemove); + + ImGui::SameLine(); + + ImGui::BeginDisabled(selection.empty()); + if (ImGui::Button(localize.get(BASIC_RELOAD), widgetSize) && window.reload) + window_command_run(window, manager, settings, document, clipboard, window.reload); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SOUNDS)); + ImGui::EndDisabled(); + + ImGui::SameLine(); + + ImGui::BeginDisabled(selection.size() != 1); + if (ImGui::Button(localize.get(BASIC_REPLACE), widgetSize) && window.replace) + window_command_run(window, manager, settings, document, clipboard, window.replace); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SOUND)); + ImGui::EndDisabled(); + + if (window.dialog && window.dialog->is_selected(Dialog::SOUND_REPLACE)) + { + if (selection.size() == 1 && !window.dialog->path.empty()) + { + auto id = *selection.begin(); + auto dialogPath = window.dialog->path; + manager.command_push( + {manager.selected, [id, dialogPath](Manager&, Document& document) + { + auto behavior = [&]() + { + auto sound = document.anm2.element_get(ElementType::SOUND_ELEMENT, id); + if (!sound) return; + sound->path = window_asset_path_get(document, dialogPath); + auto pathString = path::to_utf8(sound->path); + toasts.push(std::vformat(localize.get(TOAST_REPLACE_SOUND), std::make_format_args(id, pathString))); + logger.info(std::vformat(localize.get(TOAST_REPLACE_SOUND, anm2ed::ENGLISH), + std::make_format_args(id, pathString))); + }; + + window_edit(document, Document::SOUNDS, localize.get(EDIT_REPLACE_SOUND), behavior); + }}); + } + window.dialog->reset(); + } + }; + return window; + } + + Window layers_window_register() + { + Window window{}; + window.title = LABEL_LAYERS_WINDOW; + window.isOpen = &Settings::windowIsLayers; + window.changeType = Document::LAYERS; + window.containerType = ElementType::LAYERS; + window.elementType = ElementType::LAYER_ELEMENT; + window.childLabel = "##Layers Child"; + window.addTooltip = TOOLTIP_ADD_LAYER; + window.removeUnusedTooltip = TOOLTIP_REMOVE_UNUSED_LAYERS; + window.pasteEdit = EDIT_PASTE_LAYERS; + window.removeUnusedEdit = EDIT_REMOVE_UNUSED_LAYERS; + window.deserializeFailedToast = TOAST_DESERIALIZE_LAYERS_FAILED; + window.flags = WINDOW_ADD | WINDOW_REMOVE_UNUSED | WINDOW_COPY | WINDOW_PASTE | WINDOW_PROPERTIES; + window.storage_get = [](Document& document) -> Storage& { return document.layer; }; + window.row_label_get = [](Document&, const Element& layer) + { + return std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(layer.id, layer.name, layer.spritesheetId)); + }; + window.tooltip_draw = [](Document&, Resources& resources, const Element& layer) + { + ImGui::PushFont(resources.fonts[resource::font::BOLD].get(), resource::font::SIZE); + ImGui::TextUnformatted(layer.name.c_str()); + ImGui::PopFont(); + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(layer.id)).c_str()); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer.spritesheetId)).c_str()); + }; + window.properties_open = [](Manager& manager, int id) { manager.layer_properties_open(id); }; + window.popup_update = [](Window& window, Manager& manager, Settings&, Resources&, Clipboard&, Document& document) + { + auto& reference = document.layer.reference; + auto& propertiesPopup = manager.layerPropertiesPopup; + + manager.layer_properties_trigger(); + + if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto childSize = child_size_get(2); + auto& layer = manager.editLayer; + + if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders)) + { + if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); + input_text_string(localize.get(BASIC_NAME), &layer.name); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); + + combo_id_mapped(localize.get(LABEL_SPRITESHEET), &layer.spritesheetId, document.spritesheet.ids, + document.spritesheet.labels); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET)); + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + shortcut(manager.chords[SHORTCUT_CONFIRM]); + if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) + { + auto editedLayer = layer; + auto editedReference = reference; + manager.command_push({manager.selected, [&window, editedLayer, editedReference](Manager&, Document& document) + { + auto behavior = [&]() + { + auto layers = document.anm2.element_get(ElementType::LAYERS); + if (!layers) return; + auto changed = editedLayer; + changed.type = ElementType::LAYER_ELEMENT; + changed.tag = "Layer"; + + if (editedReference == -1) + { + auto id = element_child_next_id_get(*layers, ElementType::LAYER_ELEMENT); + changed.id = id; + layers->children.push_back(changed); + document.layer.selection = {id}; + document.layer.reference = id; + window.newElementId = id; + return; + } + + auto target = + element_child_id_get(*layers, ElementType::LAYER_ELEMENT, editedReference); + if (!target) return; + changed.id = editedReference; + *target = changed; + document.layer.selection = {editedReference}; + }; + + window_edit(document, Document::LAYERS, + localize.get(editedReference == -1 ? EDIT_ADD_LAYER + : EDIT_SET_LAYER_PROPERTIES), + behavior); + }}); + + manager.layer_properties_close(); + } + + ImGui::SameLine(); + + shortcut(manager.chords[SHORTCUT_CANCEL]); + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.layer_properties_close(); + + manager.layer_properties_end(); + ImGui::EndPopup(); + } + }; + return window; + } + + Window nulls_window_register() + { + Window window{}; + window.title = LABEL_NULLS_WINDOW; + window.isOpen = &Settings::windowIsNulls; + window.changeType = Document::NULLS; + window.containerType = ElementType::NULLS; + window.elementType = ElementType::NULL_ELEMENT; + window.childLabel = "##Nulls Child"; + window.addTooltip = TOOLTIP_ADD_NULL; + window.removeUnusedTooltip = TOOLTIP_REMOVE_UNUSED_NULLS; + window.pasteEdit = EDIT_PASTE_NULLS; + window.removeUnusedEdit = EDIT_REMOVE_UNUSED_NULLS; + window.deserializeFailedToast = TOAST_DESERIALIZE_NULLS_FAILED; + window.flags = + WINDOW_ADD | WINDOW_REMOVE_UNUSED | WINDOW_COPY | WINDOW_PASTE | WINDOW_PROPERTIES | WINDOW_REFERENCE_ITALIC; + window.storage_get = [](Document& document) -> Storage& { return document.null; }; + window.row_label_get = [](Document&, const Element& null) + { return std::vformat(localize.get(FORMAT_NULL), std::make_format_args(null.id, null.name)); }; + window.tooltip_draw = [](Document&, Resources& resources, const Element& element) + { + ImGui::PushFont(resources.fonts[resource::font::BOLD].get(), resource::font::SIZE); + ImGui::TextUnformatted(element.name.c_str()); + ImGui::PopFont(); + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(element.id)).c_str()); + }; + window.properties_open = [](Manager& manager, int id) { manager.null_properties_open(id); }; + window.popup_update = [](Window& window, Manager& manager, Settings&, Resources&, Clipboard&, Document& document) + { + auto& reference = document.null.reference; + auto& propertiesPopup = manager.nullPropertiesPopup; + + manager.null_properties_trigger(); + + if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto childSize = child_size_get(2); + auto& null = manager.editNull; + + if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders)) + { + if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); + input_text_string(localize.get(BASIC_NAME), &null.name); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_NAME)); + + ImGui::Checkbox(localize.get(LABEL_RECT), &null.isShowRect); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_RECT)); + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + shortcut(manager.chords[SHORTCUT_CONFIRM]); + if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) + { + auto editedNull = null; + auto editedReference = reference; + manager.command_push({manager.selected, [&window, editedNull, editedReference](Manager&, Document& document) + { + auto behavior = [&]() + { + auto nulls = document.anm2.element_get(ElementType::NULLS); + if (!nulls) return; + auto changed = editedNull; + changed.type = ElementType::NULL_ELEMENT; + changed.tag = "Null"; + + if (editedReference == -1) + { + auto id = element_child_next_id_get(*nulls, ElementType::NULL_ELEMENT); + changed.id = id; + nulls->children.push_back(changed); + document.null.selection = {id}; + document.null.reference = id; + window.newElementId = id; + return; + } + + auto target = element_child_id_get(*nulls, ElementType::NULL_ELEMENT, editedReference); + if (!target) return; + changed.id = editedReference; + *target = changed; + document.null.selection = {editedReference}; + }; + + window_edit(document, Document::NULLS, + localize.get(editedReference == -1 ? EDIT_ADD_NULL + : EDIT_SET_NULL_PROPERTIES), + behavior); + }}); + + manager.null_properties_close(); + } + + ImGui::SameLine(); + + shortcut(manager.chords[SHORTCUT_CANCEL]); + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.null_properties_close(); + + ImGui::EndPopup(); + } + + manager.null_properties_end(); + }; + return window; + } + + Window events_window_register() + { + Window window{}; + window.title = LABEL_EVENTS_WINDOW; + window.isOpen = &Settings::windowIsEvents; + window.changeType = Document::EVENTS; + window.containerType = ElementType::EVENTS; + window.elementType = ElementType::EVENT_ELEMENT; + window.childLabel = "##Events Child"; + window.addTooltip = TOOLTIP_ADD_EVENT; + window.removeUnusedTooltip = TOOLTIP_REMOVE_UNUSED_EVENTS; + window.addEdit = EDIT_ADD_EVENT; + window.renameEdit = EDIT_RENAME_EVENT; + window.pasteEdit = EDIT_PASTE_EVENTS; + window.removeUnusedEdit = EDIT_REMOVE_UNUSED_EVENTS; + window.deserializeFailedToast = TOAST_DESERIALIZE_EVENTS_FAILED; + window.flags = WINDOW_ADD | WINDOW_REMOVE_UNUSED | WINDOW_COPY | WINDOW_PASTE | WINDOW_RENAME; + window.storage_get = [](Document& document) -> Storage& { return document.event; }; + window.tooltip_draw = [](Document&, Resources& resources, const Element& element) + { + ImGui::PushFont(resources.fonts[resource::font::BOLD].get(), resource::font::SIZE); + ImGui::TextUnformatted(element.name.c_str()); + ImGui::PopFont(); + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(element.id)).c_str()); + }; + window.add = [](Window& window, Manager&, Settings&, Document& document, Clipboard&) + { + auto behavior = [&]() + { + auto events = document.anm2.element_get(ElementType::EVENTS); + if (!events) return; + + auto id = element_child_next_id_get(*events, ElementType::EVENT_ELEMENT); + auto event = element_make(ElementType::EVENT_ELEMENT); + event.id = id; + event.name = localize.get(TEXT_NEW_EVENT); + events->children.push_back(event); + + auto& storage = window.storage_get(document); + storage.selection = {id}; + storage.reference = id; + window.newElementId = id; + }; + + window_edit(window, document, localize.get(window.addEdit), behavior); + }; + return window; + } +} diff --git a/src/imgui/window/window.hpp b/src/imgui/window/window.hpp new file mode 100644 index 0000000..4b2033f --- /dev/null +++ b/src/imgui/window/window.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include + +#include "clipboard.hpp" +#include "dialog.hpp" +#include "util/imgui/imgui.hpp" +#include "manager.hpp" +#include "resources.hpp" +#include "settings.hpp" +#include "storage.hpp" + +namespace anm2ed::imgui +{ + enum WindowFlag + { + WINDOW_ADD = 1 << 0, + WINDOW_REMOVE = 1 << 1, + WINDOW_REMOVE_UNUSED = 1 << 2, + WINDOW_DUPLICATE = 1 << 3, + WINDOW_MERGE = 1 << 4, + WINDOW_DEFAULT = 1 << 5, + WINDOW_CUT = 1 << 6, + WINDOW_COPY = 1 << 7, + WINDOW_PASTE = 1 << 8, + WINDOW_RENAME = 1 << 9, + WINDOW_PROPERTIES = 1 << 10, + WINDOW_REFERENCE_ITALIC = 1 << 11 + }; + + using WindowFlags = int; + + constexpr bool window_flag_has(WindowFlags flags, WindowFlag flag) { return (flags & flag) != 0; } + + struct Window + { + using StorageGet = std::function; + using ElementGet = std::function; + using ElementKeyGet = std::function; + using RowLabelGet = std::function; + using RowFontGet = std::function; + using RowSelect = std::function; + using RenameFinish = std::function; + using TooltipDraw = std::function; + using RowDragDropUpdate = std::function; + using PropertiesOpen = std::function; + using Command = std::function; + using IsAvailable = std::function; + using Update = std::function; + using RowsUpdate = std::function; + + StringType title{}; + bool Settings::* isOpen{}; + Document::ChangeType changeType{}; + ElementType containerType{ElementType::UNKNOWN}; + ElementType elementType{ElementType::UNKNOWN}; + const char* childLabel{"##Window Child"}; + StringType addTooltip{}; + StringType duplicateTooltip{}; + StringType mergeTooltip{}; + StringType removeTooltip{}; + StringType removeUnusedTooltip{}; + StringType defaultTooltip{}; + StringType addEdit{}; + StringType renameEdit{}; + StringType pasteEdit{}; + StringType removeUnusedEdit{}; + StringType deserializeFailedToast{}; + StringType unavailableText{STRING_UNDEFINED}; + int newElementId{-1}; + int scrollQueued{-1}; + int renameQueued{-1}; + int renameId{-1}; + int editId{-1}; + int footerRows{-1}; + RenameState renameState{RENAME_SELECTABLE}; + std::string renameText{}; + PopupHelper popup{STRING_UNDEFINED}; + PopupHelper popup2{STRING_UNDEFINED}; + PopupHelper popup3{STRING_UNDEFINED}; + std::set selection{}; + std::set selection2{}; + std::vector dragSelection{}; + std::vector order{}; + Element editElement{}; + Dialog* dialog{}; + WindowFlags flags{WINDOW_COPY | WINDOW_PASTE}; + bool isChildPaddingZero{}; + bool isPreserveEditElementOnOpen{}; + ImVec2 tooltipWindowPadding{}; + ImVec2 tooltipItemSpacing{}; + StorageGet storage_get{}; + ElementGet element_get{}; + ElementKeyGet element_key_get{}; + RowLabelGet row_label_get{}; + RowFontGet row_font_get{}; + RowSelect row_select{}; + RenameFinish rename_finish{}; + TooltipDraw tooltip_draw{}; + RowDragDropUpdate row_drag_drop_update{}; + PropertiesOpen properties_open{}; + Command add{}; + Command remove{}; + Command duplicate{}; + Command merge{}; + Command merge_open{}; + Command default_set{}; + Command cut{}; + Command copy{}; + Command paste{}; + Command reload{}; + Command replace{}; + Command save{}; + Command pack{}; + Command trim{}; + Command properties{}; + Command open{}; + Command path_set{}; + IsAvailable is_available{}; + Update begin_update{}; + RowsUpdate rows_update{}; + Update context_update{}; + Update footer_update{}; + Update body_update{}; + Update popup_update{}; + Update post_update{}; + }; + + Window animations_window_register(); + Window regions_window_register(); + Window sounds_window_register(); + Window spritesheets_window_register(); + Window layers_window_register(); + Window nulls_window_register(); + Window events_window_register(); + void window_update(Window&, Manager&, Settings&, Resources&, Dialog&, Clipboard&); +} diff --git a/src/imgui/wizard/about.cpp b/src/imgui/wizard/about.cpp index 55f769b..8d3ed6a 100644 --- a/src/imgui/wizard/about.cpp +++ b/src/imgui/wizard/about.cpp @@ -19,6 +19,7 @@ namespace anm2ed::imgui::wizard {""}, {"Designer", font::BOLD}, {"Shweet"}, + {"OpenAI Codex"}, {""}, {"Additional Help", font::BOLD}, {"im-tem"}, @@ -37,7 +38,7 @@ namespace anm2ed::imgui::wizard {"Music", font::BOLD}, {"Soundbin"}, {"\"Digital Antidepressant\""}, - {"https://soundbin.newgrounds.com/"}, + {"https://www.newgrounds.com/audio/listen/1565433"}, {"License: CC0"}, {""}, {"Libraries", font::BOLD}, diff --git a/src/imgui/wizard/change_all_frame_properties.cpp b/src/imgui/wizard/change_all_frame_properties.cpp index e662d22..f24d39a 100644 --- a/src/imgui/wizard/change_all_frame_properties.cpp +++ b/src/imgui/wizard/change_all_frame_properties.cpp @@ -1,31 +1,34 @@ #include "change_all_frame_properties.hpp" #include +#include #include #include #include -#include "math_.hpp" +#include "math.hpp" +#include "util/imgui/imgui.hpp" using namespace anm2ed::util::math; using namespace glm; namespace anm2ed::imgui::wizard { - namespace + enum ChangeDestination { - enum ChangeDestination - { - CHANGE_DESTINATION_FRAMES, - CHANGE_DESTINATION_ANIMATIONS, - }; - } + CHANGE_DESTINATION_FRAMES, + CHANGE_DESTINATION_ANIMATIONS, + CHANGE_DESTINATION_ITEMS, + }; - void ChangeAllFrameProperties::update(Document& document, Settings& settings, bool isFromWizard) + void ChangeAllFrameProperties::update(Manager& manager, Document& document, Settings& settings, bool isFromWizard) { isChanged = false; + auto& anm2 = document.anm2; auto& frames = document.frames.selection; + auto& frameReferences = document.frames.references; + auto& itemReferences = document.items.references; auto& animations = document.animation.selection; auto& isCropX = settings.changeIsCropX; auto& isCropY = settings.changeIsCropY; @@ -71,19 +74,56 @@ namespace anm2ed::imgui::wizard auto& isNulls = settings.changeIsNulls; auto& itemType = document.reference.itemType; + auto is_frame_reference_changeable = [](const Reference& reference) { return reference.itemType != TRIGGER; }; + auto is_item_reference_changeable = [](const Reference& reference) { return reference.itemType != TRIGGER; }; + auto selected_frame_references_get = [&]() + { + std::set result = frameReferences; + if (result.empty()) + for (auto frameIndex : frames) + result.insert({document.reference.animationIndex, itemType, document.reference.itemID, frameIndex}); + std::erase_if(result, [&](const Reference& reference) { return !is_frame_reference_changeable(reference); }); + return result; + }; + auto selected_item_references_get = [&]() + { + std::set result = itemReferences; + if (result.empty() && itemType != NONE) + result.insert({document.reference.animationIndex, itemType, document.reference.itemID}); + std::erase_if(result, [&](const Reference& reference) { return !is_item_reference_changeable(reference); }); + return result; + }; + auto selectedFrameReferences = selected_frame_references_get(); + auto selectedItemReferences = selected_item_references_get(); + bool isFramesDestination = !isFromWizard || destination == CHANGE_DESTINATION_FRAMES; - bool isSelectedFramesAvailable = !frames.empty() && itemType != anm2::TRIGGER; + bool isItemsDestination = isFromWizard && destination == CHANGE_DESTINATION_ITEMS; + bool isSelectedFramesAvailable = !selectedFrameReferences.empty(); + bool isSelectedItemsAvailable = !selectedItemReferences.empty(); bool isSelectedAnimationsAvailable = !animations.empty(); if (isFromWizard) { - if (destination == CHANGE_DESTINATION_FRAMES && !isSelectedFramesAvailable && isSelectedAnimationsAvailable) - destination = CHANGE_DESTINATION_ANIMATIONS; + if (destination == CHANGE_DESTINATION_FRAMES && !isSelectedFramesAvailable) + destination = isSelectedItemsAvailable ? CHANGE_DESTINATION_ITEMS : CHANGE_DESTINATION_ANIMATIONS; + if (destination == CHANGE_DESTINATION_ITEMS && !isSelectedItemsAvailable) + destination = isSelectedFramesAvailable ? CHANGE_DESTINATION_FRAMES : CHANGE_DESTINATION_ANIMATIONS; + if (destination == CHANGE_DESTINATION_ANIMATIONS && !isSelectedAnimationsAvailable && isSelectedItemsAvailable) + destination = CHANGE_DESTINATION_ITEMS; if (destination == CHANGE_DESTINATION_ANIMATIONS && !isSelectedAnimationsAvailable && isSelectedFramesAvailable) destination = CHANGE_DESTINATION_FRAMES; isFramesDestination = destination == CHANGE_DESTINATION_FRAMES; + isItemsDestination = destination == CHANGE_DESTINATION_ITEMS; } - bool isLayerPropertyAvailable = isFramesDestination ? itemType == anm2::LAYER : isLayers; + auto isLayerPropertyAvailable = isFramesDestination + ? std::ranges::any_of(selectedFrameReferences, + [](const Reference& reference) + { return reference.itemType == LAYER; }) + : isItemsDestination + ? std::ranges::any_of(selectedItemReferences, + [](const Reference& reference) + { return reference.itemType == LAYER; }) + : isLayers; #define PROPERTIES_WIDGET(body, checkboxLabel, isEnabled) \ ImGui::Checkbox(checkboxLabel, &isEnabled); \ @@ -212,9 +252,8 @@ namespace anm2ed::imgui::wizard auto duration_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) { - PROPERTIES_WIDGET( - input_int_range(valueLabel, value, anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST), - checkboxLabel, isEnabled); + PROPERTIES_WIDGET(input_int_range(valueLabel, value, FRAME_DURATION_MIN, FRAME_DURATION_MAX, STEP, STEP_FAST), + checkboxLabel, isEnabled); }; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImGui::GetStyle().ItemInnerSpacing); @@ -248,26 +287,22 @@ namespace anm2ed::imgui::wizard std::vector fallbackIds{-1}; std::vector fallbackLabelsString{localize.get(BASIC_NONE)}; std::vector fallbackLabels{fallbackLabelsString[0].c_str()}; - std::vector interpolationIds{anm2::Frame::Interpolation::NONE, anm2::Frame::Interpolation::LINEAR, - anm2::Frame::Interpolation::EASE_IN, anm2::Frame::Interpolation::EASE_OUT, - anm2::Frame::Interpolation::EASE_IN_OUT}; - std::vector interpolationLabelsString{ - localize.get(BASIC_NONE), localize.get(BASIC_LINEAR), localize.get(BASIC_EASE_IN), - localize.get(BASIC_EASE_OUT), localize.get(BASIC_EASE_IN_OUT)}; - std::vector interpolationLabels{interpolationLabelsString[0].c_str(), - interpolationLabelsString[1].c_str(), - interpolationLabelsString[2].c_str(), - interpolationLabelsString[3].c_str(), - interpolationLabelsString[4].c_str()}; + std::vector interpolationIds{(int)Interpolation::NONE, (int)Interpolation::LINEAR, (int)Interpolation::EASE_IN, + (int)Interpolation::EASE_OUT, (int)Interpolation::EASE_IN_OUT}; + std::vector interpolationLabelsString{localize.get(BASIC_NONE), localize.get(BASIC_LINEAR), + localize.get(BASIC_EASE_IN), localize.get(BASIC_EASE_OUT), + localize.get(BASIC_EASE_IN_OUT)}; + std::vector interpolationLabels{ + interpolationLabelsString[0].c_str(), interpolationLabelsString[1].c_str(), + interpolationLabelsString[2].c_str(), interpolationLabelsString[3].c_str(), + interpolationLabelsString[4].c_str()}; const Storage* regionStorage = nullptr; - if (itemType == anm2::LAYER && document.reference.itemID != -1) + if (itemType == LAYER && document.reference.itemID != -1) { - if (auto layerIt = document.anm2.content.layers.find(document.reference.itemID); - layerIt != document.anm2.content.layers.end()) + if (auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, document.reference.itemID)) { - auto spritesheetID = layerIt->second.spritesheetID; - auto regionIt = document.regionBySpritesheet.find(spritesheetID); + auto regionIt = document.regionBySpritesheet.find(layer->spritesheetId); if (regionIt != document.regionBySpritesheet.end()) regionStorage = ®ionIt->second; } } @@ -294,9 +329,9 @@ namespace anm2ed::imgui::wizard #undef PROPERTIES_WIDGET - auto frame_change = [&](anm2::ChangeType changeType) + auto frame_change = [&](ChangeType changeType) { - anm2::FrameChange frameChange; + FrameChange frameChange; if (isCropX) frameChange.cropX = crop.x; if (isCropY) frameChange.cropY = crop.y; if (isSizeX) frameChange.sizeX = size.x; @@ -309,7 +344,7 @@ namespace anm2ed::imgui::wizard if (isScaleY) frameChange.scaleY = scale.y; if (isRotation) frameChange.rotation = std::make_optional(rotation); if (isDuration) frameChange.duration = std::make_optional(duration); - if (isRegion) frameChange.regionID = std::make_optional(regionId); + if (isRegion) frameChange.regionId = std::make_optional(regionId); if (isTintR) frameChange.tintR = tint.r; if (isTintG) frameChange.tintG = tint.g; if (isTintB) frameChange.tintB = tint.b; @@ -318,67 +353,148 @@ namespace anm2ed::imgui::wizard if (isColorOffsetG) frameChange.colorOffsetG = colorOffset.g; if (isColorOffsetB) frameChange.colorOffsetB = colorOffset.b; if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible); - if (isInterpolationSet) - frameChange.interpolation = std::make_optional(static_cast(interpolation)); + if (isInterpolationSet) frameChange.interpolation = std::make_optional(static_cast(interpolation)); if (isFlipXSet) frameChange.isFlipX = std::make_optional(isFlipX); if (isFlipYSet) frameChange.isFlipY = std::make_optional(isFlipY); - auto all_frames_selection = [](anm2::Item& item) - { - std::set selection{}; - for (int i = 0; i < (int)item.frames.size(); ++i) - selection.insert(i); - return selection; - }; + if (isFramesDestination && selectedFrameReferences.empty()) return; + if (isItemsDestination && selectedItemReferences.empty()) return; - if (isFramesDestination) - { - if (auto item = document.item_get()) - { - DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES, - item->frames_change(frameChange, itemType, changeType, frames)); + auto queuedFrameReferences = selectedFrameReferences; + auto queuedItemReferences = selectedItemReferences; + auto queuedAnimations = animations; + auto queuedIsFramesDestination = isFramesDestination; + auto queuedIsItemsDestination = isItemsDestination; + auto queuedIsRoot = isRoot; + auto queuedIsLayers = isLayers; + auto queuedIsNulls = isNulls; - isChanged = true; - } - } - else - { - auto behavior = [&]() - { - for (auto animationIndex : animations) - { - if (animationIndex < 0 || animationIndex >= (int)document.anm2.animations.items.size()) continue; - auto& animation = document.anm2.animations.items[animationIndex]; + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto all_frames_selection = [](const Element& item) + { + std::set selection{}; + int index{}; + for (const auto& frame : item.children) + if (frame.type == ElementType::FRAME) + { + selection.insert(index); + ++index; + } + return selection; + }; - if (isRoot) - { - auto selection = all_frames_selection(animation.rootAnimation); - animation.rootAnimation.frames_change(frameChange, anm2::ROOT, changeType, selection); - } + auto& anm2 = document.anm2; - if (isLayers) - { - for (auto& [_, item] : animation.layerAnimations) - { - auto selection = all_frames_selection(item); - item.frames_change(frameChange, anm2::LAYER, changeType, selection); - } - } + if (queuedIsFramesDestination) + { + std::map> groupedFrames{}; + for (auto frameReference : queuedFrameReferences) + { + auto itemReference = frameReference; + itemReference.frameIndex = -1; + groupedFrames[itemReference].insert(frameReference.frameIndex); + } - if (isNulls) - { - for (auto& [_, item] : animation.nullAnimations) - { - auto selection = all_frames_selection(item); - item.frames_change(frameChange, anm2::NULL_, changeType, selection); - } - } - } - }; + document.snapshot(localize.get(EDIT_CHANGE_FRAME_PROPERTIES)); + for (auto& [itemReference, selection] : groupedFrames) + { + auto item = anm2.element_get(itemReference.animationIndex, + static_cast(itemReference.itemType), + itemReference.itemID); + if (!item) continue; + frames_change(*item, frameChange, static_cast(itemReference.itemType), + changeType, selection); + } + document.anm2_change(Document::FRAMES); + return; + } - DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES, behavior()); - isChanged = true; - } + if (queuedIsItemsDestination) + { + document.snapshot(localize.get(EDIT_CHANGE_FRAME_PROPERTIES)); + for (auto itemReference : queuedItemReferences) + { + if (itemReference.itemType == TRIGGER) continue; + auto item = anm2.element_get(itemReference.animationIndex, + static_cast(itemReference.itemType), + itemReference.itemID); + if (!item) continue; + auto selection = all_frames_selection(*item); + frames_change(*item, frameChange, static_cast(itemReference.itemType), + changeType, selection); + } + document.anm2_change(Document::FRAMES); + return; + } + + document.snapshot(localize.get(EDIT_CHANGE_FRAME_PROPERTIES)); + for (auto animationIndex : queuedAnimations) + { + auto animation = anm2.element_get(ElementType::ANIMATION, animationIndex); + if (!animation) continue; + + if (queuedIsRoot) + { + if (auto item = animation_item_get(*animation, ItemType::ROOT)) + { + auto selection = all_frames_selection(*item); + frames_change(*item, frameChange, ItemType::ROOT, changeType, selection); + } + } + + if (queuedIsLayers) + { + auto layerAnimations = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS); + if (layerAnimations) + { + auto item_change = [&](auto&& self, Element& item) -> void + { + if (item.type == ElementType::GROUP) + { + for (auto& child : item.children) + self(self, child); + return; + } + if (item.type == ElementType::LAYER_ANIMATION) + { + auto selection = all_frames_selection(item); + frames_change(item, frameChange, ItemType::LAYER, changeType, selection); + } + }; + for (auto& item : layerAnimations->children) + item_change(item_change, item); + } + } + + if (queuedIsNulls) + { + auto nullAnimations = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS); + if (nullAnimations) + { + auto item_change = [&](auto&& self, Element& item) -> void + { + if (item.type == ElementType::GROUP) + { + for (auto& child : item.children) + self(self, child); + return; + } + if (item.type == ElementType::NULL_ANIMATION) + { + auto selection = all_frames_selection(item); + frames_change(item, frameChange, ItemType::NULL_, changeType, selection); + } + }; + for (auto& item : nullAnimations->children) + item_change(item_change, item); + } + } + } + document.anm2_change(Document::FRAMES); + }}); + isChanged = true; }; ImGui::Separator(); @@ -394,12 +510,19 @@ namespace anm2ed::imgui::wizard ImGui::SameLine(); + ImGui::BeginDisabled(!isSelectedItemsAvailable); + ImGui::RadioButton(localize.get(LABEL_ITEMS), &destination, CHANGE_DESTINATION_ITEMS); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CHANGE_ALL_DESTINATION_ITEMS)); + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::BeginDisabled(!isSelectedAnimationsAvailable); ImGui::RadioButton(localize.get(LABEL_ANIMATIONS_CHILD), &destination, CHANGE_DESTINATION_ANIMATIONS); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CHANGE_ALL_DESTINATION_ANIMATIONS)); ImGui::EndDisabled(); - ImGui::BeginDisabled(isFramesDestination); + ImGui::BeginDisabled(isFramesDestination || isItemsDestination); ImGui::Checkbox(localize.get(LABEL_ROOT), &isRoot); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CHANGE_ALL_ROOT)); ImGui::SameLine(); @@ -418,33 +541,34 @@ namespace anm2ed::imgui::wizard isTintB || isTintA || isColorOffsetR || isColorOffsetG || isColorOffsetB || isRegion || isVisibleSet || isInterpolationSet || isFlipXSet || isFlipYSet; bool isDestinationValid = isFramesDestination ? isSelectedFramesAvailable - : isSelectedAnimationsAvailable && (isRoot || isLayers || isNulls); + : isItemsDestination ? isSelectedItemsAvailable + : isSelectedAnimationsAvailable && (isRoot || isLayers || isNulls); auto rowWidgetSize = widget_size_with_row_get(5); ImGui::BeginDisabled(!isAnyProperty || !isDestinationValid); - if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(anm2::ADJUST); + if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(ChangeType::ADJUST); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADJUST)); ImGui::SameLine(); - if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(anm2::ADD); + if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(ChangeType::ADD); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_VALUES)); ImGui::SameLine(); - if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(anm2::SUBTRACT); + if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(ChangeType::SUBTRACT); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SUBTRACT_VALUES)); ImGui::SameLine(); - if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(anm2::MULTIPLY); + if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(ChangeType::MULTIPLY); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MULTIPLY_VALUES)); ImGui::SameLine(); - if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(anm2::DIVIDE); + if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(ChangeType::DIVIDE); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DIVIDE_VALUES)); ImGui::EndDisabled(); diff --git a/src/imgui/wizard/change_all_frame_properties.hpp b/src/imgui/wizard/change_all_frame_properties.hpp index 3d7373c..60fb2cc 100644 --- a/src/imgui/wizard/change_all_frame_properties.hpp +++ b/src/imgui/wizard/change_all_frame_properties.hpp @@ -1,6 +1,7 @@ #pragma once #include "document.hpp" +#include "manager.hpp" #include "settings.hpp" namespace anm2ed::imgui::wizard @@ -10,6 +11,6 @@ namespace anm2ed::imgui::wizard public: bool isChanged{}; - void update(Document&, Settings&, bool = false); + void update(Manager&, Document&, Settings&, bool = false); }; } diff --git a/src/imgui/wizard/configure.cpp b/src/imgui/wizard/configure.cpp index 7b3b996..acc927c 100644 --- a/src/imgui/wizard/configure.cpp +++ b/src/imgui/wizard/configure.cpp @@ -1,9 +1,9 @@ #include "configure.hpp" -#include "imgui_.hpp" #include "log.hpp" -#include "path_.hpp" +#include "path.hpp" #include "sdl.hpp" +#include "util/imgui/imgui.hpp" using namespace anm2ed::types; @@ -24,11 +24,9 @@ namespace anm2ed::imgui::wizard if (ImGui::BeginChild("##Tab Child", childSize, true)) { ImGui::SeparatorText(localize.get(LABEL_WINDOW_MENU)); - input_float_range(localize.get(LABEL_UI_SCALE), temporary.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f"); + input_float_range(localize.get(LABEL_UI_SCALE), temporary.uiScale, UI_SCALE_MIN, UI_SCALE_MAX, + UI_SCALE_STEP, UI_SCALE_STEP, "%.2fx"); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_UI_SCALE)); - input_float_range(localize.get(LABEL_ITEM_HEIGHT), temporary.timelineItemHeight, 0.75f, 1.25f, 0.05f, 0.05f, - "%.2f"); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_HEIGHT)); ImGui::Checkbox(localize.get(LABEL_VSYNC), &temporary.isVsync); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC)); @@ -70,13 +68,13 @@ namespace anm2ed::imgui::wizard ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_STACK_SIZE)); ImGui::SeparatorText(localize.get(LABEL_COMPATIBILITY)); - ImGui::RadioButton(localize.get(LABEL_ISAAC), &temporary.fileCompatibility, anm2::ISAAC); + ImGui::RadioButton(localize.get(LABEL_ISAAC), &temporary.fileCompatibility, ISAAC); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ISAAC)); ImGui::SameLine(); - ImGui::RadioButton(localize.get(LABEL_ANM2ED), &temporary.fileCompatibility, anm2::ANM2ED); + ImGui::RadioButton(localize.get(LABEL_ANM2ED), &temporary.fileCompatibility, ANM2ED); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED)); ImGui::SameLine(); - ImGui::RadioButton(localize.get(LABEL_ANM2ED_LIMITED), &temporary.fileCompatibility, anm2::ANM2ED_LIMITED); + ImGui::RadioButton(localize.get(LABEL_ANM2ED_LIMITED), &temporary.fileCompatibility, ANM2ED_LIMITED); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED_LIMITED)); ImGui::Checkbox(localize.get(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_ON_SAVE), diff --git a/src/imgui/wizard/generate_animation_from_grid.cpp b/src/imgui/wizard/generate_animation_from_grid.cpp index a61852b..bcc867c 100644 --- a/src/imgui/wizard/generate_animation_from_grid.cpp +++ b/src/imgui/wizard/generate_animation_from_grid.cpp @@ -1,7 +1,10 @@ #include "generate_animation_from_grid.hpp" -#include "math_.hpp" +#include + +#include "math.hpp" #include "types.hpp" +#include "util/imgui/imgui.hpp" using namespace anm2ed::types; using namespace anm2ed::util; @@ -11,10 +14,12 @@ namespace anm2ed::imgui::wizard { GenerateAnimationFromGrid::GenerateAnimationFromGrid() : Canvas(vec2()) {} - void GenerateAnimationFromGrid::update(Document& document, Resources& resources, Settings& settings) + void GenerateAnimationFromGrid::update(Manager& manager, Document& document, Resources& resources, Settings& settings) { isEnd = false; + auto& anm2 = document.anm2; + auto& reference = document.reference; auto& startPosition = settings.generateStartPosition; auto& size = settings.generateSize; auto& pivot = settings.generatePivot; @@ -35,12 +40,17 @@ namespace anm2ed::imgui::wizard ImGui::InputInt(localize.get(LABEL_GENERATE_ROWS), &rows, STEP, STEP_FAST); ImGui::InputInt(localize.get(LABEL_GENERATE_COLUMNS), &columns, STEP, STEP_FAST); - input_int_range(localize.get(LABEL_GENERATE_COUNT), count, anm2::FRAME_NUM_MIN, rows * columns); + input_int_range(localize.get(LABEL_GENERATE_COUNT), count, FRAME_NUM_MIN, rows * columns); ImGui::InputInt(localize.get(BASIC_DURATION), &delay, STEP, STEP_FAST); } ImGui::EndChild(); + rows = std::max(rows, 1); + columns = std::max(columns, 1); + count = std::clamp(count, FRAME_NUM_MIN, rows * columns); + delay = std::max(delay, FRAME_DURATION_MIN); + ImGui::SameLine(); if (ImGui::BeginChild("##Preview Child", childSize, ImGuiChildFlags_Borders)) @@ -58,28 +68,23 @@ namespace anm2ed::imgui::wizard viewport_set(); clear(isTransparent ? vec4(0) : vec4(backgroundColor, 1.0f)); - if (document.reference.itemType == anm2::LAYER) + if (reference.itemType == LAYER) { - auto layerIt = document.anm2.content.layers.find(document.reference.itemID); - if (layerIt != document.anm2.content.layers.end()) + auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID); + auto sourceTexture = layer ? document.texture_get(layer->spritesheetId) : nullptr; + if (sourceTexture) { - auto spritesheetIt = document.anm2.content.spritesheets.find(layerIt->second.spritesheetID); - if (spritesheetIt != document.anm2.content.spritesheets.end()) - { - auto& texture = spritesheetIt->second.texture; + 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); + auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(sourceTexture->size); + auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(sourceTexture->size); - 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); - auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size); - auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size); + mat4 transform = transform_get(zoom) * math::quad_model_get(size, {}, pivot); - mat4 transform = transform_get(zoom) * math::quad_model_get(size, {}, pivot); - - texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {}, - math::uv_vertices_get(uvMin, uvMax).data()); - } + texture_render(shaderTexture, sourceTexture->id, transform, vec4(1.0f), {}, + math::uv_vertices_get(uvMin, uvMax).data()); } } @@ -104,19 +109,32 @@ namespace anm2ed::imgui::wizard if (ImGui::Button(localize.get(LABEL_GENERATE), widgetSize)) { - auto generate_from_grid = [&]() - { - auto item = document.item_get(); - auto animation = document.animation_get(); + auto queuedReference = reference; + auto queuedStartPosition = startPosition; + auto queuedSize = size; + auto queuedPivot = pivot; + auto queuedColumns = columns; + auto queuedCount = count; + auto queuedDelay = delay; - if (item && animation) - { - item->frames_generate_from_grid(startPosition, size, pivot, columns, count, delay); - animation->frameNum = animation->length(); - } - }; + manager.command_push({manager.selected, + [=](Manager&, Document& document) + { + auto itemType = static_cast(queuedReference.itemType); + auto item = document.anm2.element_get(queuedReference.animationIndex, itemType, + queuedReference.itemID); + auto animation = + document.anm2.element_get(ElementType::ANIMATION, queuedReference.animationIndex); - DOCUMENT_EDIT(document, localize.get(EDIT_GENERATE_ANIMATION_FROM_GRID), Document::FRAMES, generate_from_grid()); + if (item && animation) + { + document.snapshot(localize.get(EDIT_GENERATE_ANIMATION_FROM_GRID)); + frames_generate_from_grid(*item, queuedStartPosition, queuedSize, queuedPivot, + queuedColumns, queuedCount, queuedDelay); + animation->frameNum = animation_length_get(*animation); + document.anm2_change(Document::FRAMES); + } + }}); isEnd = true; } diff --git a/src/imgui/wizard/generate_animation_from_grid.hpp b/src/imgui/wizard/generate_animation_from_grid.hpp index 88d0330..4b058f6 100644 --- a/src/imgui/wizard/generate_animation_from_grid.hpp +++ b/src/imgui/wizard/generate_animation_from_grid.hpp @@ -2,6 +2,7 @@ #include "canvas.hpp" #include "document.hpp" +#include "manager.hpp" #include "resources.hpp" #include "settings.hpp" @@ -15,6 +16,6 @@ namespace anm2ed::imgui::wizard bool isEnd{}; GenerateAnimationFromGrid(); - void update(Document&, Resources&, Settings&); + void update(Manager&, Document&, Resources&, Settings&); }; } diff --git a/src/imgui/wizard/render_animation.cpp b/src/imgui/wizard/render_animation.cpp index 9bb70d5..b965bd2 100644 --- a/src/imgui/wizard/render_animation.cpp +++ b/src/imgui/wizard/render_animation.cpp @@ -3,10 +3,10 @@ #include #include -#include "imgui_.hpp" +#include "util/imgui/imgui.hpp" #include "log.hpp" -#include "path_.hpp" -#include "process_.hpp" +#include "path.hpp" +#include "process.hpp" #include "toast.hpp" using namespace anm2ed::resource; @@ -16,7 +16,7 @@ namespace anm2ed::imgui::wizard { void RenderAnimation::range_to_animation_set(Manager& manager, Document& document) { - if (auto animation = document.animation_get()) + if (auto animation = document.anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex)) { manager.recordingStart = 0; manager.recordingEnd = animation->frameNum - 1; @@ -28,10 +28,12 @@ namespace anm2ed::imgui::wizard auto& frames = document.frames.selection; if (!frames.empty()) { - if (auto item = document.item_get()) + auto& reference = document.reference; + auto itemType = static_cast(reference.itemType); + if (auto item = document.anm2.element_get(reference.animationIndex, itemType, reference.itemID)) { int duration{}; - for (auto [i, frame] : std::views::enumerate(item->frames)) + for (auto [i, frame] : std::views::enumerate(item->children)) { if ((int)i == *frames.begin()) manager.recordingStart = duration; if ((int)i == *frames.rbegin()) manager.recordingEnd = duration + frame.duration - 1; @@ -53,7 +55,7 @@ namespace anm2ed::imgui::wizard { isEnd = false; - auto animation = document.animation_get(); + auto animation = document.anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex); if (!animation) return; auto& ffmpegPath = settings.renderFFmpegPath; @@ -155,7 +157,7 @@ namespace anm2ed::imgui::wizard ImGui::SameLine(); - ImGui::BeginDisabled(frames.empty() || reference.itemID == anm2::TRIGGER); + ImGui::BeginDisabled(frames.empty() || reference.itemType == TRIGGER); if (ImGui::Button(localize.get(LABEL_TO_SELECTED_FRAMES))) range_to_frames_set(manager, document); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_SELECTED_FRAMES)); ImGui::EndDisabled(); diff --git a/src/loader.cpp b/src/loader.cpp index 6ac8632..a7518e7 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -13,12 +13,12 @@ #include "log.hpp" #include "sdl.hpp" -#include "imgui_.hpp" +#include "util/imgui/imgui.hpp" #include "snapshots.hpp" #include "socket.hpp" -#include "util/math_.hpp" +#include "util/math.hpp" #ifdef _WIN32 #include diff --git a/src/log.cpp b/src/log.cpp index 18ecd4e..143363c 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -5,10 +5,10 @@ #include #include -#include "file_.hpp" -#include "path_.hpp" +#include "file.hpp" +#include "path.hpp" #include "sdl.hpp" -#include "time_.hpp" +#include "time.hpp" #if _WIN32 #include diff --git a/src/manager.cpp b/src/manager.cpp index f16d31c..0456a10 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -5,13 +5,14 @@ #include -#include "file_.hpp" +#include "file.hpp" #include "log.hpp" -#include "path_.hpp" +#include "path.hpp" #include "sdl.hpp" #include "strings.hpp" #include "toast.hpp" -#include "vector_.hpp" +#include "util/imgui/shortcut.hpp" +#include "vector.hpp" using namespace anm2ed::types; using namespace anm2ed::util; @@ -79,6 +80,28 @@ namespace anm2ed Document* Manager::get(int index) { return vector::find(documents, index > -1 ? index : selected); } + void Manager::command_push(Command command) + { + if (command.documentIndex == -1) command.documentIndex = selected; + commands.push_back(std::move(command)); + } + + void Manager::commands_run() + { + auto queued = std::move(commands); + commands.clear(); + + for (auto& command : queued) + { + if (command.runManager) continue; + + if (auto document = get(command.documentIndex); document) document->command_run(*this, command); + } + + for (auto& command : queued) + if (command.runManager) command.runManager(*this); + } + Document* Manager::open(const std::filesystem::path& path, bool isNew, bool isRecent) { std::string errorString{}; @@ -109,41 +132,44 @@ namespace anm2ed void Manager::new_(const std::filesystem::path& path) { open(path, true); } - void Manager::save(int index, const std::filesystem::path& path, anm2::Compatibility compatibility, + bool Manager::save(int index, const std::filesystem::path& path, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) { if (auto document = get(index); document) { std::string errorString{}; - ensure_parent_directory_exists(path); const auto previousAutosavePath = document->autosave_path_get(); - document->path = !path.empty() ? path : document->path; - document->path.replace_extension(".anm2"); + auto savePath = !path.empty() ? path : document->path; + savePath.replace_extension(".anm2"); + ensure_parent_directory_exists(savePath); + + if (!document->save(savePath, &errorString, compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, + isRoundRotation)) + return false; + const auto autosavePath = document->autosave_path_get(); - if (document->save(document->path, &errorString, compatibility, isBakeSpecialInterpolatedFramesOnSave, - isRoundScale, isRoundRotation)) - { - autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), previousAutosavePath), - autosaveFiles.end()); - if (autosavePath != previousAutosavePath) - autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), autosavePath), - autosaveFiles.end()); - autosave_file_remove(previousAutosavePath); - autosave_file_remove(autosavePath); - autosave_files_write(); - } + autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), previousAutosavePath), + autosaveFiles.end()); + if (autosavePath != previousAutosavePath) + autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), autosavePath), autosaveFiles.end()); + autosave_file_remove(previousAutosavePath); + autosave_file_remove(autosavePath); + autosave_files_write(); recent_file_add(document->path); + return true; } + + return false; } - void Manager::save(const std::filesystem::path& path, anm2::Compatibility compatibility, + bool Manager::save(const std::filesystem::path& path, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) { - save(selected, path, compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation); + return save(selected, path, compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation); } - void Manager::autosave(Document& document, anm2::Compatibility compatibility, - bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) + void Manager::autosave(Document& document, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave, + bool isRoundScale, bool isRoundRotation) { std::string errorString{}; auto autosavePath = document.autosave_path_get(); @@ -210,9 +236,9 @@ namespace anm2ed if (auto document = get(); document) { if (id == -1) - editLayer = anm2::Layer(); - else if (auto it = document->anm2.content.layers.find(id); it != document->anm2.content.layers.end()) - editLayer = it->second; + editLayer = element_make(ElementType::LAYER_ELEMENT); + else if (auto layer = document->anm2.element_get(ElementType::LAYER_ELEMENT, id); layer) + editLayer = *layer; else return; @@ -228,7 +254,7 @@ namespace anm2ed void Manager::layer_properties_close() { - editLayer = anm2::Layer(); + editLayer = element_make(ElementType::LAYER_ELEMENT); layerPropertiesPopup.close(); } @@ -236,10 +262,11 @@ namespace anm2ed { if (auto document = get(); document) { + auto nulls = document->anm2.element_get(ElementType::NULLS); if (id == -1) - editNull = anm2::Null(); - else if (auto it = document->anm2.content.nulls.find(id); it != document->anm2.content.nulls.end()) - editNull = it->second; + editNull = element_make(ElementType::NULL_ELEMENT); + else if (auto null = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr; null) + editNull = *null; else return; @@ -255,7 +282,7 @@ namespace anm2ed void Manager::null_properties_close() { - editNull = anm2::Null(); + editNull = element_make(ElementType::NULL_ELEMENT); nullPropertiesPopup.close(); } diff --git a/src/manager.hpp b/src/manager.hpp index 434e36c..dc3aeb7 100644 --- a/src/manager.hpp +++ b/src/manager.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,11 +9,21 @@ #include "document.hpp" #include "settings.hpp" #include "strings.hpp" +#include "util/imgui/popup.hpp" namespace anm2ed { constexpr auto FILE_LABEL_FORMAT = "{} [{}]"; + class Manager; + + struct Command + { + int documentIndex{-1}; + std::function run{}; + std::function runManager{}; + }; + class Manager { std::filesystem::path recent_files_path_get(); @@ -23,6 +34,7 @@ namespace anm2ed public: std::vector documents{}; + std::vector commands{}; std::map> recentFiles{}; std::size_t recentFilesCounter{}; std::vector autosaveFiles{}; @@ -47,15 +59,15 @@ namespace anm2ed std::filesystem::path spritesheetDragDropPath{}; bool isSpritesheetDragDrop{}; - anm2::Layer editLayer{}; + Element editLayer{element_make(ElementType::LAYER_ELEMENT)}; imgui::PopupHelper layerPropertiesPopup{ imgui::PopupHelper(LABEL_MANAGER_LAYER_PROPERTIES, imgui::POPUP_SMALL_NO_HEIGHT)}; - anm2::Null editNull{}; + Element editNull{element_make(ElementType::NULL_ELEMENT)}; imgui::PopupHelper nullPropertiesPopup{ imgui::PopupHelper(LABEL_MANAGER_NULL_PROPERTIES, imgui::POPUP_SMALL_NO_HEIGHT)}; - anm2::Spritesheet::Region makeRegion{}; + Element makeRegion{element_make(ElementType::REGION)}; int makeRegionSpritesheetId{-1}; bool isMakeRegionRequested{}; @@ -68,13 +80,15 @@ namespace anm2ed ~Manager(); Document* get(int = -1); + void command_push(Command); + void commands_run(); Document* open(const std::filesystem::path&, bool = false, bool = true); void new_(const std::filesystem::path&); - void save(int, const std::filesystem::path& = {}, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true, + bool save(int, const std::filesystem::path& = {}, Compatibility = Compatibility::ANM2ED, bool = false, bool = true, bool = true); - void save(const std::filesystem::path& = {}, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true, + bool save(const std::filesystem::path& = {}, Compatibility = Compatibility::ANM2ED, bool = false, bool = true, bool = true); - void autosave(Document&, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true, bool = true); + void autosave(Document&, Compatibility = Compatibility::ANM2ED, bool = false, bool = true, bool = true); void set(int); void close(int); void layer_properties_open(int = -1); diff --git a/src/render.cpp b/src/render.cpp index 4e32b2b..f183d63 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -9,10 +9,10 @@ #include #include "log.hpp" -#include "path_.hpp" -#include "process_.hpp" +#include "path.hpp" +#include "process.hpp" #include "sdl.hpp" -#include "string_.hpp" +#include "string.hpp" using namespace anm2ed::util; diff --git a/src/resource/audio.cpp b/src/resource/audio.cpp index 9ea969f..98e5405 100644 --- a/src/resource/audio.cpp +++ b/src/resource/audio.cpp @@ -3,7 +3,7 @@ #include #include -#include "file_.hpp" +#include "file.hpp" using namespace anm2ed::util; diff --git a/src/resource/icon.hpp b/src/resource/icon.hpp index 7bcb487..f38af5e 100644 --- a/src/resource/icon.hpp +++ b/src/resource/icon.hpp @@ -21,6 +21,10 @@ namespace anm2ed::resource::icon )"; + inline constexpr auto FOLDER_OPEN_DATA = R"( + +)"; + inline constexpr auto CLOSE_DATA = R"( )"; @@ -173,6 +177,7 @@ namespace anm2ed::resource::icon X(NONE, NONE_DATA, SIZE_SMALL) \ X(FILE, FILE_DATA, SIZE_NORMAL) \ X(FOLDER, FOLDER_DATA, SIZE_NORMAL) \ + X(FOLDER_OPEN, FOLDER_OPEN_DATA, SIZE_NORMAL) \ X(CLOSE, CLOSE_DATA, SIZE_NORMAL) \ X(ROOT, ROOT_DATA, SIZE_NORMAL) \ X(LAYER, LAYER_DATA, SIZE_NORMAL) \ diff --git a/src/resource/strings.hpp b/src/resource/strings.hpp index c06bd84..3d74191 100644 --- a/src/resource/strings.hpp +++ b/src/resource/strings.hpp @@ -37,6 +37,7 @@ namespace anm2ed X(BASIC_ALPHA, "Alpha", "Alpha", "Прозрачность", "透明度", "불투명도") \ X(BASIC_APPEND, "Append", "Anteponer", "Добавить к концу", "添加", "추가") \ X(BASIC_AT_FRAME, "At Frame", "En Frame", "На кадре", "触发帧", "트리거될 프레임") \ + X(BASIC_BATCH, "Batch", "Lote", "Пакетный", "批量", "일괄") \ X(BASIC_BEFORE, "Before", "Antes", "До", "前一帧", "전") \ X(BASIC_BORDER, "Border", "Borde", "Границы", "边框", "경계선") \ X(BASIC_CANCEL, "Cancel", "Cancelar", "Отмена", "取消", "취소") \ @@ -56,6 +57,7 @@ namespace anm2ed X(BASIC_EVENT, "Event", "Evento", "Событие", "事件", "이벤트") \ X(BASIC_FRAME, "Frame", "Frame", "Кадр", "帧", "프레임") \ X(BASIC_FRAMES, "Frames", "Frames", "Кадры", "帧", "프레임") \ + X(BASIC_GROUP, "Group", "Grupo", "Группа", "组", "그룹") \ X(BASIC_GRID, "Grid", "Cuadricula", "Сетка", "网格", "격자") \ X(BASIC_ID, "ID", "ID", "ID", "ID", "ID") \ X(BASIC_INDEX, "Index", "Indice", "Индекс", "下标", "인덱스") \ @@ -88,6 +90,7 @@ namespace anm2ed X(BASIC_SAVE, "Save", "Guardar", "Сохранить", "保存", "저장") \ X(BASIC_SCALE, "Scale", "Escalar", "Масштаб", "缩放", "크기") \ X(BASIC_SET_FILE_PATH, "Set File Path", "Set File Path", "Set File Path", "Set File Path", "Set File Path") \ + X(BASIC_SINGLE, "Single", "Individual", "Один", "单个", "단일") \ X(BASIC_SIZE, "Size", "Tamaño", "Размер", "大小", "비율") \ X(BASIC_SOUND, "Sound", "Sonido", "Звук", "声音", "사운드") \ X(BASIC_TRIM, "Trim", "Recortar contenido", "Обрезать по содержимому", "修剪", "내용으로 자르기") \ @@ -121,6 +124,8 @@ namespace anm2ed X(EDIT_EXTEND_FRAME, "Extend Frame", "Extender Frame", "Удлиннить кадр", "延长帧时长", "프레임 확장") \ X(EDIT_FIT_ANIMATION_LENGTH, "Fit Animation Length", "Encajar Largo de animacion", "Подогнать к длине анимации", "匹配动画时长", "애니메이션 길이 맞추기") \ X(EDIT_FPS, "FPS", "FPS", "FPS", "每秒帧数(FPS)", "FPS") \ + X(EDIT_GROUP_ITEMS, "Group Item(s)", "Agrupar item(s)", "Сгруппировать элементы", "将项目分组", "항목 그룹화") \ + X(EDIT_TOGGLE_GROUP_EXPANDED, "Toggle Group Expanded", "Alternar grupo expandido", "Развернуть/свернуть группу", "展开/折叠组", "그룹 펼침 전환") \ X(EDIT_FRAME_COLOR_OFFSET, "Frame Color Offset", "Offset de color de Frame", "Смещение цвета кадра", "帧颜色偏移", "프레임 색상 오프셋") \ X(EDIT_FRAME_CROP, "Frame Crop", "Recorte de Frame", "Обрезка кадра", "帧裁剪", "프레임 자르기") \ X(EDIT_FRAME_DURATION, "Frame Duration", "Duracion de Frame", "Продолжительность кадра", "帧时长", "프레임 유지 시간") \ @@ -144,6 +149,7 @@ namespace anm2ed X(EDIT_PACK_SPRITESHEET, "Pack Spritesheet", "Empaquetar spritesheet", "Упаковать спрайт-лист", "打包图集", "스프라이트 시트 패킹") \ X(EDIT_MOVE_ANIMATIONS, "Move Animation(s)", "Mover Animacion(es)", "Переместить анимации", "移动动画", "애니메이션 이동") \ X(EDIT_MOVE_FRAMES, "Move Frame(s)", "Mover Frame(s)", "Перемесить кадры", "移动多个/单个帧", "프레임 이동") \ + X(EDIT_MOVE_ITEMS, "Move Item(s)", "Mover item(s)", "Переместить элементы", "移动项目", "항목 이동") \ X(EDIT_MOVE_REGIONS, "Move Regions", "Mover regiones", "Переместить регионы", "移动区域", "영역 이동") \ X(EDIT_MOVE_LAYER_ANIMATION, "Move Layer Animation", "Mover Animacion de Capa", "Переместить анимацию слоя", "移动动画层", "레이어 애니메이션 이동") \ X(EDIT_PASTE_ANIMATIONS, "Paste Animation(s)", "Pegar Animacion(es)", "Вставить анимации", "粘贴动画", "애니메이션 붙여넣기") \ @@ -164,6 +170,7 @@ namespace anm2ed X(EDIT_REMOVE_UNUSED_NULLS, "Remove Unused Nulls", "Remover Nulls No Utilizados", "Удалить неизпользуемые нули", "删除未使用的Null", "미사용 Null 제거") \ X(EDIT_REMOVE_UNUSED_SOUNDS, "Remove Unused Sounds", "Remover Sonidos No Utilizados", "Удалить неизпользуемые звуки", "删除未使用的声音", "미사용 사운드 제거") \ X(EDIT_REMOVE_UNUSED_SPRITESHEETS, "Remove Unused Spritesheets", "Remover Spritesheets No Utilizadas", "Удалить неизпользуемые спрайт-листы", "删除未使用的图集", "미사용 스프라이트 시트 제거") \ + X(EDIT_RENAME_GROUP, "Rename Group", "Renombrar grupo", "Переименовать группу", "重命名组", "그룹 이름 바꾸기") \ X(EDIT_RENAME_EVENT, "Rename Event", "Renombrar Evento", "Переименовать событие", "重命名事件", "이벤트 이름 바꾸기") \ X(EDIT_REPLACE_SPRITESHEET, "Replace Spritesheet", "Reemplazar Spritesheet", "Заменить спрайт-лист", "替换图集", "스프라이트 시트 교체") \ X(EDIT_REPLACE_SOUND, "Replace Sound", "Reemplazar Sonido", "Заменить звук", "替换声音", "사운드 교체") \ @@ -193,6 +200,7 @@ namespace anm2ed X(FORMAT_SPRITESHEET_ID, "Spritesheet ID: {0}", "ID de Spritesheet: {0}", "", "图集 ID: {0}", "스프라이트 시트 ID: {0}") \ X(FORMAT_INDEX, "Index: {0}", "Indice: {0}", "Индекс: {0}", "下标: {0}", "인덱스: {0}") \ X(FORMAT_INTERPOLATED, "Interpolation: {0}", "Interpolación: {0}", "Интерполяция: {0}", "插值: {0}", "보간: {0}") \ + X(FORMAT_ITEMS_COUNT, "Items: {0}", "Items: {0}", "Предметы: {0}", "物品: {0}", "항목: {0}") \ X(FORMAT_LAYER, "#{0} {1} (Spritesheet: #{2})", "#{0} {1} (Spritesheet: #{2})", "#{0} {1} (Спрайт-лист: #{2})", "#{0} {1} (图集: #{2})", "#{0} {1} (스프라이트 시트: #{2})") \ X(FORMAT_LENGTH, "Length: {0}", "Largo: {0}", "Длина: {0}", "长度: {0}", "길이: {0}") \ X(FORMAT_LOOP, "Loop: {0}", "Loop: {0}", "Цикл: {0}", "循环: {0}", "반복: {0}") \ @@ -302,10 +310,12 @@ namespace anm2ed X(LABEL_ANM2ED, "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed") \ X(LABEL_ANM2ED_LIMITED, "Anm2Ed Limited", "Anm2Ed Limitado", "Anm2Ed Ограниченный", "Anm2Ed 限制版", "Anm2Ed 제한") \ X(LABEL_DESTINATION, "Destination", "Destino", "Назначение", "目标", "대상") \ + X(LABEL_ITEMS, "Items", "Items", "Предметы", "物品", "항목") \ X(LABEL_LOCALIZATION, "Localization", "Localizacion", "Локализация", "本地化", "현지화") \ X(LABEL_MAKE_REGION, "Make Region", "Crear Región", "Создать регион", "创建区域", "영역 만들기") \ X(LABEL_LOOP, "Loop", "Loop", "Цикл", "循环", "반복") \ X(LABEL_MANAGER_ANM2_DRAG_DROP, "Anm2 Drag Drop", "Arrastrar y Soltar Anm2", "Anm2 Drag Drop", "Anm2 拖放", "Anm2 드래그 앤 드롭") \ + X(LABEL_GROUP_PROPERTIES, "Group Properties", "Propiedades de grupo", "Свойства группы", "组属性", "그룹 속성") \ X(LABEL_REGION_PROPERTIES, "Region Properties", "Propiedades de región", "Свойства региона", "区域属性", "영역 속성") \ X(LABEL_REGION_PROPERTIES_ORIGIN, "Origin", "Origen", "Точка отсчета", "原点", "원점") \ X(LABEL_REGION_ORIGIN_TOP_LEFT, "Top Left", "Superior izquierda", "Верхний левый", "左上", "왼쪽 위") \ @@ -390,7 +400,6 @@ namespace anm2ed X(LABEL_TRANSPARENT, "Transparent", "Transparentes", "Прозрачный", "透明", "투명도") \ X(LABEL_TYPE, "Type", "Tipo", "Тип", "类型", "유형") \ X(LABEL_UI_SCALE, "UI Scale", "Escala de la Interfaz", "Размер UI", "界面缩放", "UI 비율") \ - X(LABEL_ITEM_HEIGHT, "Item Height", "Altura del Item", "Высота элемента", "项目高度", "항목 높이") \ X(LABEL_USE_DEFAULT_SETTINGS, "Use Default Settings", "Usar Opciones Predeterminadas", "Изпользовать настройки по умолчанию", "使用默认设置", "기본값으로 사용") \ X(LABEL_VALUE_COLUMN, "Value", "Valor", "Значение", "数值", "값") \ X(LABEL_VSYNC, "Vsync", "Vsync", "Вертикальная синхронизация (V-sync)", "垂直同步", "수직 동기화") \ @@ -418,6 +427,7 @@ namespace anm2ed X(SHORTCUT_STRING_EXIT, "Exit", "Salir", "Выйти", "退出", "종료") \ X(SHORTCUT_STRING_EXTEND_FRAME, "Extend Frame", "Extender Frame", "Удлиннить кадр", "延长帧", "프레임 확장") \ X(SHORTCUT_STRING_FIT, "Fit", "Encajar", "Подогнать", "匹配", "맞추기") \ + X(SHORTCUT_STRING_GROUP, "Group", "Grupo", "Группа", "组", "그룹") \ X(SHORTCUT_STRING_INSERT_FRAME, "Insert Frame", "Insertar Frame", "Вставить кадр", "插入帧", "프레임 삽입") \ X(SHORTCUT_STRING_MERGE, "Merge", "Combinar", "Соединить", "合并", "병합") \ X(SHORTCUT_STRING_MOVE, "Move", "Mover", "Передвинуть", "移动", "이동") \ @@ -457,6 +467,7 @@ namespace anm2ed X(TEXT_TOOL_SPRITESHEET_EDITOR, "This tool can only be used in Spritesheet Editor!", "¡Esta herramienta solo se puede usar en el Editor de spritesheets!", "Этот инструмент можно использовать только в \"Редакторе спрайт-листов\"!", "该工具只能在“图集编辑器”中使用!", "이 도구는 스프라이트 시트 편집기에서만 사용할 수 있습니다!") \ X(TEXT_NEW_ANIMATION, "New Animation", "Nueva Animacion", "Новая анимация", "新动画", "새 애니메이션") \ X(TEXT_NEW_EVENT, "New Event", "Nuevo Evento", "Новое событие", "新事件", "새 이벤트") \ + X(TEXT_NEW_GROUP, "New Group", "Nuevo grupo", "Новая группа", "新组", "새 그룹") \ X(TEXT_NEW_REGION, "New Region", "Nueva Región", "Новый регион", "新区域", "새 영역") \ X(TEXT_REGION_IN_USE, "A spritesheet region is in use; remove region to edit.", "Se está usando una región del spritesheet; elimina la región para editar.", "Регион спрайт-листа используется; удалите регион для редактирования.", "图集中有区域正在使用;移除区域后才能编辑。", "스프라이트 시트 영역이 사용 중입니다. 편집하려면 영역을 제거하세요.") \ X(TEXT_RECORDING_PROGRESS, "Once recording is complete, rendering may take some time.\nPlease be patient...", "Una vez que el grabado este completo, renderizar puede tomar algo de tiempo. \nPor favor se paciente...", "Когда запись завершена, рендеринг может занять некоторое время.\nПожалуйста потерпите...", "录制完成时,渲染可能会花一些时间.\n请耐心等待...", "녹화가 완료되면 렌더링에 시간이 걸릴 수 있습니다.\n잠시만 기다려 주세요...") \ @@ -535,6 +546,7 @@ namespace anm2ed X(TOOLTIP_CANCEL_ADD_ITEM, "Cancel adding an item.", "Cancelar añadir un item.", "Отменить добавление предмета.", "取消添加物品.", "항목 추가를 취소합니다.") \ X(TOOLTIP_CANCEL_BAKE_FRAMES, "Cancel baking frames.", "Cancelar hacer bake de Frames.", "Отменить запечку кадров.", "取消提前渲染.", "프레임 베이킹을 취소합니다.") \ X(TOOLTIP_CHANGE_ALL_DESTINATION_FRAMES, "Apply specified frame properties to selected frames.", "Aplica las propiedades de frame especificadas a los frames seleccionados.", "Применить указанные свойства кадра к выбранным кадрам.", "将指定的帧属性应用到所选帧。", "지정한 프레임 속성을 선택한 프레임에 적용합니다.") \ + X(TOOLTIP_CHANGE_ALL_DESTINATION_ITEMS, "Apply specified frame properties to all frames in the selected items.", "Aplica las propiedades de frame especificadas a todos los frames de los items seleccionados.", "Применить указанные свойства кадра ко всем кадрам выбранных предметов.", "将指定的帧属性应用到所选物品中的所有帧。", "선택한 항목의 모든 프레임에 지정한 프레임 속성을 적용합니다.") \ X(TOOLTIP_CHANGE_ALL_DESTINATION_ANIMATIONS, "Apply specified frame properties to the specified items in the selected animations.", "Aplica las propiedades de frame especificadas a los items indicados en las animaciones seleccionadas.", "Применить указанные свойства кадра к указанным элементам в выбранных анимациях.", "将指定的帧属性应用到所选动画中的指定项目。", "지정한 프레임 속성을 선택한 애니메이션의 지정 항목에 적용합니다.") \ X(TOOLTIP_CHANGE_ALL_ROOT, "The frame property changes will apply to root frames.", "Los cambios de propiedades de frame se aplicaran a los frames root.", "Изменения свойств кадра будут применены к корневым кадрам.", "帧属性更改将应用于根帧。", "프레임 속성 변경 사항을 Root 프레임에 적용합니다.") \ X(TOOLTIP_CHANGE_ALL_LAYERS, "The frame property changes will apply to layer frames.", "Los cambios de propiedades de frame se aplicaran a los frames de capa.", "Изменения свойств кадра будут применены к кадрам слоев.", "帧属性更改将应用于图层帧。", "프레임 속성 변경 사항을 레이어 프레임에 적용합니다.") \ @@ -681,7 +693,6 @@ namespace anm2ed X(TOOLTIP_REMOVE_TRIGGER_SOUND, "Remove the last trigger sound.", "Remover el último sonido del trigger.", "Удалить последний звук триггера.", "移除最后一个事件触发器声音.", "마지막 트리거 사운드를 제거합니다.") \ X(TOOLTIP_TRIGGER_VISIBILITY, "Toggle the trigger's visibility.", "Alterna la visibilidad del trigger.", "Переключить видимость триггера.", "切换触发器是否可见.", "트리거를 표시하거나 숨깁니다.") \ X(TOOLTIP_UI_SCALE, "Change the scale of the UI.", "Cambia la escala de la interfaz de usuario.", "Изменить масштаб пользовательского интерфейса.", "更改界面(UI)的缩放.", "UI 비율을 변경합니다.") \ - X(TOOLTIP_ITEM_HEIGHT, "Set the height of items in Timeline.", "Establece la altura de los items en la Línea de tiempo.", "Установить высоту элементов на таймлайне.", "设置时间轴中项目的高度.", "타임라인 항목의 높이를 설정합니다.") \ X(TOOLTIP_UNUSED_ITEMS_HIDDEN, "Unused layers/nulls are hidden. Press to show them.", "Las capas/nulls no utilizados estan ocultos. Presiona para hacerlos visibles", "Неиспользуемые слои/нули скрыты. Нажмите, чтобы их показать.", "正在隐藏未使用的动画层/Null. 点击以显示它们.", "사용되지 않는 레이어/Null이 숨겨져 있습니다. 표시하려면 누르세요.") \ X(TOOLTIP_UNUSED_ITEMS_SHOWN, "Unused layers/nulls are shown. Press to hide them.", "Las capas/nulls no utilizados estan visibles. Presiona para ocultarlos", "Неиспользуемые слои/нули видимы. Нажмите, чтобы их скрыть.", "正在显示未使用的动画层/Null. 点击以隐藏它们.", "사용되지 않는 레이어/Null이 표시되어 있습니다. 숨기려면 누르세요.") \ X(TOOLTIP_USE_DEFAULT_SETTINGS, "Reset the settings to their defaults.", "Reinicia las configuraciones a sus predeterminados.", "Сбросить настройки на настройки по умолчанию.", "重设所有设置为默认.", "설정을 기본값으로 재설정합니다.") \ diff --git a/src/resource/texture.cpp b/src/resource/texture.cpp index 122bef3..a3bb247 100644 --- a/src/resource/texture.cpp +++ b/src/resource/texture.cpp @@ -23,8 +23,8 @@ #pragma GCC diagnostic pop #endif -#include "file_.hpp" -#include "math_.hpp" +#include "file.hpp" +#include "math.hpp" using namespace anm2ed::resource::texture; using namespace anm2ed::util::math; diff --git a/src/settings.cpp b/src/settings.cpp index e0689e8..f14303e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -4,7 +4,7 @@ #include #include "log.hpp" -#include "path_.hpp" +#include "path.hpp" using namespace anm2ed::util; using namespace glm; diff --git a/src/settings.hpp b/src/settings.hpp index cff3c2b..cdd7e62 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -5,7 +5,7 @@ #include -#include "anm2/anm2_type.hpp" +#include "anm2/anm2.hpp" #include "origin.hpp" #include "render.hpp" #include "strings.hpp" @@ -25,6 +25,10 @@ namespace anm2ed constexpr auto OUTPUT_PATH_DEFAULT = "./output.gif"; #endif + constexpr auto UI_SCALE_MIN = 0.5f; + constexpr auto UI_SCALE_MAX = 2.0f; + constexpr auto UI_SCALE_STEP = 0.25f; + #define SETTINGS_TYPES \ X(INT, int) \ X(BOOL, bool) \ @@ -55,16 +59,17 @@ namespace anm2ed X(WINDOW_POSITION, windowPosition, STRING_UNDEFINED, IVEC2, glm::ivec2()) \ X(IS_VSYNC, isVsync, STRING_UNDEFINED, BOOL, true) \ X(UI_SCALE, uiScale, STRING_UNDEFINED, FLOAT, 1.0f) \ - X(TIMELINE_ITEM_HEIGHT, timelineItemHeight, STRING_UNDEFINED, FLOAT, 1.0f) \ X(THEME, theme, STRING_UNDEFINED, INT, types::theme::DARK) \ X(LANGUAGE, language, STRING_UNDEFINED, INT, ENGLISH) \ \ X(FILE_IS_AUTOSAVE, fileIsAutosave, STRING_UNDEFINED, BOOL, true) \ X(FILE_IS_WARN_OVERWRITE, fileIsWarnOverwrite, STRING_UNDEFINED, BOOL, true) \ - X(FILE_IS_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE_REMINDER, fileIsSpecialInterpolatedFramesOnSaveReminder, STRING_UNDEFINED, BOOL, true) \ - X(FILE_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, fileBakeSpecialInterpolatedFramesOnSave, STRING_UNDEFINED, BOOL, true) \ + X(FILE_IS_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE_REMINDER, fileIsSpecialInterpolatedFramesOnSaveReminder, \ + STRING_UNDEFINED, BOOL, true) \ + X(FILE_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, fileBakeSpecialInterpolatedFramesOnSave, STRING_UNDEFINED, BOOL, \ + true) \ X(FILE_SNAPSHOT_STACK_SIZE, fileSnapshotStackSize, STRING_UNDEFINED, INT, 50) \ - X(FILE_COMPATIBILITY, fileCompatibility, STRING_UNDEFINED, INT, anm2::ANM2ED) \ + X(FILE_COMPATIBILITY, fileCompatibility, STRING_UNDEFINED, INT, ANM2ED) \ \ X(KEYBOARD_REPEAT_DELAY, keyboardRepeatDelay, STRING_UNDEFINED, FLOAT, 0.300f) \ X(KEYBOARD_REPEAT_RATE, keyboardRepeatRate, STRING_UNDEFINED, FLOAT, 0.050f) \ @@ -161,7 +166,7 @@ namespace anm2ed \ X(MERGE_TYPE, mergeType, STRING_UNDEFINED, INT, 0) \ X(MERGE_IS_DELETE_ANIMATIONS_AFTER, mergeIsDeleteAnimationsAfter, STRING_UNDEFINED, BOOL, false) \ - X(MERGE_SPRITESHEETS_ORIGIN, mergeSpritesheetsOrigin, STRING_UNDEFINED, INT, anm2::APPEND_RIGHT) \ + X(MERGE_SPRITESHEETS_ORIGIN, mergeSpritesheetsOrigin, STRING_UNDEFINED, INT, APPEND_RIGHT) \ X(MERGE_SPRITESHEETS_IS_MAKE_REGIONS, mergeSpritesheetsIsMakeRegions, STRING_UNDEFINED, BOOL, true) \ X(MERGE_SPRITESHEETS_IS_MAKE_PRIMARY_REGION, mergeSpritesheetsIsMakePrimaryRegion, STRING_UNDEFINED, BOOL, true) \ X(MERGE_SPRITESHEETS_REGION_ORIGIN, mergeSpritesheetsRegionOrigin, STRING_UNDEFINED, INT, origin::TOP_LEFT) \ @@ -171,7 +176,7 @@ namespace anm2ed X(BAKE_IS_ROUND_SCALE, bakeIsRoundScale, STRING_UNDEFINED, BOOL, true) \ X(BAKE_IS_ROUND_ROTATION, bakeIsRoundRotation, STRING_UNDEFINED, BOOL, true) \ \ - X(TIMELINE_ADD_ITEM_TYPE, timelineAddItemType, STRING_UNDEFINED, INT, anm2::LAYER) \ + X(TIMELINE_ADD_ITEM_TYPE, timelineAddItemType, STRING_UNDEFINED, INT, LAYER) \ X(TIMELINE_ADD_ITEM_DESTINATION, timelineAddItemDestination, STRING_UNDEFINED, INT, types::destination::ALL) \ X(TIMELINE_ADD_ITEM_SOURCE, timelineAddItemSource, STRING_UNDEFINED, INT, types::source::NEW) \ X(TIMELINE_IS_SHOW_UNUSED, timelineIsShowUnused, STRING_UNDEFINED, BOOL, true) \ @@ -218,6 +223,7 @@ namespace anm2ed X(SHORTCUT_RENAME, shortcutRename, SHORTCUT_STRING_RENAME, STRING, "F2") \ X(SHORTCUT_DEFAULT, shortcutDefault, SHORTCUT_STRING_DEFAULT, STRING, "Home") \ X(SHORTCUT_MERGE, shortcutMerge, SHORTCUT_STRING_MERGE, STRING, "Ctrl+E") \ + X(SHORTCUT_GROUP, shortcutGroup, SHORTCUT_STRING_GROUP, STRING, "Ctrl+G") \ X(SHORTCUT_CONFIRM, shortcutConfirm, SHORTCUT_STRING_CONFIRM, STRING, "Enter") \ X(SHORTCUT_CANCEL, shortcutCancel, SHORTCUT_STRING_CANCEL, STRING, "Escape") \ /* Tools */ \ diff --git a/src/snapshots.hpp b/src/snapshots.hpp index 87f5380..a1776cd 100644 --- a/src/snapshots.hpp +++ b/src/snapshots.hpp @@ -1,11 +1,14 @@ #pragma once #include +#include #include #include "anm2/anm2.hpp" +#include "audio.hpp" #include "playback.hpp" #include "storage.hpp" +#include "texture.hpp" namespace anm2ed::snapshots { @@ -29,8 +32,10 @@ namespace anm2ed Storage region{}; Storage sound{}; Storage spritesheet{}; - anm2::Anm2 anm2{}; - anm2::Reference reference{}; + std::map textures{}; + std::map sounds{}; + Anm2 anm2{}; + Reference reference{}; float frameTime{}; std::string message = snapshots::ACTION; }; diff --git a/src/state.cpp b/src/state.cpp index 0fd5354..4dd269c 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -8,9 +8,10 @@ #include #include "log.hpp" -#include "path_.hpp" +#include "path.hpp" #include "strings.hpp" #include "toast.hpp" +#include "util/imgui/shortcut.hpp" using namespace anm2ed::imgui; using namespace anm2ed::util; @@ -66,6 +67,10 @@ namespace anm2ed ImGui_ImplSDL3_ProcessEvent(&event); switch (event.type) { + case SDL_EVENT_DROP_BEGIN: + spritesheetDropPaths.clear(); + soundDropPaths.clear(); + break; case SDL_EVENT_DROP_FILE: { std::string droppedFile = event.drop.data ? event.drop.data : ""; @@ -86,24 +91,46 @@ namespace anm2ed } else if (path::is_extension(droppedPath, "png")) { - if (auto document = manager.get()) - document->spritesheet_add(droppedPath); - else + spritesheetDropPaths.push_back(droppedPath); + } + else if (path::is_extension(droppedPath, "wav") || path::is_extension(droppedPath, "ogg")) + { + soundDropPaths.push_back(droppedPath); + } + break; + } + case SDL_EVENT_DROP_COMPLETE: + { + if (auto document = manager.get(); document) + { + if (!spritesheetDropPaths.empty()) + { + auto paths = spritesheetDropPaths; + manager.command_push({manager.selected, [paths](Manager&, Document& document) + { document.spritesheets_add(paths); }}); + } + if (!soundDropPaths.empty()) + { + auto paths = soundDropPaths; + manager.command_push({manager.selected, [paths](Manager&, Document& document) + { document.sounds_add(paths); }}); + } + } + else + { + if (!spritesheetDropPaths.empty()) { toasts.push(localize.get(TOAST_ADD_SPRITESHEET_FAILED)); logger.warning(localize.get(TOAST_ADD_SPRITESHEET_FAILED, anm2ed::ENGLISH)); } - } - else if (path::is_extension(droppedPath, "wav") || path::is_extension(droppedPath, "ogg")) - { - if (auto document = manager.get()) - document->sound_add(droppedPath); - else + if (!soundDropPaths.empty()) { toasts.push(localize.get(TOAST_ADD_SOUND_FAILED)); logger.warning(localize.get(TOAST_ADD_SOUND_FAILED, anm2ed::ENGLISH)); } } + spritesheetDropPaths.clear(); + soundDropPaths.clear(); break; } case SDL_EVENT_USER: // Opening files @@ -131,10 +158,40 @@ namespace anm2ed ImGui_ImplOpenGL3_NewFrame(); ImGui::NewFrame(); + if (ImGui::GetTopMostPopupModal() == nullptr) + { + constexpr ImGuiKey DOCUMENT_SHORTCUT_KEYS[] = {ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5, + ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9, ImGuiKey_0}; + for (int i = 0; i < (int)(sizeof(DOCUMENT_SHORTCUT_KEYS) / sizeof(DOCUMENT_SHORTCUT_KEYS[0])); ++i) + { + if (i >= (int)manager.documents.size()) break; + if (ImGui::Shortcut(ImGuiMod_Ctrl | DOCUMENT_SHORTCUT_KEYS[i], ImGuiInputFlags_RouteGlobal)) + { + manager.set(i); + manager.pendingSelected = i; + break; + } + } + } + taskbar.update(manager, settings, resources, dialog, isQuitting); documents.update(taskbar, manager, settings, resources, isQuitting); dockspace.update(taskbar, documents, manager, settings, resources, dialog, clipboard); + + if (!dockspace.is_canvas_focused_get()) + { + float uiScaleDelta{}; + if (imgui::shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL)) uiScaleDelta += UI_SCALE_STEP; + if (imgui::shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL)) uiScaleDelta -= UI_SCALE_STEP; + if (uiScaleDelta != 0.0f) + { + settings.uiScale = std::clamp(settings.uiScale + uiScaleDelta, UI_SCALE_MIN, UI_SCALE_MAX); + ImGui::GetStyle().FontScaleMain = settings.uiScale; + } + } + toasts.update(); + manager.commands_run(); SDL_GetWindowSize(window, &settings.windowSize.x, &settings.windowSize.y); SDL_GetWindowPosition(window, &settings.windowPosition.x, &settings.windowPosition.y); diff --git a/src/state.hpp b/src/state.hpp index 808e209..7d3473b 100644 --- a/src/state.hpp +++ b/src/state.hpp @@ -2,6 +2,9 @@ #include +#include +#include + #include "dockspace.hpp" namespace anm2ed @@ -28,6 +31,8 @@ namespace anm2ed uint64_t previousUpdate{}; double tickAccumulatorMs{}; bool wasRecording{}; + std::vector spritesheetDropPaths{}; + std::vector soundDropPaths{}; State(SDL_Window*&, Settings& settings, std::vector&); diff --git a/src/storage.hpp b/src/storage.hpp index f8382f7..5cd4987 100644 --- a/src/storage.hpp +++ b/src/storage.hpp @@ -1,6 +1,9 @@ #pragma once -#include "imgui_.hpp" +#include + +#include "anm2/anm2.hpp" +#include "util/imgui/multiselect.hpp" namespace anm2ed { @@ -13,6 +16,7 @@ namespace anm2ed std::vector labels{}; std::vector ids{}; imgui::MultiSelectStorage selection{}; + std::set references{}; void clear(); void labels_set(std::vector); diff --git a/src/util/file_.cpp b/src/util/file.cpp similarity index 98% rename from src/util/file_.cpp rename to src/util/file.cpp index a7bc886..0071b3a 100644 --- a/src/util/file_.cpp +++ b/src/util/file.cpp @@ -1,4 +1,4 @@ -#include "file_.hpp" +#include "file.hpp" #include #include diff --git a/src/util/file_.hpp b/src/util/file.hpp similarity index 100% rename from src/util/file_.hpp rename to src/util/file.hpp diff --git a/src/util/imgui/constants.hpp b/src/util/imgui/constants.hpp new file mode 100644 index 0000000..5c2691d --- /dev/null +++ b/src/util/imgui/constants.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace anm2ed::imgui +{ + constexpr auto DRAG_SPEED = 0.25f; + constexpr auto DRAG_SPEED_FAST = 1.00f; + constexpr auto STEP = 1.0f; + constexpr auto STEP_FAST = 5.0f; +} diff --git a/src/util/imgui/draw.cpp b/src/util/imgui/draw.cpp new file mode 100644 index 0000000..d48f165 --- /dev/null +++ b/src/util/imgui/draw.cpp @@ -0,0 +1,71 @@ +#include "draw.hpp" + +#include + +namespace anm2ed::imgui +{ + constexpr ImU32 DROP_LINE_COLOR = IM_COL32(255, 255, 0, 255); + constexpr float DROP_LINE_THICKNESS = 2.0f; + + struct CheckerStart + { + float position{}; + long long index{}; + }; + + CheckerStart checker_start(float minCoord, float offset, float step) + { + float world = minCoord + offset; + long long index = static_cast(std::floor(world / step)); + float first = minCoord - (world - static_cast(index) * step); + return {first, index}; + } + + bool is_drop_after(ImVec2 min, ImVec2 max) + { + return ImGui::GetIO().MousePos.y >= (min.y + max.y) * 0.5f; + } + + void drop_box_draw(ImDrawList* drawList, ImVec2 min, ImVec2 max) + { + if (!drawList) return; + auto offset = DROP_LINE_THICKNESS * 0.5f; + drawList->AddRect(ImVec2(min.x + offset, min.y + offset), ImVec2(max.x - offset, max.y - offset), + DROP_LINE_COLOR, 0.0f, 0, DROP_LINE_THICKNESS); + } + + void drop_line_draw(ImDrawList* drawList, ImVec2 min, ImVec2 max, bool isAfter) + { + if (!drawList) return; + auto offset = DROP_LINE_THICKNESS * 0.5f; + auto y = std::floor(isAfter ? max.y - offset : min.y + offset) + 0.5f; + drawList->AddLine(ImVec2(min.x, y), ImVec2(max.x, y), DROP_LINE_COLOR, DROP_LINE_THICKNESS); + } + + void render_checker_background(ImDrawList* drawList, ImVec2 min, ImVec2 max, glm::vec2 offset, float step) + { + if (!drawList || step <= 0.0f) return; + + const ImU32 colorLight = IM_COL32(204, 204, 204, 255); + const ImU32 colorDark = IM_COL32(128, 128, 128, 255); + + auto [startY, rowIndex] = checker_start(min.y, offset.y, step); + for (float y = startY; y < max.y; y += step, ++rowIndex) + { + float y1 = glm::max(y, min.y); + float y2 = glm::min(y + step, max.y); + if (y2 <= y1) continue; + + auto [startX, columnIndex] = checker_start(min.x, offset.x, step); + for (float x = startX; x < max.x; x += step, ++columnIndex) + { + float x1 = glm::max(x, min.x); + float x2 = glm::min(x + step, max.x); + if (x2 <= x1) continue; + + bool isDark = ((rowIndex + columnIndex) & 1LL) != 0; + drawList->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), isDark ? colorDark : colorLight); + } + } + } +} diff --git a/src/util/imgui/draw.hpp b/src/util/imgui/draw.hpp new file mode 100644 index 0000000..63ffe23 --- /dev/null +++ b/src/util/imgui/draw.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace anm2ed::imgui +{ + bool is_drop_after(ImVec2, ImVec2); + void drop_box_draw(ImDrawList*, ImVec2, ImVec2); + void drop_line_draw(ImDrawList*, ImVec2, ImVec2, bool); + void render_checker_background(ImDrawList*, ImVec2, ImVec2, glm::vec2, float); +} diff --git a/src/util/imgui/imgui.hpp b/src/util/imgui/imgui.hpp new file mode 100644 index 0000000..9916e8d --- /dev/null +++ b/src/util/imgui/imgui.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "constants.hpp" +#include "draw.hpp" +#include "input.hpp" +#include "layout.hpp" +#include "multiselect.hpp" +#include "popup.hpp" +#include "selectable.hpp" +#include "shortcut.hpp" +#include "theme.hpp" +#include "tooltip.hpp" diff --git a/src/util/imgui/input.cpp b/src/util/imgui/input.cpp new file mode 100644 index 0000000..2268a2a --- /dev/null +++ b/src/util/imgui/input.cpp @@ -0,0 +1,252 @@ +#include "input.hpp" + +#include +#include +#include + +#include + +#include "path.hpp" + +using namespace anm2ed::types; +using namespace anm2ed::util; +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 input_text_path(const char* label, std::filesystem::path* path, ImGuiInputTextFlags flags) + { + if (!path) return false; + + auto pathUtf8 = path::to_utf8(*path); + auto edited = input_text_string(label, &pathUtf8, flags); + if (edited) *path = path::from_utf8(pathUtf8); + + return edited; + } + + bool combo_negative_one_indexed(const std::string& label, int* index, std::vector& strings) + { + *index += 1; + bool isActivated = ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size()); + *index -= 1; + + return isActivated; + } + + bool combo_id_mapped(const std::string& label, int* id, const std::vector& ids, std::vector& labels) + { + if (!id) return false; + + int index = -1; + if (!ids.empty()) + { + auto it = std::find(ids.begin(), ids.end(), *id); + if (it != ids.end()) index = (int)std::distance(ids.begin(), it); + } + + bool isActivated = ImGui::Combo(label.c_str(), &index, labels.data(), (int)labels.size()); + if (isActivated) + { + if (index >= 0 && index < (int)ids.size()) + *id = ids[index]; + else + *id = -1; + } + + return isActivated; + } + + edit::Type drag_int_persistent(const char* label, int* value, float speed, int min, int max, const char* format, + ImGuiSliderFlags flags) + { + static bool isEditing{}; + static int start{INT_MAX}; + auto persistent = value ? *value : 0; + + ImGui::DragInt(label, &persistent, speed, min, max, format, flags); + if (!value) return edit::NONE; + if (ImGui::IsItemActivated() && persistent != start) + { + isEditing = true; + start = *value; + return edit::START; + } + else if (ImGui::IsItemDeactivatedAfterEdit()) + { + isEditing = false; + *value = persistent; + start = INT_MAX; + return edit::END; + } + else if (isEditing) + { + *value = persistent; + return edit::DURING; + } + + return edit::NONE; + } + + edit::Type drag_float_persistent(const char* label, float* value, float speed, float min, float max, + const char* format, ImGuiSliderFlags flags) + { + static bool isEditing{}; + static float start{NAN}; + auto persistent = value ? *value : 0; + + ImGui::DragFloat(label, &persistent, speed, min, max, format, flags); + if (!value) return edit::NONE; + if (ImGui::IsItemActivated() && persistent != start) + { + isEditing = true; + start = *value; + return edit::START; + } + else if (ImGui::IsItemDeactivatedAfterEdit()) + { + isEditing = false; + *value = persistent; + start = NAN; + return edit::END; + } + else if (isEditing) + { + *value = persistent; + return edit::DURING; + } + + return edit::NONE; + } + + edit::Type drag_float2_persistent(const char* label, vec2* value, float speed, float min, float max, + const char* format, ImGuiSliderFlags flags) + { + static bool isEditing{}; + static vec2 start{NAN}; + auto persistent = value ? *value : vec2(); + + ImGui::DragFloat2(label, value_ptr(persistent), speed, min, max, format, flags); + if (!value) return edit::NONE; + if (ImGui::IsItemActivated() && persistent != start) + { + isEditing = true; + start = *value; + return edit::START; + } + else if (ImGui::IsItemDeactivatedAfterEdit()) + { + isEditing = false; + *value = persistent; + start = vec2{NAN}; + return edit::END; + } + else if (isEditing) + { + *value = persistent; + return edit::DURING; + } + + return edit::NONE; + } + + edit::Type color_edit3_persistent(const char* label, vec3* value, ImGuiColorEditFlags flags) + { + static bool isEditing{}; + static vec3 start{NAN}; + auto persistent = value ? *value : vec4(); + + ImGui::ColorEdit3(label, value_ptr(persistent), flags); + if (!value) return edit::NONE; + if (ImGui::IsItemActivated() && persistent != start) + { + isEditing = true; + start = *value; + return edit::START; + } + else if (ImGui::IsItemDeactivatedAfterEdit()) + { + isEditing = false; + *value = persistent; + start = vec4{NAN}; + return edit::END; + } + else if (isEditing) + { + *value = persistent; + return edit::DURING; + } + + return edit::NONE; + } + + edit::Type color_edit4_persistent(const char* label, vec4* value, ImGuiColorEditFlags flags) + { + static bool isEditing{}; + static vec4 start{NAN}; + auto persistent = value ? *value : vec4(); + + ImGui::ColorEdit4(label, value_ptr(persistent), flags); + if (!value) return edit::NONE; + if (ImGui::IsItemActivated() && persistent != start) + { + isEditing = true; + start = *value; + return edit::START; + } + else if (ImGui::IsItemDeactivatedAfterEdit()) + { + isEditing = false; + *value = persistent; + start = vec4{NAN}; + return edit::END; + } + else if (isEditing) + { + *value = persistent; + return edit::DURING; + } + + return edit::NONE; + } + + 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 input_int2_range(const char* label, ivec2& value, ivec2 min, ivec2 max, ImGuiInputTextFlags flags) + { + auto isActivated = ImGui::InputInt2(label, value_ptr(value), flags); + value = glm::clamp(value, min, max); + return isActivated; + } + + bool input_float_range(const char* label, float& value, float min, float max, float step, float stepFast, + const char* format, ImGuiInputTextFlags flags) + { + auto isActivated = ImGui::InputFloat(label, &value, step, stepFast, format, flags); + value = glm::clamp(value, min, max); + return isActivated; + } +} diff --git a/src/util/imgui/input.hpp b/src/util/imgui/input.hpp new file mode 100644 index 0000000..cebfecf --- /dev/null +++ b/src/util/imgui/input.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "constants.hpp" +#include "types.hpp" + +namespace anm2ed::imgui +{ + int input_text_callback(ImGuiInputTextCallbackData*); + bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0); + bool input_text_path(const char*, std::filesystem::path*, ImGuiInputTextFlags = 0); + bool input_int_range(const char*, int&, int, int, int = STEP, int = STEP_FAST, ImGuiInputTextFlags = 0); + bool input_int2_range(const char*, glm::ivec2&, glm::ivec2, glm::ivec2, ImGuiInputTextFlags = 0); + bool input_float_range(const char*, float&, float, float, float = STEP, float = STEP_FAST, const char* = "%.3f", + ImGuiInputTextFlags = 0); + types::edit::Type drag_int_persistent(const char*, int*, float = DRAG_SPEED, int = {}, int = {}, const char* = "%d", + ImGuiSliderFlags = 0); + types::edit::Type drag_float_persistent(const char*, float*, float = DRAG_SPEED, float = {}, float = {}, + const char* = "%.3f", ImGuiSliderFlags = 0); + types::edit::Type drag_float2_persistent(const char*, glm::vec2*, float = DRAG_SPEED, float = {}, float = {}, + const char* = "%.3f", ImGuiSliderFlags = 0); + types::edit::Type color_edit3_persistent(const char*, glm::vec3*, ImGuiColorEditFlags = 0); + types::edit::Type color_edit4_persistent(const char*, glm::vec4*, ImGuiColorEditFlags = 0); + bool combo_negative_one_indexed(const std::string&, int*, std::vector&); + bool combo_id_mapped(const std::string&, int*, const std::vector&, std::vector&); +} diff --git a/src/util/imgui/layout.cpp b/src/util/imgui/layout.cpp new file mode 100644 index 0000000..9c613db --- /dev/null +++ b/src/util/imgui/layout.cpp @@ -0,0 +1,38 @@ +#include "layout.hpp" + +namespace anm2ed::imgui +{ + float row_widget_width_get(int count, float width) + { + return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count; + } + + ImVec2 widget_size_with_row_get(int count, float width) { return ImVec2(row_widget_width_get(count, width), 0); } + + float footer_height_get(int itemCount) + { + return ImGui::GetTextLineHeightWithSpacing() * itemCount + ImGui::GetStyle().WindowPadding.y + + ImGui::GetStyle().ItemSpacing.y * (itemCount); + } + + ImVec2 footer_size_get(int itemCount) + { + return ImVec2(ImGui::GetContentRegionAvail().x, footer_height_get(itemCount)); + } + + ImVec2 size_without_footer_get(int rowCount) + { + return ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - footer_height_get(rowCount)); + } + + ImVec2 child_size_get(int rowCount) + { + return ImVec2(ImGui::GetContentRegionAvail().x, + (ImGui::GetFrameHeightWithSpacing() * rowCount) + (ImGui::GetStyle().WindowPadding.y * 2.0f)); + } + + ImVec2 icon_size_get() + { + return ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing()); + } +} diff --git a/src/util/imgui/layout.hpp b/src/util/imgui/layout.hpp new file mode 100644 index 0000000..0bc530f --- /dev/null +++ b/src/util/imgui/layout.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace anm2ed::imgui +{ + float row_widget_width_get(int, float = ImGui::GetContentRegionAvail().x); + ImVec2 widget_size_with_row_get(int, float = ImGui::GetContentRegionAvail().x); + float footer_height_get(int = 1); + ImVec2 footer_size_get(int = 1); + ImVec2 size_without_footer_get(int = 1); + ImVec2 child_size_get(int = 1); + ImVec2 icon_size_get(); +} diff --git a/src/util/imgui/multiselect.cpp b/src/util/imgui/multiselect.cpp new file mode 100644 index 0000000..b864e7c --- /dev/null +++ b/src/util/imgui/multiselect.cpp @@ -0,0 +1,41 @@ +#include "multiselect.hpp" + +namespace anm2ed::imgui +{ + void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected) + { + auto* storage = static_cast(self->UserData); + auto value = storage ? storage->resolve_index(id) : id; + if (isSelected) + storage->insert(value); + else + storage->erase(value); + }; + + MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; } + + void MultiSelectStorage::start(size_t size, ImGuiMultiSelectFlags flags) + { + internal.UserData = this; + + io = ImGui::BeginMultiSelect(flags, this->size(), size); + apply(); + } + + void MultiSelectStorage::apply() { internal.ApplyRequests(io); } + + void MultiSelectStorage::finish() + { + io = ImGui::EndMultiSelect(); + apply(); + } + + void MultiSelectStorage::set_index_map(std::vector* map) { indexMap = map; } + + int MultiSelectStorage::resolve_index(int index) const + { + if (!indexMap) return index; + if (index < 0 || index >= (int)indexMap->size()) return index; + return (*indexMap)[index]; + } +} diff --git a/src/util/imgui/multiselect.hpp b/src/util/imgui/multiselect.hpp new file mode 100644 index 0000000..04b2dce --- /dev/null +++ b/src/util/imgui/multiselect.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include + +namespace anm2ed::imgui +{ + class MultiSelectStorage : public std::set + { + public: + ImGuiSelectionExternalStorage internal{}; + ImGuiMultiSelectIO* io{}; + std::vector* indexMap{}; + + using std::set::set; + using std::set::operator=; + using std::set::begin; + using std::set::rbegin; + using std::set::end; + using std::set::size; + using std::set::insert; + using std::set::erase; + + MultiSelectStorage(); + void start(size_t, + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ScopeWindow); + void apply(); + void finish(); + void set_index_map(std::vector*); + int resolve_index(int) const; + }; + + void external_storage_set(ImGuiSelectionExternalStorage*, int, bool); +} diff --git a/src/util/imgui/popup.cpp b/src/util/imgui/popup.cpp new file mode 100644 index 0000000..1460ce9 --- /dev/null +++ b/src/util/imgui/popup.cpp @@ -0,0 +1,74 @@ +#include "popup.hpp" + +#include +#include + +#include "types.hpp" + +namespace anm2ed::imgui +{ +#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) + + 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 + }; + +#undef POPUP_LIST + + PopupHelper::PopupHelper(StringType labelId, PopupType type, PopupPosition position) + { + this->labelId = labelId; + this->type = type; + this->position = position; + } + + void PopupHelper::open() + { + isOpen = true; + isTriggered = true; + isJustOpened = true; + } + + bool PopupHelper::is_open() { return isOpen; } + + void PopupHelper::trigger() + { + if (isTriggered) ImGui::OpenPopup(localize.get(labelId)); + isTriggered = false; + + auto viewport = ImGui::GetMainViewport(); + + switch (position) + { + case POPUP_CENTER: + ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, types::to_imvec2(glm::vec2(0.5f))); + if (POPUP_IS_HEIGHT_SET[type]) + ImGui::SetNextWindowSize(types::to_imvec2(types::to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type])); + else + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0)); + break; + case POPUP_BY_ITEM: + ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None); + case POPUP_BY_CURSOR: + default: + break; + } + } + + void PopupHelper::end() { isJustOpened = false; } + + void PopupHelper::close() { isOpen = false; } +} diff --git a/src/util/imgui/popup.hpp b/src/util/imgui/popup.hpp new file mode 100644 index 0000000..feac831 --- /dev/null +++ b/src/util/imgui/popup.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "strings.hpp" + +namespace anm2ed::imgui +{ +#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, + POPUP_BY_CURSOR + }; + + class PopupHelper + { + public: + StringType labelId{}; + PopupType type{}; + PopupPosition position{}; + bool isOpen{}; + bool isTriggered{}; + bool isJustOpened{}; + + PopupHelper(StringType, PopupType = POPUP_NORMAL, PopupPosition = POPUP_CENTER); + const char* label() const { return localize.get(labelId); } + bool is_open(); + void open(); + void trigger(); + void end(); + void close(); + }; + +#undef POPUP_LIST +} diff --git a/src/util/imgui/selectable.cpp b/src/util/imgui/selectable.cpp new file mode 100644 index 0000000..114e5ec --- /dev/null +++ b/src/util/imgui/selectable.cpp @@ -0,0 +1,60 @@ +#include "selectable.hpp" + +#include + +#include "input.hpp" + +namespace anm2ed::imgui +{ + bool isSelectableInputTextRenaming = false; + + std::string& selectable_input_text_id() + { + static std::string editID{}; + return editID; + } + + bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected, + ImGuiSelectableFlags flags, RenameState& state) + { + auto& editID = selectable_input_text_id(); + auto isRename = editID == id; + bool isActivated{}; + + if (isRename) + { + auto finish = [&]() + { + editID.clear(); + isActivated = true; + state = RENAME_FINISHED; + isSelectableInputTextRenaming = false; + }; + + if (state == RENAME_BEGIN) + { + ImGui::SetKeyboardFocusHere(); + state = RENAME_EDITING; + } + + ImGui::SetNextItemWidth(-FLT_MIN); + if (input_text_string("##Edit", &text, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) + finish(); + if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) finish(); + } + else + { + if (ImGui::Selectable(label.c_str(), isSelected, flags)) isActivated = true; + + if (state == RENAME_FORCE_EDIT || (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))) + { + state = RENAME_BEGIN; + editID = id; + isActivated = true; + isSelectableInputTextRenaming = true; + } + } + + return isActivated; + } +} diff --git a/src/util/imgui/selectable.hpp b/src/util/imgui/selectable.hpp new file mode 100644 index 0000000..82d4dcc --- /dev/null +++ b/src/util/imgui/selectable.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include + +namespace anm2ed::imgui +{ + enum RenameState + { + RENAME_SELECTABLE, + RENAME_BEGIN, + RENAME_EDITING, + RENAME_FINISHED, + RENAME_FORCE_EDIT + }; + + std::string& selectable_input_text_id(); + bool selectable_input_text(const std::string&, const std::string&, std::string&, bool, ImGuiSelectableFlags, + RenameState&); +} diff --git a/src/util/imgui/shortcut.cpp b/src/util/imgui/shortcut.cpp new file mode 100644 index 0000000..da104a8 --- /dev/null +++ b/src/util/imgui/shortcut.cpp @@ -0,0 +1,179 @@ +#include "shortcut.hpp" + +#include +#include +#include + +#include + +namespace anm2ed::imgui +{ + const std::vector> CANONICAL_KEY_NAMES = { + {ImGuiKey_A, "A"}, + {ImGuiKey_B, "B"}, + {ImGuiKey_C, "C"}, + {ImGuiKey_D, "D"}, + {ImGuiKey_E, "E"}, + {ImGuiKey_F, "F"}, + {ImGuiKey_G, "G"}, + {ImGuiKey_H, "H"}, + {ImGuiKey_I, "I"}, + {ImGuiKey_J, "J"}, + {ImGuiKey_K, "K"}, + {ImGuiKey_L, "L"}, + {ImGuiKey_M, "M"}, + {ImGuiKey_N, "N"}, + {ImGuiKey_O, "O"}, + {ImGuiKey_P, "P"}, + {ImGuiKey_Q, "Q"}, + {ImGuiKey_R, "R"}, + {ImGuiKey_S, "S"}, + {ImGuiKey_T, "T"}, + {ImGuiKey_U, "U"}, + {ImGuiKey_V, "V"}, + {ImGuiKey_W, "W"}, + {ImGuiKey_X, "X"}, + {ImGuiKey_Y, "Y"}, + {ImGuiKey_Z, "Z"}, + {ImGuiKey_0, "0"}, + {ImGuiKey_1, "1"}, + {ImGuiKey_2, "2"}, + {ImGuiKey_3, "3"}, + {ImGuiKey_4, "4"}, + {ImGuiKey_5, "5"}, + {ImGuiKey_6, "6"}, + {ImGuiKey_7, "7"}, + {ImGuiKey_8, "8"}, + {ImGuiKey_9, "9"}, + {ImGuiKey_Keypad0, "Num0"}, + {ImGuiKey_Keypad1, "Num1"}, + {ImGuiKey_Keypad2, "Num2"}, + {ImGuiKey_Keypad3, "Num3"}, + {ImGuiKey_Keypad4, "Num4"}, + {ImGuiKey_Keypad5, "Num5"}, + {ImGuiKey_Keypad6, "Num6"}, + {ImGuiKey_Keypad7, "Num7"}, + {ImGuiKey_Keypad8, "Num8"}, + {ImGuiKey_Keypad9, "Num9"}, + {ImGuiKey_KeypadAdd, "NumAdd"}, + {ImGuiKey_KeypadSubtract, "NumSubtract"}, + {ImGuiKey_KeypadMultiply, "NumMultiply"}, + {ImGuiKey_KeypadDivide, "NumDivide"}, + {ImGuiKey_KeypadEnter, "NumEnter"}, + {ImGuiKey_KeypadDecimal, "NumDecimal"}, + {ImGuiKey_KeypadEqual, "NumEqual"}, + {ImGuiKey_F1, "F1"}, + {ImGuiKey_F2, "F2"}, + {ImGuiKey_F3, "F3"}, + {ImGuiKey_F4, "F4"}, + {ImGuiKey_F5, "F5"}, + {ImGuiKey_F6, "F6"}, + {ImGuiKey_F7, "F7"}, + {ImGuiKey_F8, "F8"}, + {ImGuiKey_F9, "F9"}, + {ImGuiKey_F10, "F10"}, + {ImGuiKey_F11, "F11"}, + {ImGuiKey_F12, "F12"}, + {ImGuiKey_UpArrow, "Up"}, + {ImGuiKey_DownArrow, "Down"}, + {ImGuiKey_LeftArrow, "Left"}, + {ImGuiKey_RightArrow, "Right"}, + {ImGuiKey_Space, "Space"}, + {ImGuiKey_Enter, "Enter"}, + {ImGuiKey_Escape, "Escape"}, + {ImGuiKey_Tab, "Tab"}, + {ImGuiKey_Backspace, "Backspace"}, + {ImGuiKey_Delete, "Delete"}, + {ImGuiKey_Insert, "Insert"}, + {ImGuiKey_Home, "Home"}, + {ImGuiKey_End, "End"}, + {ImGuiKey_PageUp, "PageUp"}, + {ImGuiKey_PageDown, "PageDown"}, + {ImGuiKey_Minus, "Minus"}, + {ImGuiKey_Equal, "Equal"}, + {ImGuiKey_LeftBracket, "LeftBracket"}, + {ImGuiKey_RightBracket, "RightBracket"}, + {ImGuiKey_Semicolon, "Semicolon"}, + {ImGuiKey_Apostrophe, "Apostrophe"}, + {ImGuiKey_Comma, "Comma"}, + {ImGuiKey_Period, "Period"}, + {ImGuiKey_Slash, "Slash"}, + {ImGuiKey_Backslash, "Backslash"}, + {ImGuiKey_GraveAccent, "GraveAccent"}, + }; + + const char* shortcut_canonical_key_name(ImGuiKey key) + { + for (const auto& [mappedKey, name] : CANONICAL_KEY_NAMES) + if (mappedKey == key) return name; + return nullptr; + } + + std::string chord_to_string(ImGuiKeyChord chord) + { + std::string result; + + if (chord & ImGuiMod_Ctrl) result += "Ctrl+"; + if (chord & ImGuiMod_Shift) result += "Shift+"; + if (chord & ImGuiMod_Alt) result += "Alt+"; + if (chord & ImGuiMod_Super) result += "Super+"; + + if (auto key = (ImGuiKey)(chord & ~ImGuiMod_Mask_); key != ImGuiKey_None) + { + if (const char* name = shortcut_canonical_key_name(key); name && *name) + result += name; + else + result += ImGui::GetKeyName(key); + } + + if (!result.empty() && result.back() == '+') result.pop_back(); + + return result; + } + + ImGuiKeyChord string_to_chord(const std::string& string) + { + ImGuiKeyChord chord = 0; + ImGuiKey baseKey = ImGuiKey_None; + + std::stringstream ss(string); + std::string token; + while (std::getline(ss, token, '+')) + { + token.erase(0, token.find_first_not_of(" \t\r\n")); + token.erase(token.find_last_not_of(" \t\r\n") + 1); + + if (token.empty()) continue; + + if (auto it = MOD_MAP.find(token); it != MOD_MAP.end()) + chord |= it->second; + else if (baseKey == ImGuiKey_None) + if (auto it2 = KEY_MAP.find(token); it2 != KEY_MAP.end()) baseKey = it2->second; + } + + if (baseKey != ImGuiKey_None) chord |= baseKey; + + return chord; + } + + bool shortcut(ImGuiKeyChord chord, types::shortcut::Type type) + { + if (chord == ImGuiKey_None) return false; + + if (ImGui::GetTopMostPopupModal() != nullptr && + (type == types::shortcut::GLOBAL || type == types::shortcut::GLOBAL_SET)) + return false; + + int flags = type == types::shortcut::GLOBAL || type == types::shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal + : ImGuiInputFlags_RouteFocused; + flags |= ImGuiInputFlags_Repeat; + + if (type == types::shortcut::GLOBAL_SET || type == types::shortcut::FOCUSED_SET) + { + ImGui::SetNextItemShortcut(chord, flags); + return false; + } + + return ImGui::Shortcut(chord, flags); + } +} diff --git a/src/util/imgui/shortcut.hpp b/src/util/imgui/shortcut.hpp new file mode 100644 index 0000000..975994b --- /dev/null +++ b/src/util/imgui/shortcut.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include +#include + +#include + +#include "types.hpp" + +namespace anm2ed::imgui +{ + inline const std::unordered_map KEY_MAP = { + {"A", ImGuiKey_A}, + {"B", ImGuiKey_B}, + {"C", ImGuiKey_C}, + {"D", ImGuiKey_D}, + {"E", ImGuiKey_E}, + {"F", ImGuiKey_F}, + {"G", ImGuiKey_G}, + {"H", ImGuiKey_H}, + {"I", ImGuiKey_I}, + {"J", ImGuiKey_J}, + {"K", ImGuiKey_K}, + {"L", ImGuiKey_L}, + {"M", ImGuiKey_M}, + {"N", ImGuiKey_N}, + {"O", ImGuiKey_O}, + {"P", ImGuiKey_P}, + {"Q", ImGuiKey_Q}, + {"R", ImGuiKey_R}, + {"S", ImGuiKey_S}, + {"T", ImGuiKey_T}, + {"U", ImGuiKey_U}, + {"V", ImGuiKey_V}, + {"W", ImGuiKey_W}, + {"X", ImGuiKey_X}, + {"Y", ImGuiKey_Y}, + {"Z", ImGuiKey_Z}, + {"0", ImGuiKey_0}, + {"1", ImGuiKey_1}, + {"2", ImGuiKey_2}, + {"3", ImGuiKey_3}, + {"4", ImGuiKey_4}, + {"5", ImGuiKey_5}, + {"6", ImGuiKey_6}, + {"7", ImGuiKey_7}, + {"8", ImGuiKey_8}, + {"9", ImGuiKey_9}, + {"Num0", ImGuiKey_Keypad0}, + {"Num1", ImGuiKey_Keypad1}, + {"Num2", ImGuiKey_Keypad2}, + {"Num3", ImGuiKey_Keypad3}, + {"Num4", ImGuiKey_Keypad4}, + {"Num5", ImGuiKey_Keypad5}, + {"Num6", ImGuiKey_Keypad6}, + {"Num7", ImGuiKey_Keypad7}, + {"Num8", ImGuiKey_Keypad8}, + {"Num9", ImGuiKey_Keypad9}, + {"NumAdd", ImGuiKey_KeypadAdd}, + {"NumSubtract", ImGuiKey_KeypadSubtract}, + {"NumMultiply", ImGuiKey_KeypadMultiply}, + {"NumDivide", ImGuiKey_KeypadDivide}, + {"NumEnter", ImGuiKey_KeypadEnter}, + {"NumDecimal", ImGuiKey_KeypadDecimal}, + {"NumEqual", ImGuiKey_KeypadEqual}, + {"F1", ImGuiKey_F1}, + {"F2", ImGuiKey_F2}, + {"F3", ImGuiKey_F3}, + {"F4", ImGuiKey_F4}, + {"F5", ImGuiKey_F5}, + {"F6", ImGuiKey_F6}, + {"F7", ImGuiKey_F7}, + {"F8", ImGuiKey_F8}, + {"F9", ImGuiKey_F9}, + {"F10", ImGuiKey_F10}, + {"F11", ImGuiKey_F11}, + {"F12", ImGuiKey_F12}, + {"Up", ImGuiKey_UpArrow}, + {"Down", ImGuiKey_DownArrow}, + {"Left", ImGuiKey_LeftArrow}, + {"Right", ImGuiKey_RightArrow}, + {"Space", ImGuiKey_Space}, + {"Enter", ImGuiKey_Enter}, + {"Escape", ImGuiKey_Escape}, + {"Tab", ImGuiKey_Tab}, + {"Backspace", ImGuiKey_Backspace}, + {"Delete", ImGuiKey_Delete}, + {"Insert", ImGuiKey_Insert}, + {"Home", ImGuiKey_Home}, + {"End", ImGuiKey_End}, + {"PageUp", ImGuiKey_PageUp}, + {"PageDown", ImGuiKey_PageDown}, + {"Minus", ImGuiKey_Minus}, + {"Equal", ImGuiKey_Equal}, + {"LeftBracket", ImGuiKey_LeftBracket}, + {"RightBracket", ImGuiKey_RightBracket}, + {"Semicolon", ImGuiKey_Semicolon}, + {"Apostrophe", ImGuiKey_Apostrophe}, + {"Comma", ImGuiKey_Comma}, + {"Period", ImGuiKey_Period}, + {"Slash", ImGuiKey_Slash}, + {"Backslash", ImGuiKey_Backslash}, + {"GraveAccent", ImGuiKey_GraveAccent}, + {"Keypad0", ImGuiKey_Keypad0}, + {"Keypad1", ImGuiKey_Keypad1}, + {"Keypad2", ImGuiKey_Keypad2}, + {"Keypad3", ImGuiKey_Keypad3}, + {"Keypad4", ImGuiKey_Keypad4}, + {"Keypad5", ImGuiKey_Keypad5}, + {"Keypad6", ImGuiKey_Keypad6}, + {"Keypad7", ImGuiKey_Keypad7}, + {"Keypad8", ImGuiKey_Keypad8}, + {"Keypad9", ImGuiKey_Keypad9}, + {"KeypadAdd", ImGuiKey_KeypadAdd}, + {"KeypadSubtract", ImGuiKey_KeypadSubtract}, + {"KeypadMultiply", ImGuiKey_KeypadMultiply}, + {"KeypadDivide", ImGuiKey_KeypadDivide}, + {"KeypadEnter", ImGuiKey_KeypadEnter}, + {"KeypadDecimal", ImGuiKey_KeypadDecimal}, + {"KeypadEqual", ImGuiKey_KeypadEqual}, + {"UpArrow", ImGuiKey_UpArrow}, + {"DownArrow", ImGuiKey_DownArrow}, + {"LeftArrow", ImGuiKey_LeftArrow}, + {"RightArrow", ImGuiKey_RightArrow}, + }; + + inline const std::unordered_map MOD_MAP = { + {"Ctrl", ImGuiMod_Ctrl}, + {"Shift", ImGuiMod_Shift}, + {"Alt", ImGuiMod_Alt}, + {"Super", ImGuiMod_Super}, + }; + + std::string chord_to_string(ImGuiKeyChord); + ImGuiKeyChord string_to_chord(const std::string&); + bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET); +} diff --git a/src/util/imgui/theme.cpp b/src/util/imgui/theme.cpp new file mode 100644 index 0000000..fa3af51 --- /dev/null +++ b/src/util/imgui/theme.cpp @@ -0,0 +1,56 @@ +#include "theme.hpp" + +namespace anm2ed::imgui +{ + constexpr ImVec4 COLOR_LIGHT_BUTTON{0.98f, 0.98f, 0.98f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TITLE_BG{0.78f, 0.78f, 0.78f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TITLE_BG_ACTIVE{0.64f, 0.64f, 0.64f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TITLE_BG_COLLAPSED{0.74f, 0.74f, 0.74f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TABLE_HEADER{0.78f, 0.78f, 0.78f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB{0.74f, 0.74f, 0.74f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_HOVERED{0.82f, 0.82f, 0.82f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_SELECTED{0.92f, 0.92f, 0.92f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED{0.70f, 0.70f, 0.70f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_SELECTED{0.86f, 0.86f, 0.86f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_OVERLINE{0.55f, 0.55f, 0.55f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_OVERLINE{0.50f, 0.50f, 0.50f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_CHECK_MARK{0.0f, 0.0f, 0.0f, 1.0f}; + constexpr auto FRAME_BORDER_SIZE = 1.0f; + + void theme_set(types::theme::Type theme) + { + switch (theme) + { + case types::theme::LIGHT: + ImGui::StyleColorsLight(); + break; + case types::theme::DARK: + default: + ImGui::StyleColorsDark(); + break; + case types::theme::CLASSIC: + ImGui::StyleColorsClassic(); + break; + } + auto& style = ImGui::GetStyle(); + style.FrameBorderSize = FRAME_BORDER_SIZE; + + if (theme == types::theme::LIGHT) + { + auto& colors = style.Colors; + colors[ImGuiCol_Button] = COLOR_LIGHT_BUTTON; + colors[ImGuiCol_TitleBg] = COLOR_LIGHT_TITLE_BG; + colors[ImGuiCol_TitleBgActive] = COLOR_LIGHT_TITLE_BG_ACTIVE; + colors[ImGuiCol_TitleBgCollapsed] = COLOR_LIGHT_TITLE_BG_COLLAPSED; + colors[ImGuiCol_TableHeaderBg] = COLOR_LIGHT_TABLE_HEADER; + colors[ImGuiCol_Tab] = COLOR_LIGHT_TAB; + colors[ImGuiCol_TabHovered] = COLOR_LIGHT_TAB_HOVERED; + colors[ImGuiCol_TabSelected] = COLOR_LIGHT_TAB_SELECTED; + colors[ImGuiCol_TabSelectedOverline] = COLOR_LIGHT_TAB_OVERLINE; + colors[ImGuiCol_TabDimmed] = COLOR_LIGHT_TAB_DIMMED; + colors[ImGuiCol_TabDimmedSelected] = COLOR_LIGHT_TAB_DIMMED_SELECTED; + colors[ImGuiCol_TabDimmedSelectedOverline] = COLOR_LIGHT_TAB_DIMMED_OVERLINE; + colors[ImGuiCol_CheckMark] = COLOR_LIGHT_CHECK_MARK; + } + } +} diff --git a/src/util/imgui/theme.hpp b/src/util/imgui/theme.hpp new file mode 100644 index 0000000..29062d2 --- /dev/null +++ b/src/util/imgui/theme.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "types.hpp" + +namespace anm2ed::imgui +{ + void theme_set(types::theme::Type); +} diff --git a/src/util/imgui/tooltip.cpp b/src/util/imgui/tooltip.cpp new file mode 100644 index 0000000..10e3b87 --- /dev/null +++ b/src/util/imgui/tooltip.cpp @@ -0,0 +1,16 @@ +#include "tooltip.hpp" + +#include + +#include + +#include "strings.hpp" + +namespace anm2ed::imgui +{ + void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut) + { + ImGui::SetItemTooltip( + "%s", std::vformat(localize.get(FORMAT_TOOLTIP_SHORTCUT), std::make_format_args(tooltip, shortcut)).c_str()); + } +} diff --git a/src/util/imgui/tooltip.hpp b/src/util/imgui/tooltip.hpp new file mode 100644 index 0000000..1ba162c --- /dev/null +++ b/src/util/imgui/tooltip.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace anm2ed::imgui +{ + void set_item_tooltip_shortcut(const char*, const std::string& = {}); +} diff --git a/src/util/map_.hpp b/src/util/map.hpp similarity index 100% rename from src/util/map_.hpp rename to src/util/map.hpp diff --git a/src/util/math_.cpp b/src/util/math.cpp similarity index 99% rename from src/util/math_.cpp rename to src/util/math.cpp index fb0f158..e9649dc 100644 --- a/src/util/math_.cpp +++ b/src/util/math.cpp @@ -1,4 +1,4 @@ -#include "math_.hpp" +#include "math.hpp" #include #include diff --git a/src/util/math_.hpp b/src/util/math.hpp similarity index 100% rename from src/util/math_.hpp rename to src/util/math.hpp diff --git a/src/util/path_.cpp b/src/util/path.cpp similarity index 99% rename from src/util/path_.cpp rename to src/util/path.cpp index b9baad2..f3cfae3 100644 --- a/src/util/path_.cpp +++ b/src/util/path.cpp @@ -1,4 +1,4 @@ -#include "path_.hpp" +#include "path.hpp" #include #include diff --git a/src/util/path_.hpp b/src/util/path.hpp similarity index 100% rename from src/util/path_.hpp rename to src/util/path.hpp diff --git a/src/util/process_.cpp b/src/util/process.cpp similarity index 98% rename from src/util/process_.cpp rename to src/util/process.cpp index 2a74ee8..a448793 100644 --- a/src/util/process_.cpp +++ b/src/util/process.cpp @@ -1,4 +1,4 @@ -#include "process_.hpp" +#include "process.hpp" #ifdef WIN32 #include diff --git a/src/util/process_.hpp b/src/util/process.hpp similarity index 100% rename from src/util/process_.hpp rename to src/util/process.hpp diff --git a/src/util/sdl.cpp b/src/util/sdl.cpp index 4a229bf..8d5040d 100644 --- a/src/util/sdl.cpp +++ b/src/util/sdl.cpp @@ -2,7 +2,7 @@ #include -#include "path_.hpp" +#include "path.hpp" namespace anm2ed::util::sdl { diff --git a/src/util/string_.cpp b/src/util/string.cpp similarity index 96% rename from src/util/string_.cpp rename to src/util/string.cpp index 4bb3da2..a2a427e 100644 --- a/src/util/string_.cpp +++ b/src/util/string.cpp @@ -1,4 +1,4 @@ -#include "string_.hpp" +#include "string.hpp" #include diff --git a/src/util/string_.hpp b/src/util/string.hpp similarity index 100% rename from src/util/string_.hpp rename to src/util/string.hpp diff --git a/src/util/time_.cpp b/src/util/time.cpp similarity index 95% rename from src/util/time_.cpp rename to src/util/time.cpp index 047cac0..35a2bd6 100644 --- a/src/util/time_.cpp +++ b/src/util/time.cpp @@ -1,4 +1,4 @@ -#include "time_.hpp" +#include "time.hpp" #include #include diff --git a/src/util/time_.hpp b/src/util/time.hpp similarity index 100% rename from src/util/time_.hpp rename to src/util/time.hpp diff --git a/src/util/unordered_map_.hpp b/src/util/unordered_map.hpp similarity index 100% rename from src/util/unordered_map_.hpp rename to src/util/unordered_map.hpp diff --git a/src/util/vector_.hpp b/src/util/vector.hpp similarity index 69% rename from src/util/vector_.hpp rename to src/util/vector.hpp index dcb419e..12fa952 100644 --- a/src/util/vector_.hpp +++ b/src/util/vector.hpp @@ -85,4 +85,38 @@ namespace anm2ed::util::vector return moveIndices; } + + template std::set move_indices_to_position(std::vector& v, const std::vector& indices, + int insertPos) + { + if (indices.empty()) return {}; + + std::vector sorted = indices; + std::sort(sorted.begin(), sorted.end()); + sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end()); + std::erase_if(sorted, [&](int i) { return i < 0 || i >= (int)v.size(); }); + if (sorted.empty()) return {}; + + std::vector moveItems; + moveItems.reserve(sorted.size()); + for (int i : sorted) + moveItems.push_back(std::move(v[i])); + + for (auto i : sorted | std::views::reverse) + v.erase(v.begin() + i); + + insertPos = std::clamp(insertPos, 0, (int)v.size() + (int)sorted.size()); + for (int i : sorted) + if (i < insertPos) --insertPos; + insertPos = std::clamp(insertPos, 0, (int)v.size()); + + v.insert(v.begin() + insertPos, std::make_move_iterator(moveItems.begin()), + std::make_move_iterator(moveItems.end())); + + std::set moveIndices{}; + for (int i = 0; i < (int)moveItems.size(); i++) + moveIndices.insert(insertPos + i); + + return moveIndices; + } } diff --git a/src/util/working_directory.cpp b/src/util/working_directory.cpp index 644940c..d42fae1 100644 --- a/src/util/working_directory.cpp +++ b/src/util/working_directory.cpp @@ -3,7 +3,7 @@ #include #include "log.hpp" -#include "path_.hpp" +#include "path.hpp" namespace anm2ed::util { diff --git a/src/util/xml_.cpp b/src/util/xml.cpp similarity index 98% rename from src/util/xml_.cpp rename to src/util/xml.cpp index 4c6f4b1..33d8445 100644 --- a/src/util/xml_.cpp +++ b/src/util/xml.cpp @@ -1,7 +1,7 @@ -#include "xml_.hpp" +#include "xml.hpp" -#include "math_.hpp" -#include "path_.hpp" +#include "math.hpp" +#include "path.hpp" using namespace anm2ed::util; using namespace tinyxml2; diff --git a/src/util/xml_.hpp b/src/util/xml.hpp similarity index 100% rename from src/util/xml_.hpp rename to src/util/xml.hpp