diff --git a/src/anm2/animation.cpp b/src/anm2/animation.cpp index a275dbc..656a505 100644 --- a/src/anm2/animation.cpp +++ b/src/anm2/animation.cpp @@ -8,7 +8,6 @@ using namespace anm2ed::util; using namespace glm; - using namespace tinyxml2; namespace anm2ed::anm2 @@ -79,7 +78,7 @@ namespace anm2ed::anm2 } } - void Animation::serialize(XMLDocument& document, XMLElement* parent) + XMLElement* Animation::to_element(XMLDocument& document) { auto element = document.NewElement("Animation"); element->SetAttribute("Name", name.c_str()); @@ -103,7 +102,19 @@ namespace anm2ed::anm2 triggers.serialize(document, element, TRIGGER); - parent->InsertEndChild(element); + return element; + } + + void Animation::serialize(XMLDocument& document, XMLElement* parent) + { + parent->InsertEndChild(to_element(document)); + } + + std::string Animation::to_string() + { + XMLDocument document{}; + document.InsertEndChild(to_element(document)); + return xml::document_to_string(document); } int Animation::length() @@ -126,49 +137,16 @@ namespace anm2ed::anm2 return length; } - std::string Animation::to_string() - { - XMLDocument document{}; - - auto* element = document.NewElement("Animation"); - document.InsertFirstChild(element); - - element->SetAttribute("Name", name.c_str()); - element->SetAttribute("FrameNum", frameNum); - element->SetAttribute("Loop", isLoop); - - rootAnimation.serialize(document, element, ROOT); - - auto layerAnimationsElement = document.NewElement("LayerAnimations"); - for (auto& i : layerOrder) - { - Item& layerAnimation = layerAnimations.at(i); - layerAnimation.serialize(document, layerAnimationsElement, LAYER, i); - } - element->InsertEndChild(layerAnimationsElement); - - auto nullAnimationsElement = document.NewElement("NullAnimations"); - for (auto& [id, nullAnimation] : nullAnimations) - nullAnimation.serialize(document, nullAnimationsElement, NULL_, id); - element->InsertEndChild(nullAnimationsElement); - - triggers.serialize(document, element, TRIGGER); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - vec4 Animation::rect(bool isRootTransform) { + 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; - constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; - for (float t = 0.0f; t < (float)frameNum; t += 1.0f) { mat4 transform(1.0f); @@ -202,5 +180,4 @@ namespace anm2ed::anm2 if (!any) return vec4(-1.0f); return {minX, minY, maxX - minX, maxY - minY}; } - } \ No newline at end of file diff --git a/src/anm2/animation.h b/src/anm2/animation.h index dc47fc7..0cc68a8 100644 --- a/src/anm2/animation.h +++ b/src/anm2/animation.h @@ -26,10 +26,11 @@ namespace anm2ed::anm2 Animation(tinyxml2::XMLElement*); Item* item_get(Type, int = -1); void item_remove(Type, int = -1); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&); void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); + std::string to_string(); int length(); glm::vec4 rect(bool); - std::string to_string(); }; } \ No newline at end of file diff --git a/src/anm2/animations.cpp b/src/anm2/animations.cpp index 1ace1bb..1eaaed4 100644 --- a/src/anm2/animations.cpp +++ b/src/anm2/animations.cpp @@ -1,7 +1,5 @@ #include "animations.h" -#include - #include "xml_.h" using namespace tinyxml2; @@ -42,114 +40,4 @@ namespace anm2ed::anm2 return length; } - int Animations::merge(int target, std::set& sources, merge::Type type, bool isDeleteAfter) - { - Animation& animation = items.at(target); - - if (!animation.name.ends_with(MERGED_STRING)) animation.name = animation.name + " " + MERGED_STRING; - - auto merge_item = [&](Item& destination, Item& source) - { - switch (type) - { - case merge::APPEND: - destination.frames.insert(destination.frames.end(), source.frames.begin(), source.frames.end()); - break; - case merge::PREPEND: - destination.frames.insert(destination.frames.begin(), source.frames.begin(), source.frames.end()); - break; - case merge::REPLACE: - if (destination.frames.size() < source.frames.size()) destination.frames.resize(source.frames.size()); - for (int i = 0; i < (int)source.frames.size(); i++) - destination.frames[i] = source.frames[i]; - break; - case merge::IGNORE: - default: - break; - } - }; - - for (auto& i : sources) - { - if (i == target) continue; - if (i < 0 || i >= (int)items.size()) continue; - - auto& source = items.at(i); - - merge_item(animation.rootAnimation, source.rootAnimation); - - for (auto& [id, layerAnimation] : source.layerAnimations) - { - if (!animation.layerAnimations.contains(id)) - { - animation.layerAnimations[id] = layerAnimation; - animation.layerOrder.emplace_back(id); - } - merge_item(animation.layerAnimations[id], layerAnimation); - } - - for (auto& [id, nullAnimation] : source.nullAnimations) - { - if (!animation.nullAnimations.contains(id)) animation.nullAnimations[id] = nullAnimation; - merge_item(animation.nullAnimations[id], nullAnimation); - } - - merge_item(animation.triggers, source.triggers); - } - - if (isDeleteAfter) - { - for (auto& source : std::ranges::reverse_view(sources)) - { - if (source == target) continue; - items.erase(items.begin() + source); - } - } - - int finalIndex = target; - - if (isDeleteAfter) - { - int numDeletedBefore = 0; - for (auto& idx : sources) - { - if (idx == target) continue; - if (idx >= 0 && idx < target) ++numDeletedBefore; - } - finalIndex -= numDeletedBefore; - } - - return finalIndex; - } - - bool Animations::animations_deserialize(const std::string& string, int start, std::set& indices, - std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - if (!document.FirstChildElement("Animation")) - { - if (errorString) *errorString = "No valid animation(s)."; - return false; - } - - int count{}; - for (auto element = document.FirstChildElement("Animation"); element; - element = element->NextSiblingElement("Animation")) - { - auto index = start + count; - items.insert(items.begin() + start + count, Animation(element)); - indices.insert(index); - count++; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } } \ No newline at end of file diff --git a/src/anm2/animations.h b/src/anm2/animations.h index ce377b2..a2c4fe5 100644 --- a/src/anm2/animations.h +++ b/src/anm2/animations.h @@ -14,9 +14,7 @@ namespace anm2ed::anm2 Animations() = default; Animations(tinyxml2::XMLElement*); tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&); - int length(); void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); - int merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); - bool animations_deserialize(const std::string&, int, std::set&, std::string* = nullptr); + int length(); }; } \ No newline at end of file diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp index 0b53b9c..79963a5 100644 --- a/src/anm2/anm2.cpp +++ b/src/anm2/anm2.cpp @@ -1,12 +1,9 @@ #include "anm2.h" -#include - #include "filesystem_.h" -#include "map_.h" #include "time_.h" -#include "unordered_map_.h" #include "vector_.h" +#include "xml_.h" using namespace tinyxml2; using namespace anm2ed::types; @@ -20,41 +17,6 @@ namespace anm2ed::anm2 info.createdOn = time::get("%d-%B-%Y %I:%M:%S"); } - bool Anm2::serialize(const std::string& path, std::string* errorString) - { - XMLDocument document; - - auto* element = document.NewElement("AnimatedActor"); - document.InsertFirstChild(element); - - info.serialize(document, element); - content.serialize(document, element); - animations.serialize(document, element); - - if (document.SaveFile(path.c_str()) != XML_SUCCESS) - { - if (errorString) *errorString = document.ErrorStr(); - return false; - } - return true; - } - - std::string Anm2::to_string() - { - XMLDocument document; - - auto* element = document.NewElement("AnimatedActor"); - document.InsertFirstChild(element); - - info.serialize(document, element); - content.serialize(document, element); - animations.serialize(document, element); - - XMLPrinter printer; - document.Print(&printer); - return std::string(printer.CStr()); - } - Anm2::Anm2(const std::string& path, std::string* errorString) { XMLDocument document; @@ -75,302 +37,53 @@ namespace anm2ed::anm2 animations = Animations((XMLElement*)animationsElement); } + bool Anm2::serialize(const std::string& path, std::string* errorString) + { + XMLDocument document; + + auto* element = document.NewElement("AnimatedActor"); + document.InsertFirstChild(element); + + info.serialize(document, element); + content.serialize(document, element); + animations.serialize(document, element); + + if (document.SaveFile(path.c_str()) != XML_SUCCESS) + { + if (errorString) *errorString = document.ErrorStr(); + return false; + } + return true; + } + + XMLElement* Anm2::to_element(XMLDocument& document) + { + auto element = document.NewElement("AnimatedActor"); + document.InsertFirstChild(element); + + info.serialize(document, element); + content.serialize(document, element); + animations.serialize(document, element); + + return element; + } + + std::string Anm2::to_string() + { + XMLDocument document{}; + document.InsertEndChild(to_element(document)); + return xml::document_to_string(document); + } + uint64_t Anm2::hash() { return std::hash{}(to_string()); } - Animation* Anm2::animation_get(Reference reference) - { - return vector::find(animations.items, reference.animationIndex); - } - - std::vector Anm2::animation_names_get() - { - std::vector names{}; - for (auto& animation : animations.items) - names.push_back(animation.name); - return names; - } - - Item* Anm2::item_get(Reference reference) - { - if (Animation* animation = animation_get(reference)) - { - switch (reference.itemType) - { - case ROOT: - return &animation->rootAnimation; - case LAYER: - return unordered_map::find(animation->layerAnimations, reference.itemID); - case NULL_: - return map::find(animation->nullAnimations, reference.itemID); - case TRIGGER: - return &animation->triggers; - default: - return nullptr; - } - } - return nullptr; - } - Frame* Anm2::frame_get(Reference reference) { - Item* item = item_get(reference); - if (!item) return nullptr; - return vector::find(item->frames, reference.frameIndex); + if (auto item = item_get(reference); item) + if (vector::in_bounds(item->frames, reference.frameIndex)) return &item->frames[reference.frameIndex]; return nullptr; } - - bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id) - { - Spritesheet spritesheet(directory, path); - if (!spritesheet.is_valid()) return false; - id = map::next_id_get(content.spritesheets); - content.spritesheets[id] = std::move(spritesheet); - return true; - } - - void Anm2::spritesheet_remove(int id) - { - content.spritesheets.erase(id); - } - - Spritesheet* Anm2::spritesheet_get(int id) - { - return map::find(content.spritesheets, id); - } - - std::set Anm2::spritesheets_unused() - { - return content.spritesheets_unused(); - } - - std::vector Anm2::spritesheet_names_get() - { - std::vector names{}; - for (auto& [id, spritesheet] : content.spritesheets) - names.push_back(std::format(SPRITESHEET_FORMAT, id, spritesheet.path.c_str())); - return names; - } - - Reference Anm2::layer_add(Reference reference, std::string name, int spritesheetID, locale::Type locale) - { - auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID; - auto& layer = content.layers[id]; - - layer.name = !name.empty() ? name : layer.name; - layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0; - - auto add = [&](Animation* animation, int id) - { - animation->layerAnimations[id] = Item(); - animation->layerOrder.push_back(id); - }; - - if (locale == locale::GLOBAL) - { - for (auto& animation : animations.items) - if (!animation.layerAnimations.contains(id)) add(&animation, id); - } - else if (locale == locale::LOCAL) - { - if (auto animation = animation_get(reference)) - if (!animation->layerAnimations.contains(id)) add(animation, id); - } - - return {reference.animationIndex, LAYER, id}; - } - - Reference Anm2::null_add(Reference reference, std::string name, locale::Type locale) - { - auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID; - auto& null = content.nulls[id]; - - null.name = !name.empty() ? name : null.name; - - auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); }; - - if (locale == locale::GLOBAL) - { - for (auto& animation : animations.items) - if (!animation.nullAnimations.contains(id)) add(&animation, id); - } - else if (locale == locale::LOCAL) - { - if (auto animation = animation_get(reference)) - if (!animation->nullAnimations.contains(id)) add(animation, id); - } - - return {reference.animationIndex, LAYER, id}; - } - - void Anm2::event_add(int& id) - { - content.event_add(id); - } - - std::set Anm2::events_unused(Reference reference) - { - std::set used{}; - std::set unused{}; - - if (auto animation = animation_get(reference); animation) - for (auto& frame : animation->triggers.frames) - used.insert(frame.eventID); - else - for (auto& animation : animations.items) - for (auto& frame : animation.triggers.frames) - used.insert(frame.eventID); - - for (auto& id : content.events | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - std::set Anm2::layers_unused(Reference reference) - { - std::set used{}; - std::set unused{}; - - if (auto animation = animation_get(reference); animation) - for (auto& id : animation->layerAnimations | std::views::keys) - used.insert(id); - else - for (auto& animation : animations.items) - for (auto& id : animation.layerAnimations | std::views::keys) - used.insert(id); - - for (auto& id : content.layers | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - std::set Anm2::nulls_unused(Reference reference) - { - std::set used{}; - std::set unused{}; - - if (auto animation = animation_get(reference); animation) - for (auto& id : animation->nullAnimations | std::views::keys) - used.insert(id); - else - for (auto& animation : animations.items) - for (auto& id : animation.nullAnimations | std::views::keys) - used.insert(id); - - for (auto& id : content.nulls | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - std::vector Anm2::event_names_get() - { - std::vector names{}; - for (auto& event : content.events | std::views::values) - names.push_back(event.name); - return names; - } - - bool Anm2::sound_add(const std::string& directory, const std::string& path, int& id) - { - id = map::next_id_get(content.sounds); - content.sounds[id] = Sound(directory, path); - return true; - } - - std::set Anm2::sounds_unused() - { - std::set used; - for (auto& event : content.events | std::views::values) - used.insert(event.soundID); - - std::set unused; - for (auto& id : content.sounds | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - std::vector Anm2::sound_names_get() - { - std::vector names{}; - for (auto& [id, sound] : content.sounds) - names.push_back(std::format(SOUND_FORMAT, id, sound.path.c_str())); - return names; - } - - void Anm2::bake(Reference reference, int interval, bool isRoundScale, bool isRoundRotation) - { - Item* item = item_get(reference); - if (!item) return; - - Frame* frame = frame_get(reference); - if (!frame) return; - - if (frame->delay == FRAME_DELAY_MIN) return; - - Reference referenceNext = reference; - referenceNext.frameIndex = reference.frameIndex + 1; - - Frame* frameNext = frame_get(referenceNext); - if (!frameNext) frameNext = frame; - - Frame baseFrame = *frame; - Frame baseFrameNext = *frameNext; - - int delay{}; - int index = reference.frameIndex; - - while (delay < baseFrame.delay) - { - float interpolation = (float)delay / baseFrame.delay; - - Frame baked = baseFrame; - baked.delay = std::min(interval, baseFrame.delay - delay); - baked.isInterpolated = (index == reference.frameIndex) ? baseFrame.isInterpolated : false; - - baked.rotation = glm::mix(baseFrame.rotation, baseFrameNext.rotation, interpolation); - baked.position = glm::mix(baseFrame.position, baseFrameNext.position, interpolation); - baked.scale = glm::mix(baseFrame.scale, baseFrameNext.scale, interpolation); - baked.colorOffset = glm::mix(baseFrame.colorOffset, baseFrameNext.colorOffset, interpolation); - baked.tint = glm::mix(baseFrame.tint, baseFrameNext.tint, interpolation); - - if (isRoundScale) baked.scale = vec2(ivec2(baked.scale)); - if (isRoundRotation) baked.rotation = (int)baked.rotation; - - if (index == reference.frameIndex) - item->frames[index] = baked; - else - item->frames.insert(item->frames.begin() + index, baked); - index++; - - delay += baked.delay; - } - } - - void Anm2::generate_from_grid(Reference reference, ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, - int count, int delay) - { - auto item = item_get(reference); - if (!item) return; - - for (int i = 0; i < count; i++) - { - auto row = i / columns; - auto column = i % columns; - - Frame frame{}; - - frame.delay = delay; - frame.pivot = pivot; - frame.size = size; - frame.crop = startPosition + ivec2(size.x * column, size.y * row); - - item->frames.emplace_back(frame); - } - } -} +} \ No newline at end of file diff --git a/src/anm2/anm2.h b/src/anm2/anm2.h index 9ce3207..3f970ff 100644 --- a/src/anm2/anm2.h +++ b/src/anm2/anm2.h @@ -24,8 +24,6 @@ namespace anm2ed::anm2 auto operator<=>(const Reference&) const = default; }; - constexpr anm2::Reference REFERENCE_DEFAULT = {-1, anm2::NONE, -1, -1, -1}; - class Anm2 { public: @@ -34,39 +32,47 @@ namespace anm2ed::anm2 Animations animations{}; Anm2(); + tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&); bool serialize(const std::string&, std::string* = nullptr); std::string to_string(); Anm2(const std::string&, std::string* = nullptr); uint64_t hash(); - Animation* animation_get(Reference); - std::vector animation_names_get(); - Item* item_get(Reference); - - Frame* frame_get(Reference); - - bool spritesheet_add(const std::string&, const std::string&, int&); Spritesheet* spritesheet_get(int); + bool spritesheet_add(const std::string&, const std::string&, int&); void spritesheet_remove(int); + std::vector spritesheet_labels_get(); std::set spritesheets_unused(); - std::vector spritesheet_names_get(); + bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type type, std::string*); - int layer_add(); - Reference layer_add(Reference = REFERENCE_DEFAULT, std::string = {}, int = 0, - types::locale::Type = types::locale::GLOBAL); - std::set layers_unused(Reference = REFERENCE_DEFAULT); + void layer_add(int&); + std::set layers_unused(Reference = {}); + bool layers_deserialize(const std::string&, types::merge::Type, std::string*); - Reference null_add(Reference = REFERENCE_DEFAULT, std::string = {}, types::locale::Type = types::locale::GLOBAL); - std::set nulls_unused(Reference = REFERENCE_DEFAULT); - - bool sound_add(const std::string& directory, const std::string& path, int& id); - std::vector sound_names_get(); - std::set sounds_unused(); + void null_add(int&); + std::set nulls_unused(Reference = {}); + bool nulls_deserialize(const std::string&, types::merge::Type, std::string*); void event_add(int&); - std::set events_unused(Reference = REFERENCE_DEFAULT); - std::vector event_names_get(); - void bake(Reference, int = 1, bool = true, bool = true); - void generate_from_grid(Reference, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); + std::vector event_labels_get(); + std::set events_unused(Reference = {}); + bool events_deserialize(const std::string&, types::merge::Type, std::string*); + + bool sound_add(const std::string& directory, const std::string& path, int& id); + std::vector sound_labels_get(); + std::set sounds_unused(); + bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string*); + + Animation* animation_get(Reference); + std::vector animation_labels_get(); + int animations_merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); + bool animations_deserialize(const std::string&, int, std::set&, std::string* = nullptr); + + Item* item_get(Reference); + Reference layer_animation_add(Reference = {}, std::string = {}, int = 0, + types::locale::Type = types::locale::GLOBAL); + Reference null_animation_add(Reference = {}, std::string = {}, types::locale::Type = types::locale::GLOBAL); + + Frame* frame_get(Reference); }; } diff --git a/src/anm2/anm2_animations.cpp b/src/anm2/anm2_animations.cpp new file mode 100644 index 0000000..c4a5107 --- /dev/null +++ b/src/anm2/anm2_animations.cpp @@ -0,0 +1,137 @@ +#include "anm2.h" + +#include "vector_.h" + +using namespace anm2ed::util; +using namespace anm2ed::types; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + Animation* Anm2::animation_get(Reference reference) + { + return vector::find(animations.items, reference.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; + animations.items.insert(animations.items.begin() + start + count, Animation(element)); + 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; + } + + return finalIndex; + } + +} \ No newline at end of file diff --git a/src/anm2/anm2_events.cpp b/src/anm2/anm2_events.cpp new file mode 100644 index 0000000..903e6d8 --- /dev/null +++ b/src/anm2/anm2_events.cpp @@ -0,0 +1,75 @@ +#include "anm2.h" + +#include + +#include "map_.h" + +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("None"); + for (auto& event : content.events | std::views::values) + labels.emplace_back(event.name); + return labels; + } + + std::set Anm2::events_unused(Reference reference) + { + std::set used{}; + + if (auto animation = animation_get(reference); animation) + for (auto& frame : animation->triggers.frames) + used.insert(frame.eventID); + else + for (auto& animation : animations.items) + for (auto& frame : animation.triggers.frames) + used.insert(frame.eventID); + + 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 new file mode 100644 index 0000000..b74df0e --- /dev/null +++ b/src/anm2/anm2_items.cpp @@ -0,0 +1,83 @@ +#include "anm2.h" + +#include "map_.h" +#include "types.h" +#include "unordered_map_.h" + +using namespace anm2ed::types; +using namespace anm2ed::util; + +namespace anm2ed::anm2 +{ + Item* Anm2::item_get(Reference reference) + { + if (Animation* animation = animation_get(reference)) + { + switch (reference.itemType) + { + case ROOT: + return &animation->rootAnimation; + case LAYER: + return unordered_map::find(animation->layerAnimations, reference.itemID); + case NULL_: + return map::find(animation->nullAnimations, reference.itemID); + case TRIGGER: + return &animation->triggers; + default: + return nullptr; + } + } + return nullptr; + } + + Reference Anm2::layer_animation_add(Reference reference, std::string name, int spritesheetID, locale::Type locale) + { + auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID; + auto& layer = content.layers[id]; + + layer.name = !name.empty() ? name : layer.name; + layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0; + + auto add = [&](Animation* animation, int id) + { + animation->layerAnimations[id] = Item(); + animation->layerOrder.push_back(id); + }; + + if (locale == locale::GLOBAL) + { + for (auto& animation : animations.items) + if (!animation.layerAnimations.contains(id)) add(&animation, id); + } + else if (locale == locale::LOCAL) + { + if (auto animation = animation_get(reference)) + if (!animation->layerAnimations.contains(id)) add(animation, id); + } + + return {reference.animationIndex, LAYER, id}; + } + + Reference Anm2::null_animation_add(Reference reference, std::string name, locale::Type locale) + { + auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID; + auto& null = content.nulls[id]; + + null.name = !name.empty() ? name : null.name; + + auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); }; + + if (locale == locale::GLOBAL) + { + for (auto& animation : animations.items) + if (!animation.nullAnimations.contains(id)) add(&animation, id); + } + else if (locale == locale::LOCAL) + { + if (auto animation = animation_get(reference)) + if (!animation->nullAnimations.contains(id)) add(animation, id); + } + + return {reference.animationIndex, LAYER, id}; + } +} \ No newline at end of file diff --git a/src/anm2/anm2_layers.cpp b/src/anm2/anm2_layers.cpp new file mode 100644 index 0000000..9a48ac1 --- /dev/null +++ b/src/anm2/anm2_layers.cpp @@ -0,0 +1,66 @@ +#include "anm2.h" + +#include + +#include "map_.h" + +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(Reference reference) + { + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& id : animation->layerAnimations | std::views::keys) + used.insert(id); + else + for (auto& animation : animations.items) + for (auto& id : animation.layerAnimations | std::views::keys) + used.insert(id); + + for (auto& id : content.layers | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + 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 new file mode 100644 index 0000000..49ed6ce --- /dev/null +++ b/src/anm2/anm2_nulls.cpp @@ -0,0 +1,66 @@ +#include "anm2.h" + +#include + +#include "map_.h" + +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(Reference reference) + { + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& id : animation->nullAnimations | std::views::keys) + used.insert(id); + else + for (auto& animation : animations.items) + for (auto& id : animation.nullAnimations | std::views::keys) + used.insert(id); + + for (auto& id : content.nulls | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + 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 new file mode 100644 index 0000000..299d64c --- /dev/null +++ b/src/anm2/anm2_sounds.cpp @@ -0,0 +1,75 @@ +#include "anm2.h" + +#include + +#include "filesystem_.h" +#include "map_.h" + +using namespace anm2ed::types; +using namespace anm2ed::util; +using namespace tinyxml2; + +namespace anm2ed::anm2 +{ + bool Anm2::sound_add(const std::string& directory, const std::string& path, int& id) + { + id = map::next_id_get(content.sounds); + content.sounds[id] = Sound(directory, path); + return true; + } + + std::vector Anm2::sound_labels_get() + { + std::vector labels{}; + labels.emplace_back("None"); + for (auto& [id, sound] : content.sounds) + labels.emplace_back(sound.path.string()); + return labels; + } + + std::set Anm2::sounds_unused() + { + std::set used; + for (auto& animation : animations.items) + for (auto& trigger : animation.triggers.frames) + if (content.sounds.contains(trigger.soundID)) used.insert(trigger.soundID); + + std::set unused; + for (auto& id : content.sounds | std::views::keys) + if (!used.contains(id)) unused.insert(id); + + return unused; + } + + bool Anm2::sounds_deserialize(const std::string& string, const std::string& directory, merge::Type type, + std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + int id{}; + + if (!document.FirstChildElement("Sound")) + { + if (errorString) *errorString = "No valid sound(s)."; + return false; + } + + filesystem::WorkingDirectory workingDirectory(directory); + + for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound")) + { + auto sound = Sound(element, id); + if (type == merge::APPEND) id = map::next_id_get(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 new file mode 100644 index 0000000..9849db2 --- /dev/null +++ b/src/anm2/anm2_spritesheets.cpp @@ -0,0 +1,87 @@ +#include "anm2.h" + +#include + +#include "filesystem_.h" +#include "map_.h" + +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); + } + + void Anm2::spritesheet_remove(int id) + { + content.spritesheets.erase(id); + } + + bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id) + { + Spritesheet spritesheet(directory, path); + if (!spritesheet.is_valid()) return false; + id = map::next_id_get(content.spritesheets); + content.spritesheets[id] = std::move(spritesheet); + return true; + } + + 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; + } + + std::vector Anm2::spritesheet_labels_get() + { + std::vector labels{}; + labels.emplace_back("None"); + for (auto& [id, spritesheet] : content.spritesheets) + labels.emplace_back(std::format(SPRITESHEET_FORMAT, id, spritesheet.path.c_str())); + return labels; + } + + bool Anm2::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type, + std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + int id{}; + + if (!document.FirstChildElement("Spritesheet")) + { + if (errorString) *errorString = "No valid spritesheet(s)."; + return false; + } + + filesystem::WorkingDirectory workingDirectory(directory); + + for (auto element = document.FirstChildElement("Spritesheet"); element; + element = element->NextSiblingElement("Spritesheet")) + { + auto spritesheet = Spritesheet(element, id); + if (type == merge::APPEND) id = map::next_id_get(content.spritesheets); + content.spritesheets[id] = std::move(spritesheet); + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } +} diff --git a/src/anm2/content.cpp b/src/anm2/content.cpp index d2ca4f1..ff007ed 100644 --- a/src/anm2/content.cpp +++ b/src/anm2/content.cpp @@ -1,12 +1,5 @@ #include "content.h" -#include - -#include "filesystem_.h" -#include "map_.h" - -using namespace anm2ed::types; -using namespace anm2ed::util; using namespace tinyxml2; namespace anm2ed::anm2 @@ -60,197 +53,4 @@ namespace anm2ed::anm2 parent->InsertEndChild(element); } - std::set Content::spritesheets_unused() - { - std::set used; - for (auto& layer : layers | std::views::values) - if (layer.spritesheetID != -1) used.insert(layer.spritesheetID); - - std::set unused; - for (auto& id : spritesheets | std::views::keys) - if (!used.contains(id)) unused.insert(id); - - return unused; - } - - void Content::layer_add(int& id) - { - id = map::next_id_get(layers); - layers[id] = Layer(); - } - - void Content::null_add(int& id) - { - id = map::next_id_get(nulls); - nulls[id] = Null(); - } - - void Content::event_add(int& id) - { - id = map::next_id_get(events); - events[id] = Event(); - } - - bool Content::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type, - std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Spritesheet")) - { - if (errorString) *errorString = "No valid spritesheet(s)."; - return false; - } - - filesystem::WorkingDirectory workingDirectory(directory); - - for (auto element = document.FirstChildElement("Spritesheet"); element; - element = element->NextSiblingElement("Spritesheet")) - { - auto spritesheet = Spritesheet(element, id); - - if (type == merge::APPEND) id = map::next_id_get(spritesheets); - - spritesheets[id] = std::move(spritesheet); - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Content::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Layer")) - { - if (errorString) *errorString = "No valid layer(s)."; - return false; - } - - for (auto element = document.FirstChildElement("Layer"); element; element = element->NextSiblingElement("Layer")) - { - auto layer = Layer(element, id); - - if (type == merge::APPEND) id = map::next_id_get(layers); - - layers[id] = layer; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Content::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Null")) - { - if (errorString) *errorString = "No valid null(s)."; - return false; - } - - for (auto element = document.FirstChildElement("Null"); element; element = element->NextSiblingElement("Null")) - { - auto layer = Null(element, id); - - if (type == merge::APPEND) id = map::next_id_get(nulls); - - nulls[id] = layer; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Content::events_deserialize(const std::string& string, merge::Type type, std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Event")) - { - if (errorString) *errorString = "No valid event(s)."; - return false; - } - - for (auto element = document.FirstChildElement("Event"); element; element = element->NextSiblingElement("Event")) - { - auto layer = Event(element, id); - - if (type == merge::APPEND) id = map::next_id_get(events); - - events[id] = layer; - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - - bool Content::sounds_deserialize(const std::string& string, const std::string& directory, merge::Type type, - std::string* errorString) - { - XMLDocument document{}; - - if (document.Parse(string.c_str()) == XML_SUCCESS) - { - int id{}; - - if (!document.FirstChildElement("Sound")) - { - if (errorString) *errorString = "No valid sound(s)."; - return false; - } - - filesystem::WorkingDirectory workingDirectory(directory); - - for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound")) - { - auto sound = Sound(element, id); - - if (type == merge::APPEND) id = map::next_id_get(sounds); - - sounds[id] = std::move(sound); - } - - return true; - } - else if (errorString) - *errorString = document.ErrorStr(); - - return false; - } - -} \ No newline at end of file +} diff --git a/src/anm2/content.h b/src/anm2/content.h index e97650b..330c433 100644 --- a/src/anm2/content.h +++ b/src/anm2/content.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "event.h" #include "layer.h" @@ -9,8 +8,6 @@ #include "sound.h" #include "spritesheet.h" -#include "types.h" - namespace anm2ed::anm2 { struct Content @@ -25,22 +22,5 @@ namespace anm2ed::anm2 void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); Content(tinyxml2::XMLElement*); - - bool spritesheet_add(const std::string&, const std::string&, int&); - std::set spritesheets_unused(); - void spritesheet_remove(int&); - bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr); - - void layer_add(int&); - bool layers_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); - - void null_add(int&); - bool nulls_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); - - void event_add(int&); - bool events_deserialize(const std::string&, types::merge::Type, std::string* = nullptr); - - void sound_add(int&); - bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr); }; } \ No newline at end of file diff --git a/src/anm2/event.h b/src/anm2/event.h index 3f23d86..f86d35d 100644 --- a/src/anm2/event.h +++ b/src/anm2/event.h @@ -9,7 +9,7 @@ namespace anm2ed::anm2 { public: std::string name{"New Event"}; - int soundID{}; + int soundID{-1}; Event() = default; Event(tinyxml2::XMLElement*, int&); diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp index 0ff462b..5e22172 100644 --- a/src/anm2/frame.cpp +++ b/src/anm2/frame.cpp @@ -123,4 +123,12 @@ namespace anm2ed::anm2 { delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); } + + bool Frame::is_visible(Type type) + { + if (type == anm2::TRIGGER) + return isVisible && eventID > -1; + else + return isVisible; + } } \ No newline at end of file diff --git a/src/anm2/frame.h b/src/anm2/frame.h index b5a3f76..0e5648b 100644 --- a/src/anm2/frame.h +++ b/src/anm2/frame.h @@ -55,6 +55,7 @@ namespace anm2ed::anm2 X(delay, int, FRAME_DELAY_MIN) \ X(atFrame, int, -1) \ X(eventID, int, -1) \ + X(soundID, int, -1) \ X(pivot, glm::vec2, {}) \ X(crop, glm::vec2, {}) \ X(position, glm::vec2, {}) \ @@ -77,6 +78,7 @@ namespace anm2ed::anm2 void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type); void shorten(); void extend(); + bool is_visible(Type = NONE); }; struct FrameChange diff --git a/src/anm2/item.cpp b/src/anm2/item.cpp index 0b20ca5..d262bf4 100644 --- a/src/anm2/item.cpp +++ b/src/anm2/item.cpp @@ -1,10 +1,12 @@ #include "item.h" #include +#include "vector_.h" #include "xml_.h" using namespace anm2ed::util; using namespace tinyxml2; +using namespace glm; namespace anm2ed::anm2 { @@ -114,7 +116,7 @@ namespace anm2ed::anm2 return frame; } - void Item::frames_change(anm2::FrameChange& change, ChangeType type, int start, int numberFrames) + void Item::frames_change(FrameChange& change, ChangeType type, int start, int numberFrames) { auto useStart = numberFrames > -1 ? start : 0; auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size(); @@ -197,4 +199,54 @@ namespace anm2ed::anm2 return false; } + + void Item::frames_bake(int index, int interval, bool isRoundScale, bool isRoundRotation) + { + if (!vector::in_bounds(frames, index)) return; + + Frame& frame = frames[index]; + if (frame.delay == FRAME_DELAY_MIN) return; + + Frame frameNext = vector::in_bounds(frames, index + 1) ? frames[index + 1] : frame; + + int delay{}; + int i = index; + + while (delay < frame.delay) + { + Frame baked = frame; + float interpolation = (float)delay / frame.delay; + baked.delay = std::min(interval, frame.delay - delay); + baked.isInterpolated = (i == index) ? frame.isInterpolated : false; + baked.rotation = glm::mix(frame.rotation, frameNext.rotation, interpolation); + baked.position = glm::mix(frame.position, frameNext.position, interpolation); + baked.scale = glm::mix(frame.scale, frameNext.scale, interpolation); + baked.colorOffset = glm::mix(frame.colorOffset, frameNext.colorOffset, interpolation); + baked.tint = glm::mix(frame.tint, frameNext.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++; + + delay += baked.delay; + } + } + + void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, int delay) + { + for (int i = 0; i < count; i++) + { + Frame frame{}; + frame.delay = delay; + frame.pivot = pivot; + frame.size = size; + frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns)); + + frames.emplace_back(frame); + } + } } \ No newline at end of file diff --git a/src/anm2/item.h b/src/anm2/item.h index cb3b20c..9bf0228 100644 --- a/src/anm2/item.h +++ b/src/anm2/item.h @@ -20,7 +20,9 @@ namespace anm2ed::anm2 std::string to_string(Type, int = -1); int length(Type); Frame frame_generate(float, Type); - void frames_change(anm2::FrameChange&, ChangeType, int, int = 0); + void frames_change(FrameChange&, ChangeType, int, int = 0); 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); }; } \ No newline at end of file diff --git a/src/anm2/layer.cpp b/src/anm2/layer.cpp index 4ca3106..5dbe7ba 100644 --- a/src/anm2/layer.cpp +++ b/src/anm2/layer.cpp @@ -36,4 +36,8 @@ namespace anm2ed::anm2 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.h b/src/anm2/layer.h index 715fe1b..0ebc1fc 100644 --- a/src/anm2/layer.h +++ b/src/anm2/layer.h @@ -6,6 +6,7 @@ namespace anm2ed::anm2 { constexpr auto LAYER_FORMAT = "#{} {} (Spritesheet: #{})"; + constexpr auto LAYER_NO_SPRITESHEET_FORMAT = "#{} {} (No Spritesheet)"; class Layer { @@ -18,5 +19,6 @@ namespace anm2ed::anm2 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/sound.cpp b/src/anm2/sound.cpp index eca9655..d6f9b8a 100644 --- a/src/anm2/sound.cpp +++ b/src/anm2/sound.cpp @@ -1,7 +1,6 @@ #include "sound.h" #include "filesystem_.h" -#include "string_.h" #include "xml_.h" using namespace anm2ed::resource; @@ -28,8 +27,8 @@ namespace anm2ed::anm2 Sound::Sound(const std::string& directory, const std::string& path) { filesystem::WorkingDirectory workingDirectory(directory); - this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string(); - this->path = string::backslash_replace_to_lower(this->path); + this->path = !path.empty() ? std::filesystem::relative(path) : this->path; + this->path = filesystem::path_lower_case_backslash_handle(this->path); audio = Audio(this->path.c_str()); } @@ -38,8 +37,8 @@ namespace anm2ed::anm2 if (!element) return; element->QueryIntAttribute("Id", &id); xml::query_path_attribute(element, "Path", &path); - string::backslash_replace_to_lower(this->path); - audio = Audio(this->path.c_str()); + path = filesystem::path_lower_case_backslash_handle(path); + audio = Audio(path.c_str()); } XMLElement* Sound::to_element(XMLDocument& document, int id) @@ -56,4 +55,19 @@ namespace anm2ed::anm2 document.InsertEndChild(to_element(document, id)); return xml::document_to_string(document); } + + void Sound::reload(const std::string& directory) + { + *this = Sound(directory, this->path); + } + + bool Sound::is_valid() + { + return audio.is_valid(); + } + + void Sound::play() + { + audio.play(); + } } \ No newline at end of file diff --git a/src/anm2/sound.h b/src/anm2/sound.h index fc1e43f..951af67 100644 --- a/src/anm2/sound.h +++ b/src/anm2/sound.h @@ -7,8 +7,7 @@ namespace anm2ed::anm2 { - constexpr auto SOUND_FORMAT = "#{} {}"; - constexpr auto SOUND_FORMAT_C = "#%d %s"; + constexpr auto SOUND_FORMAT = "{}"; class Sound { @@ -26,5 +25,8 @@ namespace anm2ed::anm2 Sound(const std::string&, const std::string&); tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); std::string to_string(int); + void reload(const std::string&); + bool is_valid(); + void play(); }; } \ No newline at end of file diff --git a/src/anm2/spritesheet.cpp b/src/anm2/spritesheet.cpp index 309c40e..7f0ce56 100644 --- a/src/anm2/spritesheet.cpp +++ b/src/anm2/spritesheet.cpp @@ -1,7 +1,6 @@ #include "spritesheet.h" #include "filesystem_.h" -#include "string_.h" #include "xml_.h" using namespace anm2ed::resource; @@ -18,15 +17,15 @@ namespace anm2ed::anm2 // Spritesheet paths from Isaac Rebirth are made with the assumption that paths are case-insensitive // However when using the resource dumper, the spritesheet paths are all lowercase (on Linux anyway) // This will handle this case and make the paths OS-agnostic - this->path = string::backslash_replace_to_lower(this->path); + path = filesystem::path_lower_case_backslash_handle(path); texture = Texture(path); } Spritesheet::Spritesheet(const std::string& directory, const std::string& path) { filesystem::WorkingDirectory workingDirectory(directory); - this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string(); - this->path = string::backslash_replace_to_lower(this->path); + this->path = !path.empty() ? std::filesystem::relative(path) : this->path; + this->path = filesystem::path_lower_case_backslash_handle(this->path); texture = Texture(this->path); } diff --git a/src/canvas.h b/src/canvas.h index b1955b8..8865d5a 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -12,7 +12,7 @@ namespace anm2ed::canvas constexpr auto PIVOT_SIZE = glm::vec2(8, 8); constexpr auto ZOOM_MIN = 1.0f; constexpr auto ZOOM_MAX = 2000.0f; - constexpr auto POSITION_FORMAT = "Position: ({:8} {:8})"; + constexpr auto POSITION_FORMAT = "Position: ({:8}, {:8})"; constexpr auto DASH_LENGTH = 4.0f; constexpr auto DASH_GAP = 1.0f; @@ -20,6 +20,11 @@ namespace anm2ed::canvas constexpr auto STEP = 1.0f; constexpr auto STEP_FAST = 5.0f; + + constexpr auto GRID_SIZE_MIN = 1; + constexpr auto GRID_SIZE_MAX = 10000; + constexpr auto GRID_OFFSET_MIN = 0; + constexpr auto GRID_OFFSET_MAX = 10000; } namespace anm2ed diff --git a/src/document.cpp b/src/document.cpp index a17f208..c602acb 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -1,13 +1,10 @@ #include "document.h" -#include -#include +#include #include "filesystem_.h" #include "log.h" -#include "map_.h" #include "toast.h" -#include "vector_.h" using namespace anm2ed::anm2; using namespace anm2ed::imgui; @@ -35,6 +32,43 @@ namespace anm2ed change(Document::ALL); } + Document::Document(Document&& other) noexcept + : path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current), + anm2(current.anm2), reference(current.reference), playback(current.playback), animation(current.animation), + merge(current.merge), event(current.event), layer(current.layer), null(current.null), sound(current.sound), + spritesheet(current.spritesheet), message(current.message), previewZoom(other.previewZoom), + previewPan(other.previewPan), editorPan(other.editorPan), editorZoom(other.editorZoom), + overlayIndex(other.overlayIndex), hoveredFrame(other.hoveredFrame), hash(other.hash), saveHash(other.saveHash), + autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isOpen(other.isOpen), + isForceDirty(other.isForceDirty), isAnimationPreviewSet(other.isAnimationPreviewSet), + isSpritesheetEditorSet(other.isSpritesheetEditorSet) + { + } + + Document& Document::operator=(Document&& other) noexcept + { + if (this != &other) + { + path = std::move(other.path); + snapshots = std::move(other.snapshots); + previewZoom = other.previewZoom; + previewPan = other.previewPan; + editorPan = other.editorPan; + editorZoom = other.editorZoom; + overlayIndex = other.overlayIndex; + hoveredFrame = other.hoveredFrame; + hash = other.hash; + saveHash = other.saveHash; + autosaveHash = other.autosaveHash; + lastAutosaveTime = other.lastAutosaveTime; + isOpen = other.isOpen; + isForceDirty = other.isForceDirty; + isAnimationPreviewSet = other.isAnimationPreviewSet; + isSpritesheetEditorSet = other.isSpritesheetEditorSet; + } + return *this; + } + bool Document::save(const std::string& path, std::string* errorString) { this->path = !path.empty() ? path : this->path.string(); @@ -51,18 +85,35 @@ namespace anm2ed return false; } - bool Document::autosave(const std::string& path, std::string* errorString) + std::filesystem::path Document::autosave_path_get() { - if (anm2.serialize(path, errorString)) + return directory_get() / std::string("." + filename_get().string() + ".autosave"); + } + + std::filesystem::path Document::path_from_autosave_get(std::filesystem::path& path) + { + auto fileName = path.filename().string(); + if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin()); + + auto restorePath = path.parent_path() / fileName; + restorePath.replace_extension(""); + + return path; + } + + bool Document::autosave(std::string* errorString) + { + auto autosavePath = autosave_path_get(); + if (anm2.serialize(autosavePath, errorString)) { autosaveHash = hash; lastAutosaveTime = 0.0f; toasts.info("Autosaving..."); - logger.info(std::format("Autosaved document to: {}", path)); + logger.info(std::format("Autosaved document to: {}", autosavePath.string())); return true; } else if (errorString) - toasts.warning(std::format("Could not autosave document to: {} ({})", path, *errorString)); + toasts.warning(std::format("Could not autosave document to: {} ({})", autosavePath.string(), *errorString)); return false; } @@ -84,41 +135,30 @@ namespace anm2ed { hash_set(); - auto layers_set = [&]() { unusedLayerIDs = anm2.layers_unused(); }; - auto nulls_set = [&]() { unusedNullIDs = anm2.nulls_unused(); }; + auto layers_set = [&]() { layer.unused = anm2.layers_unused(); }; + auto nulls_set = [&]() { null.unused = anm2.nulls_unused(); }; auto events_set = [&]() { - unusedEventIDs = anm2.events_unused(); - eventNames = anm2.event_names_get(); - for (auto& name : eventNames) - eventNamesCStr.push_back(name.c_str()); + event.unused = anm2.events_unused(); + event.labels_set(anm2.event_labels_get()); }; - auto animations_set = [&]() - { - animationNames = anm2.animation_names_get(); - animationNamesCStr.clear(); - animationNames.insert(animationNames.begin(), "None"); - for (auto& name : animationNames) - animationNamesCStr.push_back(name.c_str()); - }; + auto animations_set = [&]() { animation.labels_set(anm2.animation_labels_get()); }; auto spritesheets_set = [&]() { - unusedSpritesheetIDs = anm2.spritesheets_unused(); - spritesheetNames = anm2.spritesheet_names_get(); - spritesheetNamesCStr.clear(); - for (auto& name : spritesheetNames) - spritesheetNamesCStr.push_back(name.c_str()); + spritesheet.unused = anm2.spritesheets_unused(); + spritesheet.labels_set(anm2.spritesheet_labels_get()); }; auto sounds_set = [&]() { - unusedSoundIDs = anm2.sounds_unused(); - soundNames = anm2.sound_names_get(); - soundNamesCStr.clear(); - for (auto& name : soundNames) - soundNamesCStr.push_back(name.c_str()); + sound.unused = anm2.sounds_unused(); + sound.labels_set(anm2.sound_labels_get()); + + for (auto& animation : anm2.animations.items) + for (auto& trigger : animation.triggers.frames) + if (!anm2.content.sounds.contains(trigger.soundID)) trigger.soundID = -1; }; switch (type) @@ -132,23 +172,21 @@ namespace anm2ed case EVENTS: events_set(); break; - case ANIMATIONS: - animations_set(); - break; case SPRITESHEETS: spritesheets_set(); break; case SOUNDS: sounds_set(); break; - case ITEMS: + case ANIMATIONS: + animations_set(); break; case ALL: layers_set(); nulls_set(); events_set(); - animations_set(); spritesheets_set(); + animations_set(); sounds_set(); break; default: @@ -186,569 +224,56 @@ namespace anm2ed return anm2.frame_get(reference); } - void Document::frames_bake(int interval, bool isRoundScale, bool isRoundRotation) - { - snapshot("Bake Frames"); - anm2.bake(reference, interval, isRoundScale, isRoundRotation); - change(Document::FRAMES); - } - - void Document::frames_add(anm2::Item* item) - { - if (!item) return; - - auto frame = frame_get(); - - if (frame) - { - item->frames.insert(item->frames.begin() + reference.frameIndex, *frame); - reference.frameIndex++; - } - else if (!item->frames.empty()) - { - auto frame = item->frames.back(); - item->frames.emplace_back(frame); - reference.frameIndex = item->frames.size() - 1; - } - } - - void Document::frames_delete(anm2::Item* item) - { - if (!item) return; - - snapshot("Delete Frames"); - item->frames.erase(item->frames.begin() + reference.frameIndex); - reference.frameIndex = glm::max(-1, --reference.frameIndex); - change(Document::FRAMES); - } - - void Document::frame_crop_set(anm2::Frame* frame, vec2 crop) - { - if (!frame) return; - snapshot("Frame Crop"); - frame->crop = crop; - change(Document::FRAMES); - } - - void Document::frame_size_set(anm2::Frame* frame, vec2 size) - { - if (!frame) return; - snapshot("Frame Size"); - frame->size = size; - change(Document::FRAMES); - } - - void Document::frame_position_set(anm2::Frame* frame, vec2 position) - { - if (!frame) return; - snapshot("Frame Position"); - frame->position = position; - change(Document::FRAMES); - } - - void Document::frame_pivot_set(anm2::Frame* frame, vec2 pivot) - { - if (!frame) return; - snapshot("Frame Pivot"); - frame->pivot = pivot; - change(Document::FRAMES); - } - - void Document::frame_scale_set(anm2::Frame* frame, vec2 scale) - { - if (!frame) return; - snapshot("Frame Scale"); - frame->scale = scale; - change(Document::FRAMES); - } - - void Document::frame_rotation_set(anm2::Frame* frame, float rotation) - { - if (!frame) return; - snapshot("Frame Rotation"); - frame->rotation = rotation; - change(Document::FRAMES); - } - - void Document::frame_delay_set(anm2::Frame* frame, int delay) - { - if (!frame) return; - snapshot("Frame Delay"); - frame->delay = delay; - change(Document::FRAMES); - } - - void Document::frame_tint_set(anm2::Frame* frame, vec4 tint) - { - if (!frame) return; - snapshot("Frame Tint"); - frame->tint = tint; - change(Document::FRAMES); - } - - void Document::frame_color_offset_set(anm2::Frame* frame, vec3 colorOffset) - { - if (!frame) return; - snapshot("Frame Color Offset"); - frame->colorOffset = colorOffset; - change(Document::FRAMES); - } - - void Document::frame_is_visible_set(anm2::Frame* frame, bool isVisible) - { - if (!frame) return; - snapshot("Frame Visibility"); - frame->isVisible = isVisible; - change(Document::FRAMES); - } - - void Document::frame_is_interpolated_set(anm2::Frame* frame, bool isInterpolated) - { - if (!frame) return; - snapshot("Frame Interpolation"); - frame->isInterpolated = isInterpolated; - change(Document::FRAMES); - } - - void Document::frame_flip_x(anm2::Frame* frame) - { - if (!frame) return; - snapshot("Frame Flip X"); - frame->scale.x = -frame->scale.x; - change(Document::FRAMES); - } - - void Document::frame_flip_y(anm2::Frame* frame) - { - if (!frame) return; - snapshot("Frame Flip Y"); - frame->scale.y = -frame->scale.y; - change(Document::FRAMES); - } - - void Document::frame_shorten() - { - auto frame = frame_get(); - if (!frame) return; - snapshot("Shorten Frame"); - frame->shorten(); - change(Document::FRAMES); - } - - void Document::frame_extend() - { - auto frame = frame_get(); - if (!frame) return; - snapshot("Extend Frame"); - frame->extend(); - change(Document::FRAMES); - } - - void Document::frames_change(anm2::FrameChange& frameChange, anm2::ChangeType type, bool isFromSelectedFrame, - int numberFrames) - { - auto item = item_get(); - if (!item) return; - snapshot("Change All Frame Properties"); - item->frames_change(frameChange, type, isFromSelectedFrame && frame_get() ? reference.frameIndex : 0, - isFromSelectedFrame ? numberFrames : -1); - change(Document::FRAMES); - } - - void Document::frames_deserialize(const std::string& string) - { - if (auto item = item_get()) - { - snapshot("Paste Frame(s)"); - std::set indices{}; - std::string errorString{}; - auto start = reference.frameIndex + 1; - if (item->frames_deserialize(string, reference.itemType, start, indices, &errorString)) - change(Document::FRAMES); - else - toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); - } - else - toasts.error(std::format("Failed to deserialize frame(s): select an item first!")); - } - anm2::Item* Document::item_get() { return anm2.item_get(reference); } - anm2::Spritesheet* Document::spritesheet_get() - { - return anm2.spritesheet_get(referenceSpritesheet); - } - - void Document::spritesheet_add(const std::string& path) - { - int id{}; - snapshot("Add Spritesheet"); - if (anm2.spritesheet_add(directory_get(), path, id)) - { - spritesheetMultiSelect = {id}; - toasts.info(std::format("Initialized spritesheet #{}: {}", id, path)); - change(Document::SPRITESHEETS); - } - else - toasts.error(std::format("Failed to initialize spritesheet: {}", path)); - } - - void Document::spritesheets_deserialize(const std::string& string, merge::Type type) - { - snapshot("Paste Spritesheet(s)"); - std::string errorString{}; - if (anm2.content.spritesheets_deserialize(string, directory_get(), type, &errorString)) - change(Document::SPRITESHEETS); - else - toasts.error(std::format("Failed to deserialize spritesheet(s): {}", errorString)); - } - - void Document::layers_deserialize(const std::string& string, merge::Type type) - { - snapshot("Paste Layer(s)"); - std::string errorString{}; - if (anm2.content.layers_deserialize(string, type, &errorString)) - change(Document::NULLS); - else - toasts.error(std::format("Failed to deserialize layer(s): {}", errorString)); - } - - void Document::layer_set(anm2::Layer& layer) - { - if (referenceLayer > -1) - { - snapshot("Set Layer"); - anm2.content.layers[referenceLayer] = layer; - layersMultiSelect = {referenceLayer}; - } - else - { - snapshot("Add Layer"); - auto id = map::next_id_get(anm2.content.layers); - anm2.content.layers[id] = layer; - layersMultiSelect = {id}; - } - change(Document::LAYERS); - } - - void Document::layers_remove_unused() - { - snapshot("Remove Unused Layers"); - for (auto& id : unusedLayerIDs) - anm2.content.layers.erase(id); - change(Document::LAYERS); - unusedLayerIDs.clear(); - } - - void Document::null_set(anm2::Null& null) - { - if (referenceNull > -1) - { - snapshot("Set Null"); - anm2.content.nulls[referenceNull] = null; - nullMultiSelect = {referenceNull}; - } - else - { - snapshot("Add Null"); - auto id = map::next_id_get(anm2.content.nulls); - anm2.content.nulls[id] = null; - nullMultiSelect = {id}; - } - change(Document::NULLS); - } - - void Document::null_rect_toggle(anm2::Null& null) - { - snapshot("Null Rect"); - null.isShowRect = !null.isShowRect; - change(Document::NULLS); - } - - void Document::nulls_remove_unused() - { - snapshot("Remove Unused Nulls"); - for (auto& id : unusedNullIDs) - anm2.content.nulls.erase(id); - change(Document::NULLS); - unusedNullIDs.clear(); - } - - void Document::nulls_deserialize(const std::string& string, merge::Type type) - { - snapshot("Paste Null(s)"); - std::string errorString{}; - if (anm2.content.nulls_deserialize(string, type, &errorString)) - change(Document::NULLS); - else - toasts.error(std::format("Failed to deserialize null(s): {}", errorString)); - } - - void Document::event_set(anm2::Event& event) - { - if (referenceEvent > -1) - { - snapshot("Set Event"); - anm2.content.events[referenceEvent] = event; - eventMultiSelect = {referenceEvent}; - } - else - { - snapshot("Add Event"); - auto id = map::next_id_get(anm2.content.events); - anm2.content.events[id] = event; - eventMultiSelect = {id}; - } - change(Document::EVENTS); - } - - void Document::events_remove_unused() - { - snapshot("Remove Unused Events"); - for (auto& id : unusedEventIDs) - anm2.content.events.erase(id); - change(Document::EVENTS); - unusedEventIDs.clear(); - } - - void Document::events_deserialize(const std::string& string, merge::Type type) - { - snapshot("Paste Event(s)"); - std::string errorString{}; - if (anm2.content.events_deserialize(string, type, &errorString)) - change(Document::EVENTS); - else - toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); - } - - void Document::sound_add(const std::string& path) - { - int id{}; - snapshot("Add Sound"); - if (anm2.sound_add(directory_get(), path, id)) - { - soundMultiSelect = {id}; - toasts.info(std::format("Initialized sound #{}: {}", id, path)); - change(Document::SOUNDS); - } - else - toasts.error(std::format("Failed to initialize sound: {}", path)); - } - - void Document::sounds_remove_unused() - { - snapshot("Remove Unused Sounds"); - for (auto& id : unusedSoundIDs) - anm2.content.sounds.erase(id); - change(Document::LAYERS); - unusedSoundIDs.clear(); - } - - void Document::sounds_deserialize(const std::string& string, merge::Type type) - { - snapshot("Paste Sound(s)"); - std::string errorString{}; - if (anm2.content.sounds_deserialize(string, directory_get(), type, &errorString)) - change(Document::EVENTS); - else - toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); - } - - void Document::item_add(anm2::Type type, int id, std::string& name, locale::Type locale, int spritesheetID) - { - snapshot("Add Item"); - - anm2::Reference addReference; - - if (type == anm2::LAYER) - addReference = - anm2.layer_add({reference.animationIndex, anm2::LAYER, id}, name, spritesheetID, (locale::Type)locale); - else if (type == anm2::NULL_) - addReference = anm2.null_add({reference.animationIndex, anm2::LAYER, id}, name, (locale::Type)locale); - - reference = addReference; - - change(Document::ITEMS); - } - - void Document::item_remove(anm2::Animation* animation) - { - if (!animation) return; - snapshot("Remove Item"); - animation->item_remove(reference.itemType, reference.itemID); - reference = {reference.animationIndex}; - change(Document::ITEMS); - } - - void Document::item_visible_toggle(anm2::Item* item) - { - if (!item) return; - snapshot("Item Visibility"); - item->isVisible = !item->isVisible; - change(Document::ITEMS); - } - anm2::Animation* Document::animation_get() { return anm2.animation_get(reference); } - void Document::animation_set(int index) + anm2::Spritesheet* Document::spritesheet_get() { - snapshot("Select Animation"); - reference = {index}; - change(Document::ITEMS); + return anm2.spritesheet_get(spritesheet.reference); } - void Document::animation_add() + void Document::spritesheet_add(const std::string& path) { - snapshot("Add Animation"); - anm2::Animation animation; - if (anm2::Animation* referenceAnimation = animation_get()) + auto add = [&]() { - 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()); + int id{}; + if (anm2.spritesheet_add(directory_get(), path, id)) + { + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; + this->spritesheet.selection = {id}; + toasts.info(std::format("Initialized spritesheet #{}: {}", id, spritesheet.path.string())); + } + else + toasts.error(std::format("Failed to initialize spritesheet: {}", path)); + }; - auto index = - animationMultiSelect.empty() ? (int)anm2.animations.items.size() - 1 : *animationMultiSelect.rbegin() + 1; - anm2.animations.items.insert(anm2.animations.items.begin() + index, animation); - animationMultiSelect = {index}; - reference = {index}; - change(Document::ANIMATIONS); - } - - void Document::animation_duplicate() - { - snapshot("Duplicate Animation(s)"); - auto duplicated = animationMultiSelect; - auto duplicatedEnd = std::ranges::max(duplicated); - for (auto& id : duplicated) - { - anm2.animations.items.insert(anm2.animations.items.begin() + duplicatedEnd, anm2.animations.items[id]); - animationMultiSelect.insert(++duplicatedEnd); - animationMultiSelect.erase(id); - } - change(Document::ANIMATIONS); - } - - void Document::animations_move(std::vector& indices, int index) - { - snapshot("Move Animation(s)"); - animationMultiSelect = vector::move_indices(anm2.animations.items, indices, index); - change(Document::ANIMATIONS); - } - - void Document::animations_remove() - { - snapshot("Remove Animation(s)"); - - if (!animationMultiSelect.empty()) - { - for (auto& i : animationMultiSelect | std::views::reverse) - anm2.animations.items.erase(anm2.animations.items.begin() + i); - animationMultiSelect.clear(); - } - else if (hoveredAnimation > -1) - { - anm2.animations.items.erase(anm2.animations.items.begin() + hoveredAnimation); - hoveredAnimation = -1; - } - - change(Document::ANIMATIONS); - } - - void Document::animation_default() - { - snapshot("Default Animation"); - anm2.animations.defaultAnimation = anm2.animations.items[*animationMultiSelect.begin()].name; - change(Document::ANIMATIONS); - } - - void Document::animations_deserialize(const std::string& string) - { - snapshot("Paste Animation(s)"); - auto& multiSelect = animationMultiSelect; - auto start = multiSelect.empty() ? anm2.animations.items.size() : *multiSelect.rbegin() + 1; - std::set indices{}; - std::string errorString{}; - if (anm2.animations.animations_deserialize(string, start, indices, &errorString)) - { - multiSelect = indices; - change(Document::ANIMATIONS); - } - else - toasts.error(std::format("Failed to deserialize animation(s): {}", errorString)); - } - - void Document::generate_animation_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, - int delay) - { - snapshot("Generate Animation from Grid"); - - anm2.generate_from_grid(reference, startPosition, size, pivot, columns, count, delay); - - if (auto animation = animation_get()) animation->frameNum = animation->length(); - - change(Document::ALL); - } - - void Document::animations_merge_quick() - { - snapshot("Merge Animation(s)"); - int merged{}; - if (animationMultiSelect.size() > 1) - merged = anm2.animations.merge(*animationMultiSelect.begin(), animationMultiSelect); - else if (animationMultiSelect.size() == 1 && *animationMultiSelect.begin() != (int)anm2.animations.items.size() - 1) - { - auto start = *animationMultiSelect.begin(); - auto next = *animationMultiSelect.begin() + 1; - std::set animationSet{}; - animationSet.insert(start); - animationSet.insert(next); - - merged = anm2.animations.merge(start, animationSet); - } - else - return; - - animationMultiSelect = {merged}; - reference = {merged}; - change(Document::ANIMATIONS); - } - - void Document::animations_merge(merge::Type type, bool isDeleteAnimationsAfter) - { - snapshot("Merge Animations"); - auto merged = anm2.animations.merge(mergeTarget, animationMergeMultiSelect, type, isDeleteAnimationsAfter); - animationMultiSelect = {merged}; - reference = {merged}; - change(Document::ANIMATIONS); + DOCUMENT_EDIT_PTR(this, "Add Spritesheet", Document::SPRITESHEETS, add()); } void Document::snapshot(const std::string& message) { - snapshots.push(anm2, reference, message); + this->message = message; + snapshots.push(current); } void Document::undo() { - snapshots.undo(anm2, reference, message); + snapshots.undo(); toasts.info(std::format("Undo: {}", message)); change(Document::ALL); } void Document::redo() { + snapshots.redo(); toasts.info(std::format("Redo: {}", message)); - snapshots.redo(anm2, reference, message); change(Document::ALL); } @@ -761,4 +286,5 @@ namespace anm2ed { return !snapshots.redoStack.is_empty(); } -} \ No newline at end of file + +} diff --git a/src/document.h b/src/document.h index 35ae5ef..545d153 100644 --- a/src/document.h +++ b/src/document.h @@ -1,18 +1,14 @@ #pragma once #include -#include -#include "anm2/anm2.h" -#include "imgui_.h" -#include "playback.h" #include "snapshots.h" -#include "types.h" #include namespace anm2ed { + class Document { public: @@ -31,58 +27,29 @@ namespace anm2ed }; std::filesystem::path path{}; - anm2::Anm2 anm2{}; - std::string message{}; - Playback playback{}; + Snapshots snapshots{}; + Snapshot& current = snapshots.current; + + anm2::Anm2& anm2 = current.anm2; + anm2::Reference& reference = current.reference; + Playback& playback = current.playback; + Storage& animation = current.animation; + Storage& merge = current.merge; + Storage& event = current.event; + Storage& layer = current.layer; + Storage& null = current.null; + Storage& sound = current.sound; + Storage& spritesheet = current.spritesheet; + std::string& message = current.message; float previewZoom{200}; glm::vec2 previewPan{}; glm::vec2 editorPan{}; float editorZoom{200}; + int overlayIndex{-1}; - int overlayIndex{}; - - anm2::Reference reference{}; - int hoveredAnimation{-1}; - int mergeTarget{-1}; - imgui::MultiSelectStorage animationMultiSelect; - imgui::MultiSelectStorage animationMergeMultiSelect; - std::vector animationNamesCStr{}; - std::vector animationNames{}; - - anm2::Reference hoveredFrame{anm2::REFERENCE_DEFAULT}; - - int referenceSpritesheet{-1}; - int hoveredSpritesheet{-1}; - std::set unusedSpritesheetIDs{}; - std::vector spritesheetNames{}; - std::vector spritesheetNamesCStr{}; - imgui::MultiSelectStorage spritesheetMultiSelect; - - int referenceLayer{-1}; - int hoveredLayer{-1}; - std::set unusedLayerIDs{}; - imgui::MultiSelectStorage layersMultiSelect; - - int referenceNull{-1}; - int hoveredNull{-1}; - std::set unusedNullIDs{}; - imgui::MultiSelectStorage nullMultiSelect; - - int referenceEvent{-1}; - int hoveredEvent{-1}; - std::set unusedEventIDs{}; - imgui::MultiSelectStorage eventMultiSelect; - std::vector eventNamesCStr{}; - std::vector eventNames{}; - - int referenceSound{-1}; - int hoveredSound{-1}; - std::set unusedSoundIDs{}; - imgui::MultiSelectStorage soundMultiSelect; - std::vector soundNamesCStr{}; - std::vector soundNames{}; + anm2::Reference hoveredFrame{}; uint64_t hash{}; uint64_t saveHash{}; @@ -90,10 +57,15 @@ namespace anm2ed double lastAutosaveTime{}; bool isOpen{true}; bool isForceDirty{false}; + bool isAnimationPreviewSet{false}; + bool isSpritesheetEditorSet{false}; Document(const std::string&, 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::string& = {}, std::string* = nullptr); - bool autosave(const std::string&, std::string* = nullptr); void hash_set(); void clean(); void on_change(); @@ -105,83 +77,35 @@ namespace anm2ed bool is_valid(); anm2::Frame* frame_get(); - void frames_add(anm2::Item* item); - void frames_delete(anm2::Item* item); - void frames_bake(int, bool, bool); - void frame_crop_set(anm2::Frame*, glm::vec2); - void frame_size_set(anm2::Frame*, glm::vec2); - void frame_position_set(anm2::Frame*, glm::vec2); - void frame_pivot_set(anm2::Frame*, glm::vec2); - void frame_scale_set(anm2::Frame*, glm::vec2); - void frame_rotation_set(anm2::Frame*, float); - void frame_delay_set(anm2::Frame*, int); - void frame_tint_set(anm2::Frame*, glm::vec4); - void frame_color_offset_set(anm2::Frame*, glm::vec3); - void frame_is_visible_set(anm2::Frame*, bool); - void frame_is_interpolated_set(anm2::Frame*, bool); - void frame_flip_x(anm2::Frame* frame); - void frame_flip_y(anm2::Frame* frame); - void frame_shorten(); - void frame_extend(); - void frames_change(anm2::FrameChange&, anm2::ChangeType, bool, int = -1); - void frames_deserialize(const std::string&); - anm2::Item* item_get(); - void item_add(anm2::Type, int, std::string&, types::locale::Type, int); - void item_remove(anm2::Animation*); - void item_visible_toggle(anm2::Item*); - anm2::Spritesheet* spritesheet_get(); - void spritesheet_add(const std::string&); - void spritesheets_deserialize(const std::string&, types::merge::Type); - - void layer_set(anm2::Layer&); - void layers_remove_unused(); - void layers_deserialize(const std::string&, types::merge::Type); - - void null_set(anm2::Null&); - void null_rect_toggle(anm2::Null&); - void nulls_remove_unused(); - void nulls_deserialize(const std::string&, types::merge::Type); - - void event_set(anm2::Event&); - void events_remove_unused(); - void events_deserialize(const std::string&, types::merge::Type); - - void sound_add(const std::string& path); - void sounds_remove_unused(); - void sounds_deserialize(const std::string& string, types::merge::Type); - - void animation_add(); - void animation_set(int); - void animation_duplicate(); - void animation_default(); - void animations_remove(); - void animations_move(std::vector&, int); - void animations_merge(types::merge::Type, bool); - void animations_merge_quick(); anm2::Animation* animation_get(); - void animations_deserialize(const std::string& string); - void generate_animation_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); + void spritesheet_add(const std::string&); + + bool autosave(std::string* = nullptr); + std::filesystem::path autosave_path_get(); + std::filesystem::path path_from_autosave_get(std::filesystem::path&); void snapshot(const std::string& message); void undo(); void redo(); - bool is_able_to_undo(); bool is_able_to_redo(); }; -#define DOCUMENT_SNAPSHOT(document, message) document.snapshot(message); -#define DOCUMENT_CHANGE(document, changeType) document.change(changeType); - #define DOCUMENT_EDIT(document, message, changeType, body) \ { \ - \ - DOCUMENT_SNAPSHOT(document, message) \ + document.snapshot(message); \ body; \ - DOCUMENT_CHANGE(document, changeType) \ + document.change(changeType); \ + } + +#define DOCUMENT_EDIT_PTR(document, message, changeType, body) \ + { \ + document->snapshot(message); \ + body; \ + document->change(changeType); \ } } diff --git a/src/imgui/documents.h b/src/imgui/documents.h index 0011f8b..7e9c039 100644 --- a/src/imgui/documents.h +++ b/src/imgui/documents.h @@ -1,6 +1,5 @@ #pragma once -#include "imgui.h" #include "manager.h" #include "resources.h" #include "settings.h" diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index ebcee70..0123551 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -28,17 +28,13 @@ namespace anm2ed::imgui return ImGui::InputText(label, string->data(), string->capacity() + 1, flags, input_text_callback, string); } - bool combo_strings(const std::string& label, int* index, std::vector& strings) + bool combo_negative_one_indexed(const std::string& label, int* index, std::vector& strings) { - std::vector items{}; - for (auto& string : strings) - items.push_back(string.c_str()); - return ImGui::Combo(label.c_str(), index, items.data(), (int)items.size()); - } + *index += 1; + bool isActivated = ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size()); + *index -= 1; - bool combo_strings(const std::string& label, int* index, std::vector& strings) - { - return ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size()); + return isActivated; } bool input_int_range(const char* label, int& value, int min, int max, int step, int stepFast, @@ -49,6 +45,21 @@ namespace anm2ed::imgui 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; + } + bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected, ImGuiSelectableFlags flags, bool* isRenamed) { @@ -263,7 +274,8 @@ namespace anm2ed::imgui { internal.UserData = this; - auto io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, this->size(), size); + auto io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect2d, + this->size(), size); internal.ApplyRequests(io); } diff --git a/src/imgui/imgui_.h b/src/imgui/imgui_.h index 0555b4f..4ff685d 100644 --- a/src/imgui/imgui_.h +++ b/src/imgui/imgui_.h @@ -46,108 +46,104 @@ namespace anm2ed::imgui #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}, + 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}, + {"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}, + {"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}, - {"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}, + {"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}, + {"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}, + {"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}, - - {"MouseLeft", ImGuiKey_MouseLeft}, - {"MouseRight", ImGuiKey_MouseRight}, - {"MouseMiddle", ImGuiKey_MouseMiddle}, - {"MouseX1", ImGuiKey_MouseX1}, - {"MouseX2", ImGuiKey_MouseX2}}; + {"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}, + }; const std::unordered_map MOD_MAP = { {"Ctrl", ImGuiMod_Ctrl}, @@ -167,15 +163,17 @@ namespace anm2ed::imgui int input_text_callback(ImGuiInputTextCallbackData*); bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0); bool input_int_range(const char*, int&, int, int, int = STEP, int = STEP_FAST, ImGuiInputTextFlags = 0); - bool combo_strings(const std::string&, int*, std::vector&); - bool combo_strings(const std::string&, int*, std::vector&); + bool 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); + bool combo_negative_one_indexed(const std::string&, int*, std::vector&); bool selectable_input_text(const std::string&, const std::string&, std::string&, bool = false, ImGuiSelectableFlags = 0, bool* = nullptr); void set_item_tooltip_shortcut(const char*, const std::string& = {}); void external_storage_set(ImGuiSelectionExternalStorage*, int, bool); ImVec2 icon_size_get(); bool chord_held(ImGuiKeyChord); - bool chord_repeating(ImGuiKeyChord, float = 0.125f, float = 0.025f); + bool chord_repeating(ImGuiKeyChord, float = ImGui::GetIO().KeyRepeatDelay, float = ImGui::GetIO().KeyRepeatRate); bool shortcut(std::string, types::shortcut::Type = types::shortcut::FOCUSED_SET); class MultiSelectStorage : public std::set diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index e04b0fe..8d263ae 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -23,6 +23,9 @@ namespace anm2ed::imgui void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) { auto document = manager.get(); + auto reference = document ? &document->reference : nullptr; + auto animation = document ? document->animation_get() : nullptr; + auto item = document ? document->item_get() : nullptr; if (ImGui::BeginMainMenuBar()) { @@ -30,43 +33,31 @@ namespace anm2ed::imgui if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New", settings.shortcutNew.c_str())) dialog.file_open(dialog::ANM2_NEW); - + if (ImGui::MenuItem("New", settings.shortcutNew.c_str())) dialog.file_save(dialog::ANM2_NEW); if (ImGui::MenuItem("Open", settings.shortcutOpen.c_str())) dialog.file_open(dialog::ANM2_NEW); - if (manager.recentFiles.empty()) + if (ImGui::BeginMenu("Open Recent", !manager.recentFiles.empty())) { - ImGui::BeginDisabled(); - ImGui::MenuItem("Open Recent"); - ImGui::EndDisabled(); - } - else - { - if (ImGui::BeginMenu("Open Recent")) + for (auto [i, file] : std::views::enumerate(manager.recentFiles)) { - for (auto [i, file] : std::views::enumerate(manager.recentFiles)) - { - auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string()); + auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string()); - ImGui::PushID(i); - if (ImGui::MenuItem(label.c_str())) manager.open(file); - ImGui::PopID(); - } - - if (!manager.recentFiles.empty()) - if (ImGui::MenuItem("Clear List")) manager.recent_files_clear(); - - ImGui::EndMenu(); + ImGui::PushID(i); + if (ImGui::MenuItem(label.c_str())) manager.open(file); + ImGui::PopID(); } + + if (!manager.recentFiles.empty()) + if (ImGui::MenuItem("Clear List")) manager.recent_files_clear(); + + ImGui::EndMenu(); } - ImGui::BeginDisabled(!document); - { - if (ImGui::MenuItem("Save", settings.shortcutSave.c_str())) manager.save(); - if (ImGui::MenuItem("Save As", settings.shortcutSaveAs.c_str())) dialog.file_save(dialog::ANM2_SAVE); - if (ImGui::MenuItem("Explore XML Location")) dialog.file_explorer_open(document->directory_get()); - } - ImGui::EndDisabled(); + if (ImGui::MenuItem("Save", settings.shortcutSave.c_str(), false, document)) manager.save(); + if (ImGui::MenuItem("Save As", settings.shortcutSaveAs.c_str(), false, document)) + dialog.file_save(dialog::ANM2_SAVE); + if (ImGui::MenuItem("Explore XML Location", nullptr, false, document)) + dialog.file_explorer_open(document->directory_get()); ImGui::Separator(); if (ImGui::MenuItem("Exit", settings.shortcutExit.c_str())) isQuitting = true; @@ -92,16 +83,12 @@ namespace anm2ed::imgui if (ImGui::BeginMenu("Wizard")) { - auto animation = document ? document->animation_get() : nullptr; - auto item = document ? document->item_get() : nullptr; ImGui::BeginDisabled(!item || document->reference.itemType != anm2::LAYER); if (ImGui::MenuItem("Generate Animation From Grid")) generatePopup.open(); if (ImGui::MenuItem("Change All Frame Properties")) changePopup.open(); ImGui::EndDisabled(); ImGui::Separator(); - ImGui::BeginDisabled(!animation); - if (ImGui::MenuItem("Render Animation")) renderPopup.open(); - ImGui::EndDisabled(); + if (ImGui::MenuItem("Render Animation", nullptr, false, animation)) renderPopup.open(); ImGui::EndMenu(); } @@ -158,20 +145,19 @@ namespace anm2ed::imgui auto& zoom = settings.generateZoom; auto& zoomStep = settings.viewZoomStep; - auto childSize = ImVec2(imgui::row_widget_width_get(2), imgui::size_without_footer_get().y); + auto childSize = ImVec2(row_widget_width_get(2), size_without_footer_get().y); if (ImGui::BeginChild("##Options Child", childSize, ImGuiChildFlags_Borders)) { ImGui::InputInt2("Start Position", value_ptr(startPosition)); ImGui::InputInt2("Frame Size", value_ptr(size)); ImGui::InputInt2("Pivot", value_ptr(pivot)); - ImGui::InputInt("Rows", &rows, imgui::STEP, imgui::STEP_FAST); - ImGui::InputInt("Columns", &columns, imgui::STEP, imgui::STEP_FAST); + ImGui::InputInt("Rows", &rows, STEP, STEP_FAST); + ImGui::InputInt("Columns", &columns, STEP, STEP_FAST); - ImGui::InputInt("Count", &count, imgui::STEP, imgui::STEP_FAST); - count = glm::min(count, rows * columns); + input_int_range("Count", count, anm2::FRAME_NUM_MIN, rows * columns); - ImGui::InputInt("Delay", &delay, imgui::STEP, imgui::STEP_FAST); + ImGui::InputInt("Delay", &delay, STEP, STEP_FAST); } ImGui::EndChild(); @@ -183,7 +169,7 @@ namespace anm2ed::imgui auto& time = generateTime; auto& shaderTexture = resources.shaders[resource::shader::TEXTURE]; - auto previewSize = ImVec2(ImGui::GetContentRegionAvail().x, imgui::size_without_footer_get(2).y); + auto previewSize = ImVec2(ImGui::GetContentRegionAvail().x, size_without_footer_get(2).y); generate.size_set(to_vec2(previewSize)); generate.bind(); @@ -196,7 +182,7 @@ namespace anm2ed::imgui .spritesheets[document->anm2.content.layers[document->reference.itemID].spritesheetID] .texture; - auto index = std::clamp((int)(time * count - 1), 0, count - 1); + 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); @@ -222,11 +208,18 @@ namespace anm2ed::imgui ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button("Generate", widgetSize)) { - document->generate_animation_from_grid(startPosition, size, pivot, columns, count, delay); + auto generate_from_grid = [&]() + { + item->frames_generate_from_grid(startPosition, size, pivot, columns, count, delay); + animation->frameNum = animation->length(); + }; + + DOCUMENT_EDIT_PTR(document, "Generate Animation from Grid", Document::FRAMES, generate_from_grid()); + generatePopup.close(); } @@ -267,59 +260,36 @@ namespace anm2ed::imgui auto& isFromSelectedFrame = settings.changeIsFromSelectedFrame; auto& numberFrames = settings.changeNumberFrames; - auto propertiesSize = imgui::child_size_get(10); + auto propertiesSize = child_size_get(10); if (ImGui::BeginChild("##Properties", propertiesSize, ImGuiChildFlags_Borders)) { - auto start = [&](const char* checkboxLabel, bool& isEnabled) - { - ImGui::Checkbox(checkboxLabel, &isEnabled); - ImGui::SameLine(); - ImGui::BeginDisabled(!isEnabled); - }; - auto end = [&]() { ImGui::EndDisabled(); }; +#define PROPERTIES_WIDGET(body) \ + ImGui::Checkbox(checkboxLabel, &isEnabled); \ + ImGui::SameLine(); \ + ImGui::BeginDisabled(!isEnabled); \ + body; \ + ImGui::EndDisabled(); auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value) - { - start(checkboxLabel, isEnabled); - ImGui::Checkbox(valueLabel, &value); - end(); - }; + { PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value)); }; auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value) - { - start(checkboxLabel, isEnabled); - ImGui::ColorEdit3(valueLabel, value_ptr(value)); - end(); - }; + { PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value))); }; auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value) - { - start(checkboxLabel, isEnabled); - ImGui::ColorEdit4(valueLabel, value_ptr(value)); - end(); - }; + { PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value))); }; auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value) - { - start(checkboxLabel, isEnabled); - ImGui::InputFloat2(valueLabel, value_ptr(value), math::vec2_format_get(value)); - end(); - }; + { PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), math::vec2_format_get(value))); }; auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) - { - start(checkboxLabel, isEnabled); - ImGui::InputFloat(valueLabel, &value, imgui::STEP, imgui::STEP_FAST, math::float_format_get(value)); - end(); - }; + { PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, math::float_format_get(value))); }; auto int_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) - { - start(checkboxLabel, isEnabled); - ImGui::InputInt(valueLabel, &value, imgui::STEP, imgui::STEP_FAST); - end(); - }; + { PROPERTIES_WIDGET(ImGui::InputInt(valueLabel, &value, STEP, STEP_FAST)); }; + +#undef PROPERTIES_WIDGET float2_value("##Is Crop", "Crop", isCrop, crop); float2_value("##Is Size", "Size", isSize, size); @@ -336,67 +306,53 @@ namespace anm2ed::imgui } ImGui::EndChild(); - auto settingsSize = imgui::child_size_get(2); + auto settingsSize = child_size_get(2); if (ImGui::BeginChild("##Settings", settingsSize, ImGuiChildFlags_Borders)) { ImGui::Checkbox("From Selected Frame", &isFromSelectedFrame); ImGui::SetItemTooltip("The frames after the currently referenced frame will be changed with these values.\nIf" - "off, will use all frames."); + " off, will use all frames."); ImGui::BeginDisabled(!isFromSelectedFrame); - ImGui::InputInt("Number of Frames", &numberFrames, imgui::STEP, imgui::STEP_FAST); - numberFrames = glm::clamp(numberFrames, anm2::FRAME_NUM_MIN, - (int)document->item_get()->frames.size() - document->reference.frameIndex); + input_int_range("Number of Frames", numberFrames, anm2::FRAME_NUM_MIN, + item->frames.size() - reference->frameIndex); ImGui::SetItemTooltip("Set the number of frames that will be changed."); ImGui::EndDisabled(); } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(4); + auto widgetSize = widget_size_with_row_get(4); auto frame_change = [&](anm2::ChangeType type) { anm2::FrameChange frameChange; - frameChange.crop = isCrop ? std::make_optional(crop) : std::nullopt; - frameChange.size = isSize ? std::make_optional(size) : std::nullopt; - frameChange.position = isPosition ? std::make_optional(position) : std::nullopt; - frameChange.pivot = isPivot ? std::make_optional(pivot) : std::nullopt; - frameChange.scale = isScale ? std::make_optional(scale) : std::nullopt; - frameChange.rotation = isRotation ? std::make_optional(rotation) : std::nullopt; - frameChange.delay = isDelay ? std::make_optional(delay) : std::nullopt; - frameChange.tint = isTint ? std::make_optional(tint) : std::nullopt; - frameChange.colorOffset = isColorOffset ? std::make_optional(colorOffset) : std::nullopt; - frameChange.isVisible = isVisibleSet ? std::make_optional(isVisible) : std::nullopt; - frameChange.isInterpolated = isInterpolatedSet ? std::make_optional(isInterpolated) : std::nullopt; + if (isCrop) frameChange.crop = std::make_optional(crop); + if (isSize) frameChange.size = std::make_optional(size); + if (isPosition) frameChange.position = std::make_optional(position); + if (isPivot) frameChange.pivot = std::make_optional(pivot); + if (isScale) frameChange.scale = std::make_optional(scale); + if (isRotation) frameChange.rotation = std::make_optional(rotation); + if (isDelay) frameChange.delay = std::make_optional(delay); + if (isTint) frameChange.tint = std::make_optional(tint); + if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset); + if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible); + if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated); - document->frames_change(frameChange, type, isFromSelectedFrame, numberFrames); + DOCUMENT_EDIT_PTR(document, "Change Frame Properties", Document::FRAMES, + item->frames_change(frameChange, type, + isFromSelectedFrame && document->frame_get() ? reference->frameIndex : 0, + isFromSelectedFrame ? numberFrames : -1)); + + changePopup.close(); }; - if (ImGui::Button("Add", widgetSize)) - { - frame_change(anm2::ADD); - changePopup.close(); - } - + if (ImGui::Button("Add", widgetSize)) frame_change(anm2::ADD); ImGui::SameLine(); - - if (ImGui::Button("Subtract", widgetSize)) - { - frame_change(anm2::SUBTRACT); - changePopup.close(); - } - + if (ImGui::Button("Subtract", widgetSize)) frame_change(anm2::SUBTRACT); ImGui::SameLine(); - - if (ImGui::Button("Adjust", widgetSize)) - { - frame_change(anm2::ADJUST); - changePopup.close(); - } - + if (ImGui::Button("Adjust", widgetSize)) frame_change(anm2::ADJUST); ImGui::SameLine(); - if (ImGui::Button("Cancel", widgetSize)) changePopup.close(); ImGui::EndPopup(); @@ -406,7 +362,7 @@ namespace anm2ed::imgui if (ImGui::BeginPopupModal(configurePopup.label, &configurePopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto childSize = imgui::size_without_footer_get(2); + auto childSize = size_without_footer_get(2); if (ImGui::BeginTabBar("##Configure Tabs")) { @@ -420,23 +376,32 @@ namespace anm2ed::imgui ImGui::SetItemTooltip("Enables autosaving of documents."); ImGui::BeginDisabled(!editSettings.fileIsAutosave); - ImGui::InputInt("Autosave Time (minutes", &editSettings.fileAutosaveTime, imgui::STEP, imgui::STEP_FAST); - editSettings.fileAutosaveTime = glm::clamp(editSettings.fileAutosaveTime, 0, 10); + input_int_range("Autosave Time (minutes)", editSettings.fileAutosaveTime, 0, 10); ImGui::SetItemTooltip("If changed, will autosave documents using this interval."); ImGui::EndDisabled(); - ImGui::SeparatorText("View"); + ImGui::SeparatorText("Keyboard"); - ImGui::InputFloat("Display Scale", &editSettings.displayScale, 0.25f, 0.25f, "%.2f"); - ImGui::SetItemTooltip("Change the scale of the display."); - editSettings.displayScale = glm::clamp(editSettings.displayScale, 0.5f, 2.0f); + input_float_range("Repeat Delay (seconds)", editSettings.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f, 0.05f, + "%.2f"); + ImGui::SetItemTooltip("Set how often, after repeating begins, key inputs will be fired."); - ImGui::InputFloat("Zoom Step", &editSettings.viewZoomStep, 10.0f, 10.0f, "%.2f"); - ImGui::SetItemTooltip("When zooming in/out with mouse or shortcut, this value will be used."); - editSettings.viewZoomStep = glm::clamp(editSettings.viewZoomStep, 1.0f, 250.0f); + input_float_range("Repeat Rate (seconds)", editSettings.keyboardRepeatRate, 0.005f, 1.0f, 0.005f, 0.005f, + "%.3f"); + ImGui::SetItemTooltip("Set how often, after repeating begins, key inputs will be fired."); + + ImGui::SeparatorText("UI"); + + input_float_range("UI Scale", editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f"); + ImGui::SetItemTooltip("Change the scale of the UI."); ImGui::Checkbox("Vsync", &editSettings.isVsync); ImGui::SetItemTooltip("Toggle vertical sync; synchronizes program update rate with monitor refresh rate."); + + ImGui::SeparatorText("View"); + + input_float_range("Zoom Step", editSettings.viewZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, "%.0f"); + ImGui::SetItemTooltip("When zooming in/out with mouse or shortcut, this value will be used."); } ImGui::EndChild(); @@ -445,12 +410,10 @@ namespace anm2ed::imgui if (ImGui::BeginTabItem("Shortcuts")) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (ImGui::BeginChild("##Tab Child", childSize, true)) { - if (ImGui::BeginTable("Shortcuts", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupScrollFreeze(0, 1); @@ -485,12 +448,12 @@ namespace anm2ed::imgui if (ImGui::IsKeyDown(ImGuiMod_Alt)) chord |= ImGuiMod_Alt; if (ImGui::IsKeyDown(ImGuiMod_Super)) chord |= ImGuiMod_Super; - for (auto& key : imgui::KEY_MAP | std::views::values) + for (auto& key : KEY_MAP | std::views::values) { if (ImGui::IsKeyPressed(key)) { chord |= key; - *settingString = imgui::chord_to_string(chord); + *settingString = chord_to_string(chord); selectedShortcut = -1; break; } @@ -510,7 +473,7 @@ namespace anm2ed::imgui ImGui::EndTabBar(); } - auto widgetSize = imgui::widget_size_with_row_get(3); + auto widgetSize = widget_size_with_row_get(3); if (ImGui::Button("Save", widgetSize)) { @@ -543,23 +506,42 @@ namespace anm2ed::imgui auto& ffmpegPath = settings.renderFFmpegPath; auto& path = settings.renderPath; auto& format = settings.renderFormat; + auto& scale = settings.renderScale; + auto& isRaw = settings.renderIsRawAnimation; auto& type = settings.renderType; auto& start = manager.recordingStart; auto& end = manager.recordingEnd; - auto& isRange = settings.renderIsRange; - auto widgetSize = imgui::widget_size_with_row_get(2); + auto& isRange = manager.isRecordingRange; + auto widgetSize = widget_size_with_row_get(2); auto dialogType = type == render::PNGS ? dialog::PNG_DIRECTORY_SET : type == render::GIF ? dialog::GIF_PATH_SET : type == render::WEBM ? dialog::WEBM_PATH_SET : dialog::NONE; - if (ImGui::ImageButton("##FFmpeg Path Set", resources.icons[icon::FOLDER].id, imgui::icon_size_get())) + auto replace_extension = [&]() + { path = std::filesystem::path(path).replace_extension(render::EXTENSIONS[type]); }; + + auto range_to_length = [&]() + { + start = 0; + end = animation->frameNum; + }; + + if (renderPopup.isJustOpened) + { + replace_extension(); + if (!isRange) range_to_length(); + } + + if (ImGui::ImageButton("##FFmpeg Path Set", resources.icons[icon::FOLDER].id, icon_size_get())) dialog.file_open(dialog::FFMPEG_PATH_SET); ImGui::SameLine(); - imgui::input_text_string("FFmpeg Path", &ffmpegPath); + input_text_string("FFmpeg Path", &ffmpegPath); + ImGui::SetItemTooltip("Set the path where the FFmpeg installation is located.\nFFmpeg is required to render " + "animations.\nhttps://ffmpeg.org"); dialog.set_string_to_selected_path(ffmpegPath, dialog::FFMPEG_PATH_SET); - if (ImGui::ImageButton("##Path Set", resources.icons[icon::FOLDER].id, imgui::icon_size_get())) + if (ImGui::ImageButton("##Path Set", resources.icons[icon::FOLDER].id, icon_size_get())) { if (dialogType == dialog::PNG_DIRECTORY_SET) dialog.folder_open(dialogType); @@ -567,25 +549,44 @@ namespace anm2ed::imgui dialog.file_open(dialogType); } ImGui::SameLine(); - imgui::input_text_string(type == render::PNGS ? "Directory" : "Path", &path); + input_text_string(type == render::PNGS ? "Directory" : "Path", &path); + ImGui::SetItemTooltip("Set the output path or directory for the animation."); dialog.set_string_to_selected_path(path, dialogType); - ImGui::Combo("Type", &type, render::STRINGS, render::COUNT); + if (ImGui::Combo("Type", &type, render::STRINGS, render::COUNT)) replace_extension(); + ImGui::SetItemTooltip("Set the type of the output."); ImGui::BeginDisabled(type != render::PNGS); - imgui::input_text_string("Format", &format); + input_text_string("Format", &format); + ImGui::SetItemTooltip( + "For outputted images, each image will use this format.\n{} represents the index of each image."); ImGui::EndDisabled(); ImGui::BeginDisabled(!isRange); - imgui::input_int_range("Start", start, 0, animation->frameNum - 1); - ImGui::InputInt("End", &end, start, animation->frameNum); + input_int_range("Start", start, 0, animation->frameNum - 1); + ImGui::SetItemTooltip("Set the starting time of the animation."); + input_int_range("End", end, start + 1, animation->frameNum); + ImGui::SetItemTooltip("Set the ending time of the animation."); ImGui::EndDisabled(); - ImGui::Checkbox("Custom Range", &isRange); + ImGui::BeginDisabled(!isRaw); + input_float_range("Scale", scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx"); + ImGui::SetItemTooltip("Set the output scale of the animation."); + ImGui::EndDisabled(); + + if (ImGui::Checkbox("Custom Range", &isRange)) + if (!isRange) range_to_length(); + ImGui::SetItemTooltip("Toggle using a custom range for the animation."); + + ImGui::SameLine(); + + ImGui::Checkbox("Raw", &isRaw); + ImGui::SetItemTooltip("Record only the layers of the animation."); if (ImGui::Button("Render", widgetSize)) { manager.isRecording = true; + manager.isRecordingStart = true; playback.time = start; playback.isPlaying = true; renderPopup.close(); @@ -599,6 +600,8 @@ namespace anm2ed::imgui ImGui::EndPopup(); } + renderPopup.end(); + aboutPopup.trigger(); if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize)) @@ -607,10 +610,10 @@ namespace anm2ed::imgui ImGui::EndPopup(); } - if (imgui::shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_NEW); - if (imgui::shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN); - if (imgui::shortcut(settings.shortcutSave, shortcut::GLOBAL)) document->save(); - if (imgui::shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE); - if (imgui::shortcut(settings.shortcutExit, shortcut::GLOBAL)) isQuitting = true; + if (shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_NEW); + if (shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN); + if (shortcut(settings.shortcutSave, shortcut::GLOBAL)) document->save(); + if (shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE); + if (shortcut(settings.shortcutExit, shortcut::GLOBAL)) isQuitting = true; } } diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index da3ae99..f798abf 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -32,6 +32,10 @@ namespace anm2ed::imgui { auto& anm2 = document.anm2; auto& playback = document.playback; + auto& zoom = document.previewZoom; + auto& pan = document.previewPan; + auto& isRootTransform = settings.previewIsRootTransform; + auto& scale = settings.renderScale; if (playback.isPlaying) { @@ -41,18 +45,52 @@ namespace anm2ed::imgui if (isSound && !anm2.content.sounds.empty()) if (auto animation = document.animation_get(); animation) if (animation->triggers.isVisible && !isOnlyShowLayers) - if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible) - anm2.content.sounds[anm2.content.events[trigger.eventID].soundID].audio.play(); + if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); + trigger.is_visible(anm2::TRIGGER)) + if (anm2.content.sounds.contains(trigger.soundID)) anm2.content.sounds[trigger.soundID].audio.play(); document.reference.frameTime = playback.time; } if (manager.isRecording) { + if (manager.isRecordingStart) + { + if (settings.renderIsRawAnimation) + { + savedSettings = settings; + settings.previewBackgroundColor = vec4(); + settings.previewIsGrid = false; + settings.previewIsAxes = false; + settings.timelineIsOnlyShowLayers = true; + + savedZoom = zoom; + savedPan = pan; + + if (auto animation = document.animation_get()) + { + auto rect = animation->rect(isRootTransform); + size = vec2(rect.z, rect.w) * scale; + set_to_rect(zoom, pan, rect); + } + + isSizeTrySet = false; + + bind(); + viewport_set(); + clear(settings.previewBackgroundColor); + unbind(); + } + + manager.isRecordingStart = false; + + return; // Need to wait an additional frame. Kind of hacky, but oh well. + } + auto pixels = pixels_get(); renderFrames.push_back(Texture(pixels.data(), size)); - if (playback.isFinished) + if (playback.time > manager.recordingEnd || playback.isFinished) { auto& ffmpegPath = settings.renderFFmpegPath; auto& path = settings.renderPath; @@ -72,7 +110,7 @@ namespace anm2ed::imgui isSuccess = false; break; } - logger.info(std::format("Saved frame to PNG: {}", outputPath.string())); + logger.info(std::format("Saved frame to: {}", outputPath.string())); } if (isSuccess) @@ -89,6 +127,12 @@ namespace anm2ed::imgui } renderFrames.clear(); + + pan = savedPan; + zoom = savedZoom; + settings = savedSettings; + isSizeTrySet = true; + playback.isPlaying = false; playback.isFinished = false; manager.isRecording = false; @@ -127,22 +171,26 @@ namespace anm2ed::imgui auto& shaderGrid = resources.shaders[shader::GRID]; auto& shaderTexture = resources.shaders[shader::TEXTURE]; - settings.previewPan = pan; - settings.previewZoom = zoom; + auto center_view = [&]() { pan = vec2(); }; if (ImGui::Begin("Animation Preview", &settings.windowIsAnimationPreview)) { - auto childSize = ImVec2(imgui::row_widget_width_get(4), + auto childSize = ImVec2(row_widget_width_get(4), (ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2)); if (ImGui::BeginChild("##Grid Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { - ImGui::Checkbox("Grid", &isGrid); + ImGui::SetItemTooltip("Toggle the visibility of the grid."); ImGui::SameLine(); ImGui::ColorEdit4("Color", value_ptr(gridColor), ImGuiColorEditFlags_NoInputs); - ImGui::InputInt2("Size", value_ptr(gridSize)); - ImGui::InputInt2("Offset", value_ptr(gridOffset)); + ImGui::SetItemTooltip("Change the grid's color."); + + input_int2_range("Size", gridSize, ivec2(GRID_SIZE_MIN), ivec2(GRID_SIZE_MAX)); + ImGui::SetItemTooltip("Change the size of all cells in the grid."); + + input_int2_range("Offset", gridOffset, ivec2(GRID_OFFSET_MIN), ivec2(GRID_OFFSET_MAX)); + ImGui::SetItemTooltip("Change the offset of the grid."); } ImGui::EndChild(); @@ -151,19 +199,20 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::InputFloat("Zoom", &zoom, zoomStep, zoomStep, "%.0f%%"); + ImGui::SetItemTooltip("Change the zoom of the preview."); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); - imgui::shortcut(settings.shortcutCenterView); + shortcut(settings.shortcutCenterView); if (ImGui::Button("Center View", widgetSize)) pan = vec2(); - imgui::set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); + set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); ImGui::SameLine(); - imgui::shortcut(settings.shortcutFit); + shortcut(settings.shortcutFit); if (ImGui::Button("Fit", widgetSize)) if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform)); - imgui::set_item_tooltip_shortcut("Set the view to match the extent of the animation.", settings.shortcutFit); + set_item_tooltip_shortcut("Set the view to match the extent of the animation.", settings.shortcutFit); ImGui::TextUnformatted(std::format(POSITION_FORMAT, (int)mousePos.x, (int)mousePos.y).c_str()); } @@ -174,14 +223,19 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); + ImGui::SetItemTooltip("Change the background color."); ImGui::SameLine(); ImGui::Checkbox("Axes", &isAxes); + ImGui::SetItemTooltip("Toggle the axes' visbility."); ImGui::SameLine(); ImGui::ColorEdit4("Color", value_ptr(axesColor), ImGuiColorEditFlags_NoInputs); + ImGui::SetItemTooltip("Set the color of the axes."); - imgui::combo_strings("Overlay", &overlayIndex, document.animationNamesCStr); + combo_negative_one_indexed("Overlay", &overlayIndex, document.animation.labels); + ImGui::SetItemTooltip("Set an animation to be drawn over the current animation."); ImGui::InputFloat("Alpha", &overlayTransparency, 0, 0, "%.0f"); + ImGui::SetItemTooltip("Set the alpha of the overlayed animation."); } ImGui::EndChild(); @@ -189,12 +243,14 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Helpers Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { - auto helpersChildSize = ImVec2(imgui::row_widget_width_get(2), ImGui::GetContentRegionAvail().y); + auto helpersChildSize = ImVec2(row_widget_width_get(2), ImGui::GetContentRegionAvail().y); if (ImGui::BeginChild("##Helpers Child 1", helpersChildSize)) { ImGui::Checkbox("Root Transform", &isRootTransform); + ImGui::SetItemTooltip("Root frames will transform the rest of the animation."); ImGui::Checkbox("Pivots", &isPivots); + ImGui::SetItemTooltip("Toggle the visibility of the animation's pivots."); } ImGui::EndChild(); @@ -203,7 +259,9 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Helpers Child 2", helpersChildSize)) { ImGui::Checkbox("Alt Icons", &isAltIcons); + ImGui::SetItemTooltip("Toggle a different appearance of the target icons."); ImGui::Checkbox("Border", &isBorder); + ImGui::SetItemTooltip("Toggle the visibility of borders around layers."); } ImGui::EndChild(); } @@ -211,7 +269,7 @@ namespace anm2ed::imgui auto cursorScreenPos = ImGui::GetCursorScreenPos(); - size_set(to_vec2(ImGui::GetContentRegionAvail())); + if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail())); bind(); viewport_set(); clear(backgroundColor); @@ -234,23 +292,23 @@ namespace anm2ed::imgui vec4 color = isOnionskin ? vec4(colorOffset, alphaOffset) : color::GREEN; - texture_render(shaderTexture, resources.icons[icon::TARGET].id, rootTransform, color); + auto icon = isAltIcons ? icon::TARGET_ALT : icon::TARGET; + texture_render(shaderTexture, resources.icons[icon].id, rootTransform, color); } for (auto& id : animation->layerOrder) { - auto& layerAnimation = animation->layerAnimations.at(id); + auto& layerAnimation = animation->layerAnimations[id]; if (!layerAnimation.isVisible) continue; auto& layer = anm2.content.layers.at(id); - if (auto frame = layerAnimation.frame_generate(time, anm2::LAYER); frame.isVisible) + if (auto frame = layerAnimation.frame_generate(time, anm2::LAYER); frame.is_visible()) { auto spritesheet = anm2.spritesheet_get(layer.spritesheetID); - if (!spritesheet) continue; + if (!spritesheet || !spritesheet->is_valid()) continue; auto& texture = spritesheet->texture; - if (!texture.is_valid()) continue; auto layerModel = math::quad_model_get(frame.size, frame.position, frame.pivot, math::percent_to_unit(frame.scale), frame.rotation); @@ -288,7 +346,8 @@ namespace anm2ed::imgui if (auto frame = nullAnimation.frame_generate(time, anm2::NULL_); frame.isVisible) { - auto icon = isShowRect ? icon::POINT : icon::TARGET; + auto icon = isShowRect ? icon::POINT : isAltIcons ? icon::TARGET_ALT : icon::TARGET; + auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE; auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED @@ -341,9 +400,8 @@ namespace anm2ed::imgui render(animation, frameTime); - if (overlayIndex > 0) - render(document.anm2.animation_get({overlayIndex - 1}), frameTime, {}, - 1.0f - math::uint8_to_float(overlayTransparency)); + if (auto overlayAnimation = anm2.animation_get({overlayIndex})) + render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency)); if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime); } @@ -356,7 +414,8 @@ namespace anm2ed::imgui if (animation && animation->triggers.isVisible && !isOnlyShowLayers) { - if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); trigger.isVisible) + if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); + trigger.isVisible && trigger.eventID > -1) { auto clipMin = ImGui::GetItemRectMin(); auto clipMax = ImGui::GetItemRectMax(); @@ -374,45 +433,57 @@ namespace anm2ed::imgui if (isPreviewHovered) { - ImGui::SetKeyboardFocusHere(-1); - - mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); - - auto isMouseClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + auto isMouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); auto isMouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); auto isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); - auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false); - auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false); - auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false); - auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false); + auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); + auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta); + auto mouseWheel = ImGui::GetIO().MouseWheel; + + auto isLeftJustPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false); + auto isRightJustPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false); + auto isUpJustPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false); + auto isDownJustPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false); + auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow); + auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow); + auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow); + auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow); + auto isLeftDown = ImGui::IsKeyDown(ImGuiKey_LeftArrow); + auto isRightDown = ImGui::IsKeyDown(ImGuiKey_RightArrow); + auto isUpDown = ImGui::IsKeyDown(ImGuiKey_UpArrow); + auto isDownDown = ImGui::IsKeyDown(ImGuiKey_DownArrow); auto isLeftReleased = ImGui::IsKeyReleased(ImGuiKey_LeftArrow); auto isRightReleased = ImGui::IsKeyReleased(ImGuiKey_RightArrow); auto isUpReleased = ImGui::IsKeyReleased(ImGuiKey_UpArrow); auto isDownReleased = ImGui::IsKeyReleased(ImGuiKey_DownArrow); - auto isLeft = imgui::chord_repeating(ImGuiKey_LeftArrow); - auto isRight = imgui::chord_repeating(ImGuiKey_RightArrow); - auto isUp = imgui::chord_repeating(ImGuiKey_UpArrow); - auto isDown = imgui::chord_repeating(ImGuiKey_DownArrow); - auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); - auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta); - auto mouseWheel = ImGui::GetIO().MouseWheel; - auto isZoomIn = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomIn)); - auto isZoomOut = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomOut)); + auto isKeyJustPressed = isLeftJustPressed || isRightJustPressed || isUpJustPressed || isDownJustPressed; + auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown; + auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; + + auto isZoomIn = chord_repeating(string_to_chord(settings.shortcutZoomIn)); + auto isZoomOut = chord_repeating(string_to_chord(settings.shortcutZoomOut)); + + auto isBegin = isMouseClicked || isKeyJustPressed; + auto isDuring = isMouseDown || isKeyDown; + auto isEnd = isMouseReleased || isKeyReleased; + auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); + auto frame = document.frame_get(); auto useTool = tool; auto step = isMod ? canvas::STEP_FAST : canvas::STEP; - auto isKeyPressed = isLeftPressed || isRightPressed || isUpPressed || isDownPressed; - auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; - auto isBegin = isMouseClick || isKeyPressed; - auto isEnd = isMouseReleased || isKeyReleased; + mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); if (isMouseMiddleDown) useTool = tool::PAN; if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE; if (tool == tool::SCALE && isMouseRightDown) useTool = tool::MOVE; - ImGui::SetMouseCursor(tool::INFO[useTool].cursor); + auto& areaType = tool::INFO[useTool].areaType; + auto cursor = areaType == tool::ANIMATION_PREVIEW || areaType == tool::ALL ? tool::INFO[useTool].cursor + : ImGuiMouseCursor_NotAllowed; + ImGui::SetMouseCursor(cursor); + ImGui::SetKeyboardFocusHere(-1); switch (useTool) { @@ -423,28 +494,62 @@ namespace anm2ed::imgui if (!frame) break; if (isBegin) document.snapshot("Frame Position"); if (isMouseDown) frame->position = mousePos; - if (isLeft) frame->position.x -= step; - if (isRight) frame->position.x += step; - if (isUp) frame->position.y -= step; - if (isDown) frame->position.y += step; + if (isLeftPressed) frame->position.x -= step; + if (isRightPressed) frame->position.x += step; + if (isUpPressed) frame->position.y -= step; + if (isDownPressed) frame->position.y += step; if (isEnd) document.change(Document::FRAMES); + if (isDuring) + { + if (ImGui::BeginTooltip()) + { + auto positionFormat = math::vec2_format_get(frame->position); + auto positionString = std::format("Position: ({}, {})", positionFormat, positionFormat); + ImGui::Text(positionString.c_str(), frame->position.x, frame->position.y); + ImGui::EndTooltip(); + } + } break; case tool::SCALE: if (!frame) break; if (isBegin) document.snapshot("Frame Scale"); if (isMouseDown) frame->scale += mouseDelta; - if (isLeft) frame->scale.x -= step; - if (isRight) frame->scale.x += step; - if (isUp) frame->scale.y -= step; - if (isDown) frame->scale.y += step; + if (isLeftPressed) frame->scale.x -= step; + if (isRightPressed) frame->scale.x += step; + if (isUpPressed) frame->scale.y -= step; + if (isDownPressed) frame->scale.y += step; + + if (isDuring) + { + if (ImGui::BeginTooltip()) + { + auto scaleFormat = math::vec2_format_get(frame->scale); + auto scaleString = std::format("Scale: ({}, {})", scaleFormat, scaleFormat); + ImGui::Text(scaleString.c_str(), frame->scale.x, frame->scale.y); + ImGui::EndTooltip(); + } + } + if (isEnd) document.change(Document::FRAMES); break; case tool::ROTATE: if (!frame) break; if (isBegin) document.snapshot("Frame Rotation"); if (isMouseDown) frame->rotation += mouseDelta.y; - if (isLeft || isDown) frame->rotation -= step; - if (isUp || isRight) frame->rotation += step; + if (isLeftPressed || isDownPressed) frame->rotation -= step; + if (isUpPressed || isRightPressed) frame->rotation += step; + + if (isDuring) + { + if (ImGui::BeginTooltip()) + { + auto rotationFormat = math::float_format_get(frame->rotation); + auto rotationString = std::format("Rotation: {}", rotationFormat); + ImGui::Text(rotationString.c_str(), frame->rotation); + ImGui::EndTooltip(); + } + } + if (isEnd) document.change(Document::FRAMES); break; default: @@ -452,7 +557,8 @@ namespace anm2ed::imgui } if (mouseWheel != 0 || isZoomIn || isZoomOut) - zoom_set(zoom, pan, vec2(mousePos), (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); + zoom_set(zoom, pan, mouseWheel != 0 ? vec2(mousePos) : vec2(), + (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); } } ImGui::End(); @@ -478,5 +584,14 @@ namespace anm2ed::imgui ImGui::EndPopup(); } + + if (!document.isAnimationPreviewSet) + { + center_view(); + zoom = settings.previewStartZoom; + document.isAnimationPreviewSet = true; + } + + settings.previewStartZoom = zoom; } } diff --git a/src/imgui/window/animation_preview.h b/src/imgui/window/animation_preview.h index 486e174..33fb991 100644 --- a/src/imgui/window/animation_preview.h +++ b/src/imgui/window/animation_preview.h @@ -10,6 +10,10 @@ namespace anm2ed::imgui class AnimationPreview : public Canvas { bool isPreviewHovered{}; + bool isSizeTrySet{true}; + Settings savedSettings{}; + float savedZoom{}; + glm::vec2 savedPan{}; glm::ivec2 mousePos{}; std::vector renderFrames{}; diff --git a/src/imgui/window/animations.cpp b/src/imgui/window/animations.cpp index c3ed704..dcac6c1 100644 --- a/src/imgui/window/animations.cpp +++ b/src/imgui/window/animations.cpp @@ -2,6 +2,10 @@ #include +#include "toast.h" +#include "vector_.h" + +using namespace anm2ed::util; using namespace anm2ed::resource; using namespace anm2ed::types; @@ -12,10 +16,11 @@ namespace anm2ed::imgui auto& document = *manager.get(); auto& anm2 = document.anm2; auto& reference = document.reference; - auto& hovered = document.hoveredAnimation; - auto& multiSelect = document.animationMultiSelect; - auto& mergeMultiSelect = document.animationMergeMultiSelect; - auto& mergeTarget = document.mergeTarget; + auto& hovered = document.animation.hovered; + auto& selection = document.animation.selection; + auto& mergeSelection = document.merge.selection; + auto& mergeReference = document.merge.reference; + auto& overlayIndex = document.overlayIndex; hovered = -1; @@ -25,7 +30,7 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders)) { - multiSelect.start(anm2.animations.items.size()); + selection.start(anm2.animations.items.size()); for (auto [i, animation] : std::views::enumerate(anm2.animations.items)) { @@ -40,11 +45,11 @@ namespace anm2ed::imgui : font::REGULAR; ImGui::PushFont(resources.fonts[font].get(), font::SIZE); - ImGui::SetNextItemSelectionUserData(i); + ImGui::SetNextItemSelectionUserData((int)i); if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i), - animation.name, multiSelect.contains(i))) - document.animation_set(i); - if (ImGui::IsItemHovered()) hovered = i; + animation.name, selection.contains((int)i))) + reference = {(int)i}; + if (ImGui::IsItemHovered()) hovered = (int)i; ImGui::PopFont(); if (ImGui::BeginItemTooltip()) @@ -69,11 +74,12 @@ namespace anm2ed::imgui if (ImGui::BeginDragDropSource()) { - static std::vector selection; - selection.assign(multiSelect.begin(), multiSelect.end()); - ImGui::SetDragDropPayload("Animation Drag Drop", selection.data(), selection.size() * sizeof(int)); - for (auto& i : selection) - ImGui::TextUnformatted(anm2.animations.items[i].name.c_str()); + 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(); } @@ -85,7 +91,8 @@ namespace anm2ed::imgui auto payloadCount = payload->DataSize / sizeof(int); std::vector indices(payloadIndices, payloadIndices + payloadCount); std::sort(indices.begin(), indices.end()); - document.animations_move(indices, i); + DOCUMENT_EDIT(document, "Move Animation(s)", Document::ANIMATIONS, + selection = vector::move_indices(anm2.animations.items, indices, i)); } ImGui::EndDragDropTarget(); } @@ -93,14 +100,14 @@ namespace anm2ed::imgui ImGui::PopID(); } - multiSelect.finish(); + selection.finish(); auto copy = [&]() { - if (!multiSelect.empty()) + if (!selection.empty()) { std::string clipboardText{}; - for (auto& i : multiSelect) + for (auto& i : selection) clipboardText += anm2.animations.items[i].to_string(); clipboard.set(clipboardText); } @@ -111,13 +118,41 @@ namespace anm2ed::imgui auto cut = [&]() { copy(); - document.animations_remove(); + + auto remove = [&]() + { + if (!selection.empty()) + { + for (auto& i : selection | std::views::reverse) + anm2.animations.items.erase(anm2.animations.items.begin() + i); + selection.clear(); + } + else if (hovered > -1) + { + anm2.animations.items.erase(anm2.animations.items.begin() + hovered); + hovered = -1; + } + }; + + DOCUMENT_EDIT(document, "Cut Animation(s)", Document::ANIMATIONS, remove()); }; auto paste = [&]() { auto clipboardText = clipboard.get(); - document.animations_deserialize(clipboardText); + + auto deserialize = [&]() + { + 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)) + selection = indices; + else + toasts.error(std::format("Failed to deserialize animation(s): {}", errorString)); + }; + + DOCUMENT_EDIT(document, "Paste Animation(s)", Document::ANIMATIONS, deserialize()); }; if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); @@ -126,15 +161,9 @@ namespace anm2ed::imgui if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - ImGui::BeginDisabled(multiSelect.empty() && hovered == -1); - if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str())) cut(); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(clipboard.is_empty()); - if (ImGui::MenuItem("Paste", settings.shortcutPaste.c_str())) paste(); - ImGui::EndDisabled(); - + if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, !selection.empty() || hovered > -1)) cut(); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !selection.empty() || hovered > -1)) copy(); + if (ImGui::MenuItem("Paste", settings.shortcutPaste.c_str(), false, !clipboard.is_empty())) paste(); ImGui::EndPopup(); } } @@ -143,33 +172,98 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(5); shortcut(settings.shortcutAdd); - if (ImGui::Button("Add", widgetSize)) document.animation_add(); + if (ImGui::Button("Add", widgetSize)) + { + auto add = [&]() + { + anm2::Animation 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 = 0; + if (!anm2.animations.items.empty()) + index = selection.empty() ? (int)anm2.animations.items.size() - 1 : *selection.rbegin() + 1; + + anm2.animations.items.insert(anm2.animations.items.begin() + index, animation); + selection = {index}; + reference = {index}; + }; + + DOCUMENT_EDIT(document, "Add Animation", Document::ANIMATIONS, add()); + } set_item_tooltip_shortcut("Add a new animation.", settings.shortcutAdd); ImGui::SameLine(); - ImGui::BeginDisabled(multiSelect.empty()); + ImGui::BeginDisabled(selection.empty()); { shortcut(settings.shortcutDuplicate); - if (ImGui::Button("Duplicate", widgetSize)) document.animation_duplicate(); + if (ImGui::Button("Duplicate", widgetSize)) + { + auto duplicate = [&]() + { + 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, "Duplicate Animation(s)", Document::ANIMATIONS, duplicate()); + } set_item_tooltip_shortcut("Duplicate the selected animation(s).", settings.shortcutDuplicate); ImGui::SameLine(); - if (shortcut(settings.shortcutMerge, shortcut::FOCUSED)) - if (multiSelect.size() > 0) document.animations_merge_quick(); + if (shortcut(settings.shortcutMerge, shortcut::FOCUSED) && !selection.empty()) + { + auto merge_quick = [&]() + { + int merged{}; + if (selection.contains(overlayIndex)) overlayIndex = -1; - ImGui::BeginDisabled(multiSelect.size() != 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, "Merge Animations", Document::ANIMATIONS, merge_quick()) + } + + ImGui::BeginDisabled(selection.size() != 1); { if (ImGui::Button("Merge", widgetSize)) { mergePopup.open(); - mergeMultiSelect.clear(); - mergeTarget = *multiSelect.begin(); + mergeSelection.clear(); + mergeReference = *selection.begin(); } } ImGui::EndDisabled(); - set_item_tooltip_shortcut("Open the merge popup.\nUsing the shortcut will merge the animations with\nthe last " "configured merge settings.", settings.shortcutMerge); @@ -177,14 +271,31 @@ namespace anm2ed::imgui ImGui::SameLine(); shortcut(settings.shortcutRemove); - if (ImGui::Button("Remove", widgetSize)) document.animations_remove(); + if (ImGui::Button("Remove", widgetSize)) + { + auto remove = [&]() + { + for (auto& i : selection | std::views::reverse) + { + if (i == overlayIndex) overlayIndex = -1; + anm2.animations.items.erase(anm2.animations.items.begin() + i); + } + selection.clear(); + }; + + DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, remove()); + } set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate); ImGui::SameLine(); shortcut(settings.shortcutDefault); - ImGui::BeginDisabled(multiSelect.size() != 1); - if (ImGui::Button("Default", widgetSize)) document.animation_default(); + ImGui::BeginDisabled(selection.size() != 1); + if (ImGui::Button("Default", widgetSize)) + { + DOCUMENT_EDIT(document, "Default Animation", Document::ANIMATIONS, + anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name); + } ImGui::EndDisabled(); set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDefault); } @@ -196,7 +307,7 @@ namespace anm2ed::imgui { auto merge_close = [&]() { - mergeMultiSelect.clear(); + mergeSelection.clear(); mergePopup.close(); }; @@ -212,19 +323,21 @@ namespace anm2ed::imgui if (ImGui::BeginChild("Animations", animationsSize, ImGuiChildFlags_Borders)) { - mergeMultiSelect.start(anm2.animations.items.size()); + mergeSelection.start(anm2.animations.items.size()); for (auto [i, animation] : std::views::enumerate(anm2.animations.items)) { + if (i == mergeReference) continue; + ImGui::PushID(i); ImGui::SetNextItemSelectionUserData(i); - ImGui::Selectable(animation.name.c_str(), mergeMultiSelect.contains(i)); + ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i)); ImGui::PopID(); } - mergeMultiSelect.finish(); + mergeSelection.finish(); } ImGui::EndChild(); @@ -258,7 +371,17 @@ namespace anm2ed::imgui if (ImGui::Button("Merge", widgetSize)) { - document.animations_merge((merge::Type)type, isDeleteAnimationsAfter); + auto merge = [&]() + { + if (mergeSelection.contains(overlayIndex)) overlayIndex = -1; + auto merged = + anm2.animations_merge(mergeReference, mergeSelection, (merge::Type)type, isDeleteAnimationsAfter); + + selection = {merged}; + reference = {merged}; + }; + + DOCUMENT_EDIT(document, "Merge Animations", Document::ANIMATIONS, merge()); merge_close(); } ImGui::SameLine(); diff --git a/src/imgui/window/events.cpp b/src/imgui/window/events.cpp index fa3da45..e7f5843 100644 --- a/src/imgui/window/events.cpp +++ b/src/imgui/window/events.cpp @@ -2,6 +2,10 @@ #include +#include "map_.h" +#include "toast.h" + +using namespace anm2ed::util; using namespace anm2ed::resource; using namespace anm2ed::types; @@ -11,10 +15,10 @@ namespace anm2ed::imgui { auto& document = *manager.get(); auto& anm2 = document.anm2; - auto& unused = document.unusedEventIDs; - auto& hovered = document.hoveredEvent; - auto& reference = document.referenceEvent; - auto& multiSelect = document.eventMultiSelect; + auto& unused = document.event.unused; + auto& hovered = document.event.hovered; + auto& reference = document.event.reference; + auto& selection = document.event.selection; hovered = -1; @@ -24,23 +28,15 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Events Child", childSize, true)) { - multiSelect.start(anm2.content.events.size()); + selection.start(anm2.content.events.size()); for (auto& [id, event] : anm2.content.events) { ImGui::PushID(id); ImGui::SetNextItemSelectionUserData(id); - ImGui::Selectable(event.name.c_str(), multiSelect.contains(id)); - if (ImGui::IsItemHovered()) - { - hovered = id; - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) - { - reference = id; - editEvent = document.anm2.content.events[reference]; - propertiesPopup.open(); - } - } + if (selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id), + event.name, selection.contains(id))) + if (ImGui::IsItemHovered()) hovered = id; if (ImGui::BeginItemTooltip()) { @@ -52,14 +48,14 @@ namespace anm2ed::imgui ImGui::PopID(); } - multiSelect.finish(); + selection.finish(); auto copy = [&]() { - if (!multiSelect.empty()) + if (!selection.empty()) { std::string clipboardText{}; - for (auto& id : multiSelect) + for (auto& id : selection) clipboardText += anm2.content.events[id].to_string(id); clipboard.set(clipboardText); } @@ -69,8 +65,12 @@ namespace anm2ed::imgui auto paste = [&](merge::Type type) { - auto clipboardText = clipboard.get(); - document.events_deserialize(clipboardText, type); + std::string errorString{}; + document.snapshot("Paste Event(s)"); + if (anm2.events_deserialize(clipboard.get(), type, &errorString)) + document.change(Document::EVENTS); + else + toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); }; if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); @@ -78,25 +78,16 @@ namespace anm2ed::imgui if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - ImGui::BeginDisabled(); - ImGui::MenuItem("Cut", settings.shortcutCut.c_str()); - ImGui::EndDisabled(); + ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !selection.empty() || hovered > -1)) copy(); - ImGui::BeginDisabled(multiSelect.empty() && hovered == -1); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(clipboard.is_empty()); + if (ImGui::BeginMenu("Paste", !clipboard.is_empty())) { - if (ImGui::BeginMenu("Paste")) - { - if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); - if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); + if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); + if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); - ImGui::EndMenu(); - } + ImGui::EndMenu(); } - ImGui::EndDisabled(); ImGui::EndPopup(); } @@ -108,53 +99,36 @@ namespace anm2ed::imgui shortcut(settings.shortcutAdd); if (ImGui::Button("Add", widgetSize)) { - reference = -1; - editEvent = anm2::Event(); - propertiesPopup.open(); + auto add = [&]() + { + auto id = map::next_id_get(anm2.content.events); + anm2.content.events[id] = anm2::Event(); + selection = {id}; + reference = {id}; + }; + + DOCUMENT_EDIT(document, "Add Event", Document::EVENTS, add()); } set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd); ImGui::SameLine(); shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unused.empty()); - if (ImGui::Button("Remove Unused", widgetSize)) document.events_remove_unused(); + if (ImGui::Button("Remove Unused", widgetSize)) + { + auto remove_unused = [&]() + { + for (auto& id : unused) + anm2.content.events.erase(id); + unused.clear(); + }; + + DOCUMENT_EDIT(document, "Remove Unused Events", Document::EVENTS, remove_unused()); + } ImGui::EndDisabled(); set_item_tooltip_shortcut("Remove unused events (i.e., ones not used by any trigger in any animation.)", settings.shortcutRemove); } ImGui::End(); - - propertiesPopup.trigger(); - - if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto childSize = child_size_get(2); - auto& event = editEvent; - - if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders)) - { - if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); - input_text_string("Name", &event.name); - ImGui::SetItemTooltip("Set the event's name."); - combo_strings("Sound", &event.soundID, document.soundNames); - ImGui::SetItemTooltip("Set the event sound; it will play when a trigger associated with this event activates."); - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize)) - { - document.event_set(event); - propertiesPopup.close(); - } - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", widgetSize)) propertiesPopup.close(); - - propertiesPopup.end(); - ImGui::EndPopup(); - } } } diff --git a/src/imgui/window/events.h b/src/imgui/window/events.h index 25a3705..7d01fce 100644 --- a/src/imgui/window/events.h +++ b/src/imgui/window/events.h @@ -9,9 +9,6 @@ namespace anm2ed::imgui { class Events { - anm2::Event editEvent{}; - PopupHelper propertiesPopup{PopupHelper("Event Properties", POPUP_SMALL_NO_HEIGHT)}; - public: void update(Manager&, Settings&, Resources&, Clipboard&); }; diff --git a/src/imgui/window/frame_properties.cpp b/src/imgui/window/frame_properties.cpp index f83b002..5ffab31 100644 --- a/src/imgui/window/frame_properties.cpp +++ b/src/imgui/window/frame_properties.cpp @@ -1,7 +1,5 @@ #include "frame_properties.h" -#include - #include #include "math_.h" @@ -18,9 +16,7 @@ namespace anm2ed::imgui if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties)) { auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& reference = document.reference; - auto& type = reference.itemType; + auto& type = document.reference.itemType; auto frame = document.frame_get(); auto useFrame = frame ? *frame : anm2::Frame(); @@ -28,13 +24,16 @@ namespace anm2ed::imgui { if (type == anm2::TRIGGER) { - std::vector eventNames{}; - for (auto& event : anm2.content.events | std::views::values) - eventNames.emplace_back(event.name); - - if (imgui::combo_strings("Event", frame ? &useFrame.eventID : &dummy_value(), eventNames)) + if (combo_negative_one_indexed("Event", frame ? &useFrame.eventID : &dummy_value(), + document.event.labels)) DOCUMENT_EDIT(document, "Trigger Event", Document::FRAMES, frame->eventID = useFrame.eventID); ImGui::SetItemTooltip("Change the event this trigger uses."); + + if (combo_negative_one_indexed("Sound", frame ? &useFrame.soundID : &dummy_value(), + document.sound.labels)) + DOCUMENT_EDIT(document, "Trigger Sound", Document::FRAMES, frame->soundID = useFrame.soundID); + ImGui::SetItemTooltip("Change the sound this trigger uses."); + if (ImGui::InputInt("At Frame", frame ? &useFrame.atFrame : &dummy_value(), imgui::STEP, imgui::STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) DOCUMENT_EDIT(document, "Trigger At Frame", Document::FRAMES, frame->atFrame = useFrame.atFrame); diff --git a/src/imgui/window/layers.cpp b/src/imgui/window/layers.cpp index 1b12e34..b09d1e9 100644 --- a/src/imgui/window/layers.cpp +++ b/src/imgui/window/layers.cpp @@ -2,6 +2,10 @@ #include +#include "map_.h" +#include "toast.h" + +using namespace anm2ed::util; using namespace anm2ed::resource; using namespace anm2ed::types; @@ -11,10 +15,10 @@ namespace anm2ed::imgui { auto& document = *manager.get(); auto& anm2 = document.anm2; - auto& reference = document.referenceLayer; - auto& unused = document.unusedLayerIDs; - auto& hovered = document.hoveredLayer; - auto& multiSelect = document.layersMultiSelect; + auto& reference = document.layer.reference; + auto& unused = document.layer.unused; + auto& hovered = document.layer.hovered; + auto& selection = document.layer.selection; auto& propertiesPopup = manager.layerPropertiesPopup; hovered = -1; @@ -25,11 +29,11 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Layers Child", childSize, true)) { - multiSelect.start(anm2.content.layers.size()); + selection.start(anm2.content.layers.size()); for (auto& [id, layer] : anm2.content.layers) { - auto isSelected = multiSelect.contains(id); + auto isSelected = selection.contains(id); ImGui::PushID(id); @@ -55,14 +59,14 @@ namespace anm2ed::imgui ImGui::PopID(); } - multiSelect.finish(); + selection.finish(); auto copy = [&]() { - if (!multiSelect.empty()) + if (!selection.empty()) { std::string clipboardText{}; - for (auto& id : multiSelect) + for (auto& id : selection) clipboardText += anm2.content.layers[id].to_string(id); clipboard.set(clipboardText); } @@ -72,8 +76,12 @@ namespace anm2ed::imgui auto paste = [&](merge::Type type) { - auto clipboardText = clipboard.get(); - document.layers_deserialize(clipboardText, type); + std::string errorString{}; + document.snapshot("Paste Layer(s)"); + if (anm2.layers_deserialize(clipboard.get(), type, &errorString)) + document.change(Document::NULLS); + else + toasts.error(std::format("Failed to deserialize layer(s): {}", errorString)); }; if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); @@ -81,25 +89,16 @@ namespace anm2ed::imgui if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - ImGui::BeginDisabled(); - ImGui::MenuItem("Cut", settings.shortcutCut.c_str()); - ImGui::EndDisabled(); + ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !selection.empty() || hovered > -1)) copy(); - ImGui::BeginDisabled(multiSelect.empty() && hovered == -1); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(clipboard.is_empty()); + if (ImGui::BeginMenu("Paste", !clipboard.is_empty())) { - if (ImGui::BeginMenu("Paste")) - { - if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); - if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); + if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); + if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); - ImGui::EndMenu(); - } + ImGui::EndMenu(); } - ImGui::EndDisabled(); ImGui::EndPopup(); } @@ -115,7 +114,17 @@ namespace anm2ed::imgui shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unused.empty()); - if (ImGui::Button("Remove Unused", widgetSize)) document.layers_remove_unused(); + if (ImGui::Button("Remove Unused", widgetSize)) + { + auto remove_unused = [&]() + { + for (auto& id : unused) + anm2.content.layers.erase(id); + unused.clear(); + }; + + DOCUMENT_EDIT(document, "Remove Unused Layers", Document::LAYERS, remove_unused()); + } ImGui::EndDisabled(); set_item_tooltip_shortcut("Remove unused layers (i.e., ones not used in any animation.)", settings.shortcutRemove); @@ -134,7 +143,7 @@ namespace anm2ed::imgui if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); input_text_string("Name", &layer.name); ImGui::SetItemTooltip("Set the item's name."); - combo_strings("Spritesheet", &layer.spritesheetID, document.spritesheetNames); + combo_negative_one_indexed("Spritesheet", &layer.spritesheetID, document.spritesheet.labels); ImGui::SetItemTooltip("Set the layer item's spritesheet."); } ImGui::EndChild(); @@ -143,7 +152,26 @@ namespace anm2ed::imgui if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize)) { - document.layer_set(layer); + auto add = [&]() + { + auto id = map::next_id_get(anm2.content.layers); + anm2.content.layers[id] = layer; + selection = {id}; + }; + + auto set = [&]() + { + anm2.content.layers[reference] = layer; + selection = {reference}; + }; + + if (reference == -1) + { + DOCUMENT_EDIT(document, "Add Layer", Document::LAYERS, add()); + } + else + DOCUMENT_EDIT(document, "Set Layer Properties", Document::LAYERS, set()); + manager.layer_properties_close(); } diff --git a/src/imgui/window/nulls.cpp b/src/imgui/window/nulls.cpp index 302674c..db776f0 100644 --- a/src/imgui/window/nulls.cpp +++ b/src/imgui/window/nulls.cpp @@ -2,7 +2,11 @@ #include +#include "map_.h" +#include "toast.h" + using namespace anm2ed::resource; +using namespace anm2ed::util; using namespace anm2ed::types; namespace anm2ed::imgui @@ -11,10 +15,10 @@ namespace anm2ed::imgui { auto& document = *manager.get(); auto& anm2 = document.anm2; - auto& reference = document.referenceNull; - auto& unused = document.unusedNullIDs; - auto& hovered = document.hoveredNull; - auto& multiSelect = document.nullMultiSelect; + auto& reference = document.null.reference; + auto& unused = document.null.unused; + auto& hovered = document.null.hovered; + auto& selection = document.null.selection; auto& propertiesPopup = manager.nullPropertiesPopup; hovered = -1; @@ -25,11 +29,11 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Nulls Child", childSize, true)) { - multiSelect.start(anm2.content.nulls.size()); + selection.start(anm2.content.nulls.size()); for (auto& [id, null] : anm2.content.nulls) { - auto isSelected = multiSelect.contains(id); + auto isSelected = selection.contains(id); auto isReferenced = reference == id; ImGui::PushID(id); @@ -55,14 +59,14 @@ namespace anm2ed::imgui ImGui::PopID(); } - multiSelect.finish(); + selection.finish(); auto copy = [&]() { - if (!multiSelect.empty()) + if (!selection.empty()) { std::string clipboardText{}; - for (auto& id : multiSelect) + for (auto& id : selection) clipboardText += anm2.content.nulls[id].to_string(id); clipboard.set(clipboardText); } @@ -72,8 +76,12 @@ namespace anm2ed::imgui auto paste = [&](merge::Type type) { - auto clipboardText = clipboard.get(); - document.nulls_deserialize(clipboardText, type); + std::string errorString{}; + document.snapshot("Paste Null(s)"); + if (anm2.nulls_deserialize(clipboard.get(), type, &errorString)) + document.change(Document::NULLS); + else + toasts.error(std::format("Failed to deserialize null(s): {}", errorString)); }; if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); @@ -81,25 +89,16 @@ namespace anm2ed::imgui if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - ImGui::BeginDisabled(); - ImGui::MenuItem("Cut", settings.shortcutCut.c_str()); - ImGui::EndDisabled(); + ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, selection.empty() || hovered > -1)) copy(); - ImGui::BeginDisabled(multiSelect.empty() && hovered == -1); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(clipboard.is_empty()); + if (ImGui::BeginMenu("Paste", !clipboard.is_empty())) { - if (ImGui::BeginMenu("Paste")) - { - if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); - if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); + if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); + if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); - ImGui::EndMenu(); - } + ImGui::EndMenu(); } - ImGui::EndDisabled(); ImGui::EndPopup(); } @@ -115,7 +114,17 @@ namespace anm2ed::imgui shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unused.empty()); - if (ImGui::Button("Remove Unused", widgetSize)) document.nulls_remove_unused(); + if (ImGui::Button("Remove Unused", widgetSize)) + { + auto remove_unused = [&]() + { + for (auto& id : unused) + anm2.content.nulls.erase(id); + unused.clear(); + }; + + DOCUMENT_EDIT(document, "Remove Unused Events", Document::EVENTS, remove_unused()); + } ImGui::EndDisabled(); set_item_tooltip_shortcut("Remove unused nulls (i.e., ones not used in any animation.)", settings.shortcutRemove); } @@ -143,7 +152,26 @@ namespace anm2ed::imgui if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize)) { - document.null_set(null); + auto add = [&]() + { + auto id = map::next_id_get(anm2.content.nulls); + anm2.content.nulls[id] = null; + selection = {id}; + }; + + auto set = [&]() + { + anm2.content.nulls[reference] = null; + selection = {reference}; + }; + + if (reference == -1) + { + DOCUMENT_EDIT(document, "Add Null", Document::NULLS, add()); + } + else + DOCUMENT_EDIT(document, "Set Null Properties", Document::NULLS, set()); + manager.null_properties_close(); } diff --git a/src/imgui/window/sounds.cpp b/src/imgui/window/sounds.cpp index c567d62..35f1cf4 100644 --- a/src/imgui/window/sounds.cpp +++ b/src/imgui/window/sounds.cpp @@ -2,6 +2,8 @@ #include +#include "toast.h" + using namespace anm2ed::dialog; using namespace anm2ed::types; using namespace anm2ed::resource; @@ -12,10 +14,10 @@ namespace anm2ed::imgui { auto& document = *manager.get(); auto& anm2 = document.anm2; - auto& reference = document.referenceNull; - auto& unused = document.unusedNullIDs; - auto& hovered = document.hoveredNull; - auto& multiSelect = document.soundMultiSelect; + auto& reference = document.sound.reference; + auto& unused = document.sound.unused; + auto& hovered = document.null.hovered; + auto& selection = document.sound.selection; hovered = -1; @@ -25,24 +27,18 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Sounds Child", childSize, true)) { - multiSelect.start(anm2.content.sounds.size()); + selection.start(anm2.content.sounds.size()); for (auto& [id, sound] : anm2.content.sounds) { - auto isSelected = multiSelect.contains(id); + 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); - if (ImGui::Selectable(std::format(anm2::SOUND_FORMAT, id, sound.path.string()).c_str(), isSelected)) - sound.audio.play(); - if (ImGui::IsItemHovered()) - { - hovered = id; - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) - ; - } + if (ImGui::Selectable(sound.path.c_str(), isSelected)) sound.play(); + if (ImGui::IsItemHovered()) hovered = id; if (isReferenced) ImGui::PopFont(); @@ -58,14 +54,14 @@ namespace anm2ed::imgui ImGui::PopID(); } - multiSelect.finish(); + selection.finish(); auto copy = [&]() { - if (!multiSelect.empty()) + if (!selection.empty()) { std::string clipboardText{}; - for (auto& id : multiSelect) + for (auto& id : selection) clipboardText += anm2.content.sounds[id].to_string(id); clipboard.set(clipboardText); } @@ -75,8 +71,12 @@ namespace anm2ed::imgui auto paste = [&](merge::Type type) { - auto clipboardText = clipboard.get(); - document.sounds_deserialize(clipboardText, type); + std::string errorString{}; + document.snapshot("Paste Sound(s)"); + if (anm2.sounds_deserialize(clipboard.get(), document.directory_get(), type, &errorString)) + document.change(Document::SOUNDS); + else + toasts.error(std::format("Failed to deserialize sound(s): {}", errorString)); }; if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); @@ -84,25 +84,16 @@ namespace anm2ed::imgui if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - ImGui::BeginDisabled(); - ImGui::MenuItem("Cut", settings.shortcutCut.c_str()); - ImGui::EndDisabled(); + ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, false); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), !selection.empty() && hovered > -1)) copy(); - ImGui::BeginDisabled(multiSelect.empty() && hovered == -1); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(clipboard.is_empty()); + if (ImGui::BeginMenu("Paste", !clipboard.is_empty())) { - if (ImGui::BeginMenu("Paste")) - { - if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); - if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); + if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); + if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); - ImGui::EndMenu(); - } + ImGui::EndMenu(); } - ImGui::EndDisabled(); ImGui::EndPopup(); } @@ -119,7 +110,16 @@ namespace anm2ed::imgui imgui::shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unused.empty()); if (ImGui::Button("Remove Unused", widgetSize)) - ; + { + auto remove_unused = [&]() + { + for (auto& id : unused) + anm2.content.sounds.erase(id); + unused.clear(); + }; + + DOCUMENT_EDIT(document, "Remove Unused Sounds", Document::SOUNDS, remove_unused()); + }; ImGui::EndDisabled(); imgui::set_item_tooltip_shortcut("Remove unused sounds (i.e., ones not used in any trigger.)", settings.shortcutRemove); @@ -128,7 +128,20 @@ namespace anm2ed::imgui if (dialog.is_selected(dialog::SOUND_OPEN)) { - document.sound_add(dialog.path); + auto add = [&]() + { + int id{}; + if (anm2.sound_add(document.directory_get(), dialog.path, id)) + { + selection = {id}; + toasts.info(std::format("Initialized sound #{}: {}", id, dialog.path)); + } + else + toasts.error(std::format("Failed to initialize sound: {}", dialog.path)); + }; + + DOCUMENT_EDIT(document, "Add Sound", Document::SOUNDS, add()); + dialog.reset(); } } diff --git a/src/imgui/window/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp index 8a0f9de..7d91fb5 100644 --- a/src/imgui/window/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -1,5 +1,8 @@ #include "spritesheet_editor.h" +#include +#include + #include "math_.h" #include "tool.h" #include "types.h" @@ -20,17 +23,20 @@ namespace anm2ed::imgui void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources) { + auto& document = *manager.get(); auto& anm2 = document.anm2; auto& reference = document.reference; - auto& referenceSpritesheet = document.referenceSpritesheet; + auto& referenceSpritesheet = document.spritesheet.reference; auto& pan = document.editorPan; auto& zoom = document.editorZoom; auto& backgroundColor = settings.editorBackgroundColor; auto& gridColor = settings.editorGridColor; auto& gridSize = settings.editorGridSize; auto& gridOffset = settings.editorGridOffset; + auto& toolColor = settings.toolColor; auto& isGrid = settings.editorIsGrid; + auto& isGridSnap = settings.editorIsGridSnap; auto& zoomStep = settings.viewZoomStep; auto& isBorder = settings.editorIsBorder; auto spritesheet = document.spritesheet_get(); @@ -39,6 +45,8 @@ namespace anm2ed::imgui auto& shaderTexture = resources.shaders[shader::TEXTURE]; auto& dashedShader = resources.shaders[shader::DASHED]; + auto center_view = [&]() { pan = -size * 0.5f; }; + if (ImGui::Begin("Spritesheet Editor", &settings.windowIsSpritesheetEditor)) { auto childSize = ImVec2(imgui::row_widget_width_get(3), @@ -47,10 +55,19 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Grid Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::Checkbox("Grid", &isGrid); + ImGui::SetItemTooltip("Toggle the visibility of the grid."); + ImGui::SameLine(); + ImGui::Checkbox("Snap", &isGridSnap); + ImGui::SetItemTooltip("Cropping will snap points to the grid."); ImGui::SameLine(); ImGui::ColorEdit4("Color", value_ptr(gridColor), ImGuiColorEditFlags_NoInputs); - ImGui::InputInt2("Size", value_ptr(gridSize)); - ImGui::InputInt2("Offset", value_ptr(gridOffset)); + ImGui::SetItemTooltip("Change the grid's color."); + + input_int2_range("Size", gridSize, ivec2(GRID_SIZE_MIN), ivec2(GRID_SIZE_MAX)); + ImGui::SetItemTooltip("Change the size of all cells in the grid."); + + input_int2_range("Offset", gridOffset, ivec2(GRID_OFFSET_MIN), ivec2(GRID_OFFSET_MAX)); + ImGui::SetItemTooltip("Change the offset of the grid."); } ImGui::EndChild(); @@ -59,11 +76,12 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::InputFloat("Zoom", &zoom, zoomStep, zoomStep, "%.0f%%"); + ImGui::SetItemTooltip("Change the zoom of the editor."); auto widgetSize = ImVec2(imgui::row_widget_width_get(2), 0); imgui::shortcut(settings.shortcutCenterView); - if (ImGui::Button("Center View", widgetSize)) pan = -size * 0.5f; + if (ImGui::Button("Center View", widgetSize)) center_view(); imgui::set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); ImGui::SameLine(); @@ -82,8 +100,10 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); + ImGui::SetItemTooltip("Change the background color."); ImGui::Checkbox("Border", &isBorder); + ImGui::SetItemTooltip("Toggle a border appearing around the spritesheet."); } ImGui::EndChild(); @@ -127,45 +147,85 @@ namespace anm2ed::imgui if (ImGui::IsItemHovered()) { - ImGui::SetKeyboardFocusHere(-1); - - previousMousePos = mousePos; - mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); - - auto isMouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); - auto isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); + auto isMouseLeftClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + auto isMouseLeftReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); + auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); + auto isMouseRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right); + auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); + auto isMouseRightReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Right); auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta); auto mouseWheel = ImGui::GetIO().MouseWheel; - auto& toolColor = settings.toolColor; - auto isZoomIn = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomIn)); - auto isZoomOut = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomOut)); - auto isLeft = imgui::chord_repeating(ImGuiKey_LeftArrow); - auto isRight = imgui::chord_repeating(ImGuiKey_RightArrow); - auto isUp = imgui::chord_repeating(ImGuiKey_UpArrow); - auto isDown = imgui::chord_repeating(ImGuiKey_DownArrow); - auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); - auto step = isMod ? canvas::STEP_FAST : canvas::STEP; - auto useTool = tool; - auto isMouseClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left); - auto isMouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); - auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false); - auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false); - auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false); - auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false); + auto isMouseClicked = isMouseLeftClicked || isMouseRightClicked; + auto isMouseDown = isMouseLeftDown || isMouseRightDown; + auto isMouseReleased = isMouseLeftReleased || isMouseRightReleased; + + auto isLeftJustPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false); + auto isRightJustPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false); + auto isUpJustPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false); + auto isDownJustPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false); + auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow); + auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow); + auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow); + auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow); + auto isLeftDown = ImGui::IsKeyDown(ImGuiKey_LeftArrow); + auto isRightDown = ImGui::IsKeyDown(ImGuiKey_RightArrow); + auto isUpDown = ImGui::IsKeyDown(ImGuiKey_UpArrow); + auto isDownDown = ImGui::IsKeyDown(ImGuiKey_DownArrow); auto isLeftReleased = ImGui::IsKeyReleased(ImGuiKey_LeftArrow); auto isRightReleased = ImGui::IsKeyReleased(ImGuiKey_RightArrow); auto isUpReleased = ImGui::IsKeyReleased(ImGuiKey_UpArrow); auto isDownReleased = ImGui::IsKeyReleased(ImGuiKey_DownArrow); - auto frame = document.frame_get(); - auto isKeyPressed = isLeftPressed || isRightPressed || isUpPressed || isDownPressed; + auto isKeyJustPressed = isLeftJustPressed || isRightJustPressed || isUpJustPressed || isDownJustPressed; + auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown; auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; - auto isBegin = isMouseClick || isKeyPressed; + + auto isZoomIn = chord_repeating(string_to_chord(settings.shortcutZoomIn)); + auto isZoomOut = chord_repeating(string_to_chord(settings.shortcutZoomOut)); + + auto isBegin = isMouseClicked || isKeyJustPressed; + auto isDuring = isMouseDown || isKeyDown; auto isEnd = isMouseReleased || isKeyReleased; - if (isMouseMiddleDown) useTool = tool::PAN; + auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); - ImGui::SetMouseCursor(tool::INFO[useTool].cursor); + auto frame = document.frame_get(); + auto useTool = tool; + auto step = isMod ? canvas::STEP_FAST : canvas::STEP; + auto stepX = isGridSnap ? step * gridSize.x : step; + auto stepY = isGridSnap ? step * gridSize.y : step; + previousMousePos = mousePos; + mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); + + auto snap_rect = [&](glm::vec2 minPoint, glm::vec2 maxPoint) + { + if (isGridSnap) + { + if (gridSize.x != 0) + { + minPoint.x = std::floor(minPoint.x / gridSize.x) * gridSize.x; + maxPoint.x = std::ceil(maxPoint.x / gridSize.x) * gridSize.x; + } + if (gridSize.y != 0) + { + minPoint.y = std::floor(minPoint.y / gridSize.y) * gridSize.y; + maxPoint.y = std::ceil(maxPoint.y / gridSize.y) * gridSize.y; + } + } + return std::pair{minPoint, maxPoint}; + }; + + if (isMouseMiddleDown) useTool = tool::PAN; + if (tool == tool::MOVE && isMouseRightDown) useTool = tool::CROP; + if (tool == tool::CROP && isMouseRightDown) useTool = tool::MOVE; + if (tool == tool::DRAW && isMouseRightDown) useTool = tool::ERASE; + if (tool == tool::ERASE && isMouseRightDown) useTool = tool::DRAW; + + auto& areaType = tool::INFO[useTool].areaType; + auto cursor = areaType == tool::SPRITESHEET_EDITOR || areaType == tool::ALL ? tool::INFO[useTool].cursor + : ImGuiMouseCursor_NotAllowed; + ImGui::SetMouseCursor(cursor); + ImGui::SetKeyboardFocusHere(-1); switch (useTool) { @@ -176,36 +236,84 @@ namespace anm2ed::imgui if (!frame) break; if (isBegin) document.snapshot("Frame Pivot"); if (isMouseDown) frame->pivot = ivec2(mousePos - frame->crop); - if (isLeft) frame->pivot.x -= step; - if (isRight) frame->pivot.x += step; - if (isUp) frame->pivot.y -= step; - if (isDown) frame->pivot.y += step; + if (isLeftPressed) frame->pivot.x -= step; + if (isRightPressed) frame->pivot.x += step; + if (isUpPressed) frame->pivot.y -= step; + if (isDownPressed) frame->pivot.y += step; + if (isDuring) + { + if (ImGui::BeginTooltip()) + { + auto pivotFormat = math::vec2_format_get(frame->pivot); + auto pivotString = std::format("Pivot: ({}, {})", pivotFormat, pivotFormat); + ImGui::Text(pivotString.c_str(), frame->pivot.x, frame->pivot.y); + ImGui::EndTooltip(); + } + } + if (isEnd) document.change(Document::FRAMES); break; case tool::CROP: if (!frame) break; - if (isBegin) document.snapshot(isMod ? "Frame Size" : "Frame Crop"); - if (isMouseClicked) frame->crop = ivec2(mousePos); - if (isMouseDown) frame->size = ivec2(mousePos - frame->crop); - if (isLeft) isMod ? frame->size.x -= step : frame->crop.x -= step; - if (isRight) isMod ? frame->size.x += step : frame->crop.x += step; - if (isUp) isMod ? frame->size.y -= step : frame->crop.y -= step; - if (isDown) isMod ? frame->size.y += step : frame->crop.y += step; + if (isBegin) document.snapshot("Frame Crop"); + + if (isMouseClicked) + { + cropAnchor = mousePos; + frame->crop = cropAnchor; + frame->size = vec2(); + } + if (isMouseDown) + { + auto [minPoint, maxPoint] = snap_rect(glm::min(cropAnchor, mousePos), glm::max(cropAnchor, mousePos)); + frame->crop = minPoint; + frame->size = maxPoint - minPoint; + } + if (isLeftPressed) frame->crop.x -= stepX; + if (isRightPressed) frame->crop.x += stepX; + if (isUpPressed) frame->crop.y -= stepY; + if (isDownPressed) frame->crop.y += stepY; + if (isDuring) + { + if (!isMouseDown) + { + auto minPoint = glm::min(frame->crop, frame->crop + frame->size); + auto maxPoint = glm::max(frame->crop, frame->crop + frame->size); + frame->crop = minPoint; + frame->size = maxPoint - minPoint; + if (isGridSnap) + { + auto [snapMin, snapMax] = snap_rect(frame->crop, frame->crop + frame->size); + frame->crop = snapMin; + frame->size = snapMax - snapMin; + } + } + if (ImGui::BeginTooltip()) + { + auto cropFormat = math::vec2_format_get(frame->crop); + auto sizeFormat = math::vec2_format_get(frame->size); + auto cropString = std::format("Crop: ({}, {})", cropFormat, cropFormat); + auto sizeString = std::format("Size: ({}, {})", sizeFormat, sizeFormat); + ImGui::Text(cropString.c_str(), frame->crop.x, frame->crop.y); + ImGui::Text(sizeString.c_str(), frame->size.x, frame->size.y); + ImGui::EndTooltip(); + } + } if (isEnd) document.change(Document::FRAMES); break; case tool::DRAW: case tool::ERASE: { if (!spritesheet) break; - auto color = tool == tool::DRAW ? toolColor : vec4(); - if (isMouseClicked) document.snapshot(tool == tool::DRAW ? "Draw" : "Erase"); + auto color = useTool == tool::DRAW ? toolColor : vec4(); + if (isMouseClicked) document.snapshot(useTool == tool::DRAW ? "Draw" : "Erase"); if (isMouseDown) spritesheet->texture.pixel_line(ivec2(previousMousePos), ivec2(mousePos), color); - if (isMouseReleased) document.change(Document::FRAMES); + if (isMouseReleased) document.change(Document::SPRITESHEETS); break; } case tool::COLOR_PICKER: { - if (isMouseDown) + if (isDuring) { auto position = to_vec2(ImGui::GetMousePos()); toolColor = pixel_read(position, {settings.windowSize.x, settings.windowSize.y}); @@ -222,9 +330,26 @@ namespace anm2ed::imgui } if (mouseWheel != 0 || isZoomIn || isZoomOut) - zoom_set(zoom, pan, mousePos, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); + { + auto focus = mouseWheel != 0 ? vec2(mousePos) : vec2(); + if (auto spritesheet = document.spritesheet_get(); spritesheet && mouseWheel == 0) + focus = spritesheet->texture.size / 2; + + zoom_set(zoom, pan, focus, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); + } } } ImGui::End(); + + if (!document.isSpritesheetEditorSet) + { + size = settings.editorSize; + zoom = settings.editorStartZoom; + center_view(); + document.isSpritesheetEditorSet = true; + } + + settings.editorSize = size; + settings.editorStartZoom = zoom; } } diff --git a/src/imgui/window/spritesheet_editor.h b/src/imgui/window/spritesheet_editor.h index 55db23d..8c992fe 100644 --- a/src/imgui/window/spritesheet_editor.h +++ b/src/imgui/window/spritesheet_editor.h @@ -11,6 +11,7 @@ namespace anm2ed::imgui { glm::vec2 mousePos{}; glm::vec2 previousMousePos{}; + glm::vec2 cropAnchor{}; public: SpritesheetEditor(); diff --git a/src/imgui/window/spritesheets.cpp b/src/imgui/window/spritesheets.cpp index 2d22b13..d271f1c 100644 --- a/src/imgui/window/spritesheets.cpp +++ b/src/imgui/window/spritesheets.cpp @@ -2,6 +2,7 @@ #include +#include "document.h" #include "toast.h" using namespace anm2ed::types; @@ -15,10 +16,10 @@ namespace anm2ed::imgui { auto& document = *manager.get(); auto& anm2 = document.anm2; - auto& multiSelect = document.spritesheetMultiSelect; - auto& unused = document.unusedSpritesheetIDs; - auto& hovered = document.hoveredSpritesheet; - auto& reference = document.referenceSpritesheet; + auto& selection = document.spritesheet.selection; + auto& unused = document.spritesheet.unused; + auto& hovered = document.spritesheet.hovered; + auto& reference = document.spritesheet.reference; hovered = -1; @@ -30,10 +31,10 @@ namespace anm2ed::imgui { auto copy = [&]() { - if (!multiSelect.empty()) + if (!selection.empty()) { std::string clipboardText{}; - for (auto& id : multiSelect) + for (auto& id : selection) clipboardText += anm2.content.spritesheets[id].to_string(id); clipboard.set(clipboardText); } @@ -43,43 +44,38 @@ namespace anm2ed::imgui auto paste = [&](merge::Type type) { - auto clipboardText = clipboard.get(); - document.spritesheets_deserialize(clipboardText, type); + std::string errorString{}; + document.snapshot("Paste Spritesheet(s)"); + if (anm2.spritesheets_deserialize(clipboard.get(), document.directory_get(), type, &errorString)) + document.change(Document::SPRITESHEETS); + else + toasts.error(std::format("Failed to deserialize spritesheet(s): {}", errorString)); }; - if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); + if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - ImGui::BeginDisabled(); - ImGui::MenuItem("Cut", settings.shortcutCut.c_str()); - ImGui::EndDisabled(); + ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, true); - ImGui::BeginDisabled(multiSelect.empty() && hovered == -1); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); - ImGui::EndDisabled(); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !selection.empty() || hovered > -1)) copy(); - ImGui::BeginDisabled(clipboard.is_empty()); + if (ImGui::BeginMenu("Paste", !clipboard.is_empty())) { - if (ImGui::BeginMenu("Paste")) - { - if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); - if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); + if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND); + if (ImGui::MenuItem("Replace")) paste(merge::REPLACE); - ImGui::EndMenu(); - } + ImGui::EndMenu(); } - ImGui::EndDisabled(); - ImGui::EndPopup(); } ImGui::PopStyleVar(2); }; - auto childSize = imgui::size_without_footer_get(2); + auto childSize = size_without_footer_get(2); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); @@ -89,7 +85,7 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); - multiSelect.start(anm2.content.spritesheets.size()); + selection.start(anm2.content.spritesheets.size()); for (auto& [id, spritesheet] : anm2.content.spritesheets) { @@ -97,7 +93,7 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders)) { - auto isSelected = multiSelect.contains(id); + auto isSelected = selection.contains(id); auto isReferenced = id == reference; auto cursorPos = ImGui::GetCursorPos(); auto& texture = spritesheet.texture.is_valid() ? spritesheet.texture : resources.icons[icon::NONE]; @@ -177,7 +173,7 @@ namespace anm2ed::imgui ImGui::PopID(); } - multiSelect.finish(); + selection.finish(); ImGui::PopStyleVar(2); @@ -185,11 +181,11 @@ namespace anm2ed::imgui } ImGui::EndChild(); - auto rowOneWidgetSize = imgui::widget_size_with_row_get(4); + auto rowOneWidgetSize = widget_size_with_row_get(4); - imgui::shortcut(settings.shortcutAdd); + shortcut(settings.shortcutAdd); if (ImGui::Button("Add", rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_OPEN); - imgui::set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd); + set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd); if (dialog.is_selected(dialog::SPRITESHEET_OPEN)) { @@ -199,16 +195,21 @@ namespace anm2ed::imgui ImGui::SameLine(); - ImGui::BeginDisabled(multiSelect.empty()); + ImGui::BeginDisabled(selection.empty()); { if (ImGui::Button("Reload", rowOneWidgetSize)) { - for (auto& id : multiSelect) + auto reload = [&]() { - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - spritesheet.reload(document.directory_get()); - toasts.info(std::format("Reloaded spritesheet #{}: {}", id, spritesheet.path.string())); - } + for (auto& id : selection) + { + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; + spritesheet.reload(document.directory_get()); + toasts.info(std::format("Reloaded spritesheet #{}: {}", id, spritesheet.path.string())); + } + }; + + DOCUMENT_EDIT(document, "Reload Spritesheet(s)", Document::SPRITESHEETS, reload()); } ImGui::SetItemTooltip("Reloads the selected spritesheets."); } @@ -216,7 +217,7 @@ namespace anm2ed::imgui ImGui::SameLine(); - ImGui::BeginDisabled(multiSelect.size() != 1); + ImGui::BeginDisabled(selection.size() != 1); { if (ImGui::Button("Replace", rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_REPLACE); ImGui::SetItemTooltip("Replace the selected spritesheet with a new one."); @@ -225,10 +226,15 @@ namespace anm2ed::imgui if (dialog.is_selected(dialog::SPRITESHEET_REPLACE)) { - auto& id = *multiSelect.begin(); - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - spritesheet = anm2::Spritesheet(document.directory_get(), dialog.path); - toasts.info(std::format("Replaced spritesheet #{}: {}", id, spritesheet.path.string())); + auto replace = [&]() + { + auto& id = *selection.begin(); + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; + spritesheet = anm2::Spritesheet(document.directory_get(), dialog.path); + toasts.info(std::format("Replaced spritesheet #{}: {}", id, spritesheet.path.string())); + }; + + DOCUMENT_EDIT(document, "Replace Spritesheet", Document::SPRITESHEETS, replace()); dialog.reset(); } @@ -236,50 +242,54 @@ namespace anm2ed::imgui ImGui::BeginDisabled(unused.empty()); { - imgui::shortcut(settings.shortcutRemove); + shortcut(settings.shortcutRemove); if (ImGui::Button("Remove Unused", rowOneWidgetSize)) { - for (auto& id : unused) + auto remove_unused = [&]() { - anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - toasts.info(std::format("Removed spritesheet #{}: {}", id, spritesheet.path.string())); - anm2.spritesheet_remove(id); - } - unused.clear(); - document.change(Document::SPRITESHEETS); + for (auto& id : unused) + { + anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; + toasts.info(std::format("Removed spritesheet #{}: {}", id, spritesheet.path.string())); + anm2.content.spritesheets.erase(id); + } + unused.clear(); + }; + + DOCUMENT_EDIT(document, "Remove Unused Spritesheets", Document::SPRITESHEETS, remove_unused()); } - imgui::set_item_tooltip_shortcut("Remove all unused spritesheets (i.e., not used in any layer.).", - settings.shortcutRemove); + set_item_tooltip_shortcut("Remove all unused spritesheets (i.e., not used in any layer.).", + settings.shortcutRemove); } ImGui::EndDisabled(); - auto rowTwoWidgetSize = imgui::widget_size_with_row_get(3); + auto rowTwoWidgetSize = widget_size_with_row_get(3); - imgui::shortcut(settings.shortcutSelectAll); - ImGui::BeginDisabled(multiSelect.size() == anm2.content.spritesheets.size()); + shortcut(settings.shortcutSelectAll); + ImGui::BeginDisabled(selection.size() == anm2.content.spritesheets.size()); { if (ImGui::Button("Select All", rowTwoWidgetSize)) for (auto& id : anm2.content.spritesheets | std::views::keys) - multiSelect.insert(id); + selection.insert(id); } ImGui::EndDisabled(); - imgui::set_item_tooltip_shortcut("Select all spritesheets.", settings.shortcutSelectAll); + set_item_tooltip_shortcut("Select all spritesheets.", settings.shortcutSelectAll); ImGui::SameLine(); - imgui::shortcut(settings.shortcutSelectNone); - ImGui::BeginDisabled(multiSelect.empty()); - if (ImGui::Button("Select None", rowTwoWidgetSize)) multiSelect.clear(); - imgui::set_item_tooltip_shortcut("Unselect all spritesheets.", settings.shortcutSelectNone); + shortcut(settings.shortcutSelectNone); + ImGui::BeginDisabled(selection.empty()); + if (ImGui::Button("Select None", rowTwoWidgetSize)) selection.clear(); + set_item_tooltip_shortcut("Unselect all spritesheets.", settings.shortcutSelectNone); ImGui::EndDisabled(); ImGui::SameLine(); - ImGui::BeginDisabled(multiSelect.empty()); + ImGui::BeginDisabled(selection.empty()); { if (ImGui::Button("Save", rowTwoWidgetSize)) { - for (auto& id : multiSelect) + for (auto& id : selection) { anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; if (spritesheet.save(document.directory_get())) diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index f845b1a..68b33c5 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -4,6 +4,8 @@ #include +#include "toast.h" + using namespace anm2ed::resource; using namespace anm2ed::types; using namespace glm; @@ -48,41 +50,59 @@ namespace anm2ed::imgui void Timeline::context_menu(Document& document, Settings& settings, Clipboard& clipboard) { auto& hoveredFrame = document.hoveredFrame; + auto& anm2 = document.anm2; + auto& reference = document.reference; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); auto copy = [&]() { - if (auto frame = document.anm2.frame_get(hoveredFrame)) - { - clipboard.set(frame->to_string(hoveredFrame.itemType)); - } + if (auto frame = anm2.frame_get(hoveredFrame)) clipboard.set(frame->to_string(hoveredFrame.itemType)); }; auto cut = [&]() { copy(); - document.frames_delete(document.item_get()); + auto frames_delete = [&]() + { + if (auto item = anm2.item_get(reference); item) + { + item->frames.erase(item->frames.begin() + reference.frameIndex); + reference.frameIndex = glm::max(-1, --reference.frameIndex); + } + }; + + DOCUMENT_EDIT(document, "Cut Frame(s)", Document::FRAMES, frames_delete()); }; - auto paste = [&]() { document.frames_deserialize(clipboard.get()); }; + auto paste = [&]() + { + if (auto item = document.item_get()) + { + document.snapshot("Paste Frame(s)"); + std::set indices{}; + std::string errorString{}; + auto start = reference.frameIndex + 1; + if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) + document.change(Document::FRAMES); + else + toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); + } + else + toasts.error(std::format("Failed to deserialize frame(s): select an item first!")); + }; - if (imgui::shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); - if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); + if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); + if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); + if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - ImGui::BeginDisabled(hoveredFrame == anm2::REFERENCE_DEFAULT); - if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str())) cut(); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy(); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(clipboard.is_empty()); - if (ImGui::MenuItem("Paste")) paste(); - ImGui::EndDisabled(); + if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, hoveredFrame != anm2::Reference{})) cut(); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, hoveredFrame != anm2::Reference{})) copy(); + if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste(); ImGui::EndPopup(); } @@ -165,7 +185,7 @@ namespace anm2ed::imgui if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference; } - ImGui::Image(resources.icons[icon].id, imgui::icon_size_get()); + ImGui::Image(resources.icons[icon].id, icon_size_get()); ImGui::SameLine(); ImGui::TextUnformatted(label.c_str()); @@ -181,8 +201,8 @@ namespace anm2ed::imgui (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE; - if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, imgui::icon_size_get())) - document.item_visible_toggle(item); + if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) + DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, isVisible = !isVisible); ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show."); if (type == anm2::NULL_) @@ -194,8 +214,8 @@ namespace anm2ed::imgui 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, imgui::icon_size_get())) - document.null_rect_toggle(null); + if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get())) + DOCUMENT_EDIT(document, "Null Rect", Document::FRAMES, null.isShowRect = !null.isShowRect); ImGui::SetItemTooltip(isShowRect ? "The null's rect is shown. Press to hide." : "The null's rect is hidden. Press to show."); } @@ -217,7 +237,7 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; - if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, imgui::icon_size_get())) + if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, icon_size_get())) isShowUnused = !isShowUnused; ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide." : "Unused layers/nulls are hidden. Press to show."); @@ -229,7 +249,7 @@ namespace anm2ed::imgui ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, imgui::icon_size_get())) + if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get())) isOnlyShowLayers = !isOnlyShowLayers; ImGui::SetItemTooltip(isOnlyShowLayers ? "Only layers are visible. Press to show all items." : "All items are visible. Press to only show layers."); @@ -329,21 +349,30 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + style.WindowPadding.x, ImGui::GetCursorPosY())); - auto widgetSize = imgui::widget_size_with_row_get(2, ImGui::GetContentRegionAvail().x - style.WindowPadding.x); + auto widgetSize = widget_size_with_row_get(2, ImGui::GetContentRegionAvail().x - style.WindowPadding.x); ImGui::BeginDisabled(!animation); { - imgui::shortcut(settings.shortcutAdd); + shortcut(settings.shortcutAdd); if (ImGui::Button("Add", widgetSize)) propertiesPopup.open(); - imgui::set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd); + set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd); ImGui::SameLine(); ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER && reference.itemType != anm2::NULL_); { - imgui::shortcut(settings.shortcutRemove); - if (ImGui::Button("Remove", widgetSize)) document.item_remove(animation); - imgui::set_item_tooltip_shortcut("Remove the selected items from the animation.", settings.shortcutRemove); + shortcut(settings.shortcutRemove); + if (ImGui::Button("Remove", widgetSize)) + { + auto remove = [&]() + { + animation->item_remove(reference.itemType, reference.itemID); + reference = {reference.animationIndex}; + }; + + DOCUMENT_EDIT(document, "Remove Item", Document::ITEMS, remove()); + } + set_item_tooltip_shortcut("Remove the selected items from the animation.", settings.shortcutRemove); } ImGui::EndDisabled(); } @@ -534,11 +563,12 @@ namespace anm2ed::imgui if (ImGui::Button("##Frame Button", size)) { if (type != anm2::TRIGGER && ImGui::IsKeyDown(ImGuiMod_Alt)) - document.frame_is_interpolated_set(&frame, !frame.isInterpolated); + DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, + frame.isInterpolated = !frame.isInterpolated); if (type == anm2::LAYER) { - document.referenceSpritesheet = anm2.content.layers[id].spritesheetID; - document.layersMultiSelect = {id}; + document.spritesheet.reference = anm2.content.layers[id].spritesheetID; + document.layer.selection = {id}; } reference = frameReference; reference.frameTime = frameTime; @@ -575,8 +605,8 @@ namespace anm2ed::imgui Clipboard& clipboard) { auto& anm2 = document.anm2; + auto& reference = document.reference; auto& playback = document.playback; - auto& hoveredFrame = document.hoveredFrame; auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15; @@ -697,16 +727,16 @@ namespace anm2ed::imgui ImGui::SetCursorPos( ImVec2(ImGui::GetStyle().WindowPadding.x, ImGui::GetCursorPos().y + ImGui::GetStyle().ItemSpacing.y)); - auto widgetSize = imgui::widget_size_with_row_get(10); + auto widgetSize = widget_size_with_row_get(10); ImGui::BeginDisabled(!animation); { auto label = playback.isPlaying ? "Pause" : "Play"; auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation."; - imgui::shortcut(settings.shortcutPlayPause); + shortcut(settings.shortcutPlayPause); if (ImGui::Button(label, widgetSize)) playback.toggle(); - imgui::set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); + set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); ImGui::SameLine(); @@ -714,17 +744,45 @@ namespace anm2ed::imgui ImGui::BeginDisabled(!item); { - imgui::shortcut(settings.shortcutAdd); - if (ImGui::Button("Insert Frame", widgetSize)) document.frames_add(item); - imgui::set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd); + shortcut(settings.shortcutAdd); + if (ImGui::Button("Insert Frame", widgetSize)) + { + auto insert_frame = [&]() + { + auto frame = document.frame_get(); + if (frame) + { + item->frames.insert(item->frames.begin() + reference.frameIndex, *frame); + reference.frameIndex++; + } + else if (!item->frames.empty()) + { + auto frame = item->frames.back(); + item->frames.emplace_back(frame); + reference.frameIndex = item->frames.size() - 1; + } + }; + + DOCUMENT_EDIT(document, "Insert Frame", Document::FRAMES, insert_frame()); + } + set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd); ImGui::SameLine(); ImGui::BeginDisabled(!document.frame_get()); { - imgui::shortcut(settings.shortcutRemove); - if (ImGui::Button("Delete Frame", widgetSize)) document.frames_delete(item); - imgui::set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove); + shortcut(settings.shortcutRemove); + if (ImGui::Button("Delete Frame", widgetSize)) + { + auto delete_frame = [&]() + { + item->frames.erase(item->frames.begin() + reference.frameIndex); + reference.frameIndex = glm::max(-1, --reference.frameIndex); + }; + + DOCUMENT_EDIT(document, "Delete Frame(s)", Document::FRAMES, delete_frame()); + } + set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove); ImGui::SameLine(); @@ -743,8 +801,8 @@ namespace anm2ed::imgui ImGui::SameLine(); ImGui::SetNextItemWidth(widgetSize.x); - ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value(), imgui::STEP, - imgui::STEP_FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); + ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value(), STEP, STEP_FAST, + !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX); ImGui::SetItemTooltip("Set the animation's length."); @@ -766,7 +824,7 @@ namespace anm2ed::imgui ImGui::SameLine(); ImGui::SetNextItemWidth(widgetSize.x); - imgui::input_text_string("Author", &anm2.info.createdBy); + input_text_string("Author", &anm2.info.createdBy); ImGui::SetItemTooltip("Set the author of the document."); ImGui::SameLine(); @@ -818,8 +876,8 @@ namespace anm2ed::imgui isUnusedItemsSet = true; } - auto footerSize = imgui::footer_size_get(); - auto optionsSize = imgui::child_size_get(11); + auto footerSize = footer_size_get(); + auto optionsSize = child_size_get(11); auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - (optionsSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 4)); if (ImGui::BeginChild("Options", optionsSize, ImGuiChildFlags_Borders)) @@ -890,12 +948,11 @@ namespace anm2ed::imgui ImGui::BeginDisabled(source == source::EXISTING); { - imgui::input_text_string("Name", &addItemName); + input_text_string("Name", &addItemName); ImGui::SetItemTooltip("Set the item's name."); ImGui::BeginDisabled(type != anm2::LAYER); { - auto spritesheets = anm2.spritesheet_names_get(); - imgui::combo_strings("Spritesheet", &addItemSpritesheetID, spritesheets); + combo_negative_one_indexed("Spritesheet", &addItemSpritesheetID, document.spritesheet.labels); ImGui::SetItemTooltip("Set the layer item's spritesheet."); } ImGui::EndDisabled(); @@ -934,11 +991,24 @@ namespace anm2ed::imgui } ImGui::EndChild(); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button("Add", widgetSize)) { - document.item_add((anm2::Type)type, addItemID, addItemName, (locale::Type)locale, addItemSpritesheetID); + anm2::Reference addReference{}; + + document.snapshot("Add Item"); + if (type == anm2::LAYER) + addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, + addItemSpritesheetID - 1, (locale::Type)locale); + else if (type == anm2::NULL_) + addReference = anm2.null_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, + (locale::Type)locale); + + document.change(Document::ITEMS); + + reference = addReference; + item_properties_close(); } ImGui::SetItemTooltip("Add the item, with the settings specified."); @@ -961,7 +1031,7 @@ namespace anm2ed::imgui auto frame = document.frame_get(); - imgui::input_int_range("Interval", interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); + input_int_range("Interval", interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); ImGui::SetItemTooltip("Set the maximum delay of each frame that will be baked."); ImGui::Checkbox("Round Rotation", &isRoundRotation); @@ -970,11 +1040,13 @@ namespace anm2ed::imgui ImGui::Checkbox("Round Scale", &isRoundScale); ImGui::SetItemTooltip("Scale will be rounded to the nearest whole number."); - auto widgetSize = imgui::widget_size_with_row_get(2); + auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button("Bake", widgetSize)) { - document.frames_bake(interval, isRoundScale, isRoundRotation); + if (auto item = document.item_get()) + DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES, + item->frames_bake(reference.frameIndex, interval, isRoundScale, isRoundRotation)); bakePopup.close(); } ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected."); @@ -1009,24 +1081,41 @@ namespace anm2ed::imgui popups(document, animation, settings); - if (imgui::shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle(); + if (shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle(); if (animation) { - if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutPreviousFrame))) + if (chord_repeating(string_to_chord(settings.shortcutPreviousFrame))) { playback.decrement(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); reference.frameTime = playback.time; } - if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutNextFrame))) + if (chord_repeating(string_to_chord(settings.shortcutNextFrame))) { playback.increment(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); reference.frameTime = playback.time; } } - if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutShortenFrame))) document.frame_shorten(); - if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutExtendFrame))) document.frame_extend(); + if (ImGui::IsKeyChordPressed(string_to_chord(settings.shortcutShortenFrame))) document.snapshot("Shorten Frame"); + if (chord_repeating(string_to_chord(settings.shortcutShortenFrame))) + { + if (auto frame = document.frame_get()) + { + frame->shorten(); + document.change(Document::FRAMES); + } + } + + if (ImGui::IsKeyChordPressed(string_to_chord(settings.shortcutExtendFrame))) document.snapshot("Extend Frame"); + if (chord_repeating(string_to_chord(settings.shortcutExtendFrame))) + { + if (auto frame = document.frame_get()) + { + frame->extend(); + document.change(Document::FRAMES); + } + } } } \ No newline at end of file diff --git a/src/imgui/window/timeline.h b/src/imgui/window/timeline.h index 3c0560d..94a9552 100644 --- a/src/imgui/window/timeline.h +++ b/src/imgui/window/timeline.h @@ -16,9 +16,9 @@ namespace anm2ed::imgui PopupHelper propertiesPopup{PopupHelper("Item Properties", POPUP_NORMAL)}; PopupHelper bakePopup{PopupHelper("Bake", POPUP_TO_CONTENT)}; std::string addItemName{}; - int addItemSpritesheetID{}; bool addItemIsRect{}; int addItemID{-1}; + int addItemSpritesheetID{-1}; bool isUnusedItemsSet{}; std::set unusedItems{}; glm::vec2 scroll{}; diff --git a/src/imgui/window/welcome.cpp b/src/imgui/window/welcome.cpp index 38c69c2..b9d997f 100644 --- a/src/imgui/window/welcome.cpp +++ b/src/imgui/window/welcome.cpp @@ -31,12 +31,10 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button("New", widgetSize)) dialog.file_open(dialog::ANM2_NEW); // handled in taskbar.cpp - ImGui::SameLine(); - if (ImGui::Button("Open", widgetSize)) dialog.file_open(dialog::ANM2_OPEN); // handled in taskbar.cpp - if (ImGui::BeginChild("##Recent Files Child", ImVec2(), ImGuiChildFlags_Borders)) + if (ImGui::BeginChild("##Recent Files Child", {}, ImGuiChildFlags_Borders)) { for (auto [i, file] : std::views::enumerate(manager.recentFiles)) { diff --git a/src/manager.cpp b/src/manager.cpp index 7d47128..086d080 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -90,20 +90,23 @@ namespace anm2ed void Manager::autosave(Document& document) { - auto filename = "." + document.filename_get().string() + ".autosave"; - auto path = document.directory_get() / filename; std::string errorString{}; - document.autosave(path, &errorString); + auto autosavePath = document.autosave_path_get(); + document.autosave(&errorString); - autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), path), autosaveFiles.end()); - autosaveFiles.insert(autosaveFiles.begin(), path); + autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), autosavePath), autosaveFiles.end()); + autosaveFiles.insert(autosaveFiles.begin(), autosavePath); autosave_files_write(); } void Manager::close(int index) { - if (index < 0 || index >= (int)documents.size()) return; + if (!vector::in_bounds(documents, index)) return; + + autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), get()->autosave_path_get()), + autosaveFiles.end()); + autosave_files_write(); documents.erase(documents.begin() + index); @@ -146,7 +149,7 @@ namespace anm2ed else editLayer = document->anm2.content.layers.at(id); - document->referenceLayer = id; + document->layer.reference = id; layerPropertiesPopup.open(); } @@ -177,7 +180,7 @@ namespace anm2ed else editNull = document->anm2.content.nulls.at(id); - document->referenceNull = id; + document->null.reference = id; nullPropertiesPopup.open(); } diff --git a/src/manager.h b/src/manager.h index 549de70..485b309 100644 --- a/src/manager.h +++ b/src/manager.h @@ -22,8 +22,10 @@ namespace anm2ed int pendingSelected{-1}; bool isRecording{}; + bool isRecordingStart{}; int recordingStart{}; int recordingEnd{}; + bool isRecordingRange{}; anm2::Layer editLayer{}; imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; diff --git a/src/render.h b/src/render.h index 6e9cdfb..22f42d8 100644 --- a/src/render.h +++ b/src/render.h @@ -5,21 +5,27 @@ namespace anm2ed::render { #define RENDER_LIST \ - X(PNGS, "PNGs") \ - X(GIF, "GIF") \ - X(WEBM, "WebM") \ - X(MP4, "MP4") + X(PNGS, "PNGs", "") \ + X(GIF, "GIF", ".gif") \ + X(WEBM, "WebM", ".webm") \ + X(MP4, "MP4", ".mp4") enum Type { -#define X(symbol, string) symbol, +#define X(symbol, string, extension) symbol, RENDER_LIST #undef X COUNT }; constexpr const char* STRINGS[] = { -#define X(symbol, string) string, +#define X(symbol, string, extension) string, + RENDER_LIST +#undef X + }; + + constexpr const char* EXTENSIONS[] = { +#define X(symbol, string, extension) extension, RENDER_LIST #undef X }; diff --git a/src/resource/audio.cpp b/src/resource/audio.cpp index 11979f2..af04685 100644 --- a/src/resource/audio.cpp +++ b/src/resource/audio.cpp @@ -22,9 +22,9 @@ namespace anm2ed::resource internal = nullptr; } - void Audio::play() + void Audio::play(MIX_Mixer* mixer) { - MIX_PlayAudio(mixer_get(), internal); + MIX_PlayAudio(mixer ? mixer : mixer_get(), internal); } Audio::Audio(Audio&& other) noexcept @@ -46,4 +46,9 @@ namespace anm2ed::resource { unload(); } + + bool Audio::is_valid() + { + return internal; + } } diff --git a/src/resource/audio.h b/src/resource/audio.h index 6c2cc52..569ae8a 100644 --- a/src/resource/audio.h +++ b/src/resource/audio.h @@ -19,6 +19,7 @@ namespace anm2ed::resource Audio(const Audio&) = delete; Audio& operator=(const Audio&) = delete; - void play(); + bool is_valid(); + void play(MIX_Mixer* = nullptr); }; } diff --git a/src/resource/icon.h b/src/resource/icon.h index 147a884..4101ddf 100644 --- a/src/resource/icon.h +++ b/src/resource/icon.h @@ -129,6 +129,10 @@ namespace anm2ed::resource::icon )"; + constexpr auto TARGET_ALT_DATA = R"( + + )"; + constexpr auto INTERPOLATED_DATA = R"( )"; @@ -185,7 +189,8 @@ namespace anm2ed::resource::icon X(PLAYHEAD, PLAYHEAD_DATA, SIZE_NORMAL) \ X(PIVOT, PIVOT_DATA, SIZE_NORMAL) \ X(POINT, UNINTERPOLATED_DATA, SIZE_NORMAL) \ - X(TARGET, TARGET_DATA, SIZE_HUGE) + X(TARGET, TARGET_DATA, SIZE_HUGE) \ + X(TARGET_ALT, TARGET_ALT_DATA, SIZE_HUGE) enum Type { diff --git a/src/settings.h b/src/settings.h index 61eba79..e25bfd8 100644 --- a/src/settings.h +++ b/src/settings.h @@ -43,11 +43,14 @@ namespace anm2ed /* Symbol / Name / String / Type / Default */ \ X(WINDOW_SIZE, windowSize, "Window Size", IVEC2_WH, {1600, 900}) \ X(IS_VSYNC, isVsync, "Vsync", BOOL, true) \ - X(DISPLAY_SCALE, displayScale, "Display Scale", FLOAT, 1.0f) \ + X(UI_SCALE, uiScale, "UI Scale", FLOAT, 1.0f) \ \ X(FILE_IS_AUTOSAVE, fileIsAutosave, "Autosave", BOOL, true) \ X(FILE_AUTOSAVE_TIME, fileAutosaveTime, "Autosave Time", INT, 1) \ \ + X(KEYBOARD_REPEAT_DELAY, keyboardRepeatDelay, "Repeat Delay", FLOAT, 0.300f) \ + X(KEYBOARD_REPEAT_RATE, keyboardRepeatRate, "Repeat Rate", FLOAT, 0.050f) \ + \ X(VIEW_ZOOM_STEP, viewZoomStep, "Zoom Step", FLOAT, 50.0f) \ \ X(PLAYBACK_IS_LOOP, playbackIsLoop, "Loop", BOOL, true) \ @@ -87,8 +90,7 @@ namespace anm2ed X(PREVIEW_IS_BORDER, previewIsBorder, "Border", BOOL, false) \ X(PREVIEW_IS_ALT_ICONS, previewIsAltIcons, "Alt Icons", BOOL, false) \ X(PREVIEW_OVERLAY_TRANSPARENCY, previewOverlayTransparency, "Alpha", FLOAT, 255) \ - X(PREVIEW_ZOOM, previewZoom, "Zoom", FLOAT, 200.0f) \ - X(PREVIEW_PAN, previewPan, "Pan", VEC2, {}) \ + X(PREVIEW_START_ZOOM, previewStartZoom, "Start Zoom", FLOAT, 200.0f) \ X(PREVIEW_GRID_SIZE, previewGridSize, "Size", IVEC2, {32, 32}) \ X(PREVIEW_GRID_OFFSET, previewGridOffset, "Offset", IVEC2, {}) \ X(PREVIEW_GRID_COLOR, previewGridColor, "Color", VEC4, {1.0f, 1.0f, 1.0f, 0.125f}) \ @@ -109,9 +111,9 @@ namespace anm2ed X(EDITOR_IS_GRID, editorIsGrid, "Grid", BOOL, true) \ X(EDITOR_IS_GRID_SNAP, editorIsGridSnap, "Snap", BOOL, true) \ X(EDITOR_IS_BORDER, editorIsBorder, "Border", BOOL, true) \ - X(EDITOR_ZOOM, editorZoom, "Zoom", FLOAT, 200.0f) \ - X(EDITOR_PAN, editorPan, "Pan", VEC2, {0.0, 0.0}) \ - X(EDITOR_GRID_SIZE, editorGridSize, "Size", IVEC2, {32, 32}) \ + X(EDITOR_START_ZOOM, editorStartZoom, "Zoom", FLOAT, 200.0f) \ + X(EDITOR_SIZE, editorSize, "Size", IVEC2_WH, {1200, 600}) \ + X(EDITOR_GRID_SIZE, editorGridSize, "Grid Size", IVEC2, {32, 32}) \ X(EDITOR_GRID_OFFSET, editorGridOffset, "Offset", IVEC2, {32, 32}) \ X(EDITOR_GRID_COLOR, editorGridColor, "Color", VEC4, {1.0, 1.0, 1.0, 0.125}) \ X(EDITOR_BACKGROUND_COLOR, editorBackgroundColor, "Background Color", VEC4, {0.113, 0.184, 0.286, 1.0}) \ @@ -143,9 +145,7 @@ namespace anm2ed X(RENDER_TYPE, renderType, "Output", INT, render::PNGS) \ X(RENDER_PATH, renderPath, "Path", STRING, ".") \ X(RENDER_FORMAT, renderFormat, "Format", STRING, "{}.png") \ - X(RENDER_IS_USE_ANIMATION_BOUNDS, renderIsUseAnimationBounds, "Use Animation Bounds", BOOL, true) \ - X(RENDER_IS_TRANSPARENT, renderIsTransparent, "Transparent", BOOL, true) \ - X(RENDER_IS_RANGE, renderIsRange, "Range", BOOL, false) \ + X(RENDER_IS_RAW_ANIMATION, renderIsRawAnimation, "Raw Animation", BOOL, true) \ X(RENDER_SCALE, renderScale, "Scale", FLOAT, 1.0f) \ X(RENDER_FFMPEG_PATH, renderFFmpegPath, "FFmpeg Path", STRING, FFMPEG_PATH_DEFAULT) diff --git a/src/snapshots.cpp b/src/snapshots.cpp index 304624e..582b8ba 100644 --- a/src/snapshots.cpp +++ b/src/snapshots.cpp @@ -9,7 +9,7 @@ namespace anm2ed return top == 0; } - void SnapshotStack::push(Snapshot& snapshot) + void SnapshotStack::push(const Snapshot& snapshot) { if (top >= MAX) { @@ -31,34 +31,27 @@ namespace anm2ed top = 0; } - void Snapshots::push(const anm2::Anm2& anm2, anm2::Reference reference, const std::string& message) + void Snapshots::push(const Snapshot& snapshot) { - Snapshot snapshot = {anm2, reference, message}; undoStack.push(snapshot); redoStack.clear(); } - void Snapshots::undo(anm2::Anm2& anm2, anm2::Reference& reference, std::string& message) + void Snapshots::undo() { - if (auto current = undoStack.pop()) + if (auto snapshot = undoStack.pop()) { - Snapshot snapshot = {anm2, reference, message}; - redoStack.push(snapshot); - anm2 = current->anm2; - reference = current->reference; - message = current->message; + redoStack.push(current); + current = *snapshot; } } - void Snapshots::redo(anm2::Anm2& anm2, anm2::Reference& reference, std::string& message) + void Snapshots::redo() { - if (auto current = redoStack.pop()) + if (auto snapshot = redoStack.pop()) { - Snapshot snapshot = {anm2, reference, message}; - undoStack.push(snapshot); - anm2 = current->anm2; - reference = current->reference; - message = current->message; + undoStack.push(current); + current = *snapshot; } } diff --git a/src/snapshots.h b/src/snapshots.h index a18d4a7..3fefd20 100644 --- a/src/snapshots.h +++ b/src/snapshots.h @@ -1,6 +1,8 @@ #pragma once #include "anm2/anm2.h" +#include "playback.h" +#include "storage.h" namespace anm2ed::snapshots { @@ -15,6 +17,14 @@ namespace anm2ed public: anm2::Anm2 anm2{}; anm2::Reference reference{}; + Playback playback{}; + Storage animation{}; + Storage merge{}; + Storage event{}; + Storage layer{}; + Storage null{}; + Storage sound{}; + Storage spritesheet{}; std::string message = snapshots::ACTION; }; @@ -25,7 +35,7 @@ namespace anm2ed int top{}; bool is_empty(); - void push(Snapshot& snapshot); + void push(const Snapshot&); Snapshot* pop(); void clear(); }; @@ -35,11 +45,12 @@ namespace anm2ed public: SnapshotStack undoStack{}; SnapshotStack redoStack{}; + Snapshot current{}; Snapshot* get(); - void push(const anm2::Anm2&, anm2::Reference, const std::string&); - void undo(anm2::Anm2& anm2, anm2::Reference& reference, std::string&); - void redo(anm2::Anm2& anm2, anm2::Reference& reference, std::string&); + void push(const Snapshot&); + void undo(); + void redo(); void reset(); }; } diff --git a/src/state.cpp b/src/state.cpp index ff0fb2a..d6ea49b 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -27,7 +27,6 @@ namespace anm2ed void State::tick(Settings& settings) { - dockspace.tick(manager, settings); if (auto document = manager.get()) { @@ -36,6 +35,8 @@ namespace anm2ed document->playback.tick(document->anm2.info.fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording); } + + dockspace.tick(manager, settings); } void State::update(SDL_Window*& window, Settings& settings) @@ -57,10 +58,8 @@ namespace anm2ed if (auto document = manager.get()) document->spritesheet_add(droppedFile); else - toasts.warning(std::format("Could not open spritesheet: (open a document first!)", droppedFile)); + toasts.info("Failed to add spritesheet! Open a document first."); } - else - toasts.warning(std::format("Could not parse file: {} (must be .anm2 or .png)", droppedFile)); break; } case SDL_EVENT_QUIT: @@ -80,7 +79,9 @@ namespace anm2ed dockspace.update(taskbar, documents, manager, settings, resources, dialog, clipboard); toasts.update(); - ImGui::GetStyle().FontScaleMain = settings.displayScale; + ImGui::GetStyle().FontScaleMain = settings.uiScale; + ImGui::GetIO().KeyRepeatDelay = settings.keyboardRepeatDelay; + ImGui::GetIO().KeyRepeatRate = settings.keyboardRepeatRate; SDL_GetWindowSize(window, &settings.windowSize.x, &settings.windowSize.y); if (isQuitting && manager.documents.empty()) isQuit = true; diff --git a/src/storage.cpp b/src/storage.cpp new file mode 100644 index 0000000..95b9e79 --- /dev/null +++ b/src/storage.cpp @@ -0,0 +1,12 @@ +#include "storage.h" + +namespace anm2ed +{ + void Storage::labels_set(std::vector labels) + { + labelsString = labels; + this->labels.clear(); + for (auto& label : labelsString) + this->labels.emplace_back(label.c_str()); + } +} \ No newline at end of file diff --git a/src/storage.h b/src/storage.h new file mode 100644 index 0000000..db43cf3 --- /dev/null +++ b/src/storage.h @@ -0,0 +1,28 @@ +#pragma once + +#include "anm2/anm2.h" +#include "imgui_.h" + +namespace anm2ed +{ + class Storage + { + public: + int reference{-1}; + int hovered{-1}; + std::set unused{}; + std::vector labelsString{}; + std::vector labels{}; + imgui::MultiSelectStorage selection{}; + + void labels_set(std::vector); + }; + + class FrameStorage + { + public: + anm2::Type referenceType{anm2::NONE}; + int referenceID{-1}; + int referenceFrameIndex{-1}; + }; +} \ No newline at end of file diff --git a/src/tool.h b/src/tool.h index 1c143cf..109630e 100644 --- a/src/tool.h +++ b/src/tool.h @@ -21,53 +21,61 @@ namespace anm2ed::tool COUNT }; + enum AreaType + { + ANIMATION_PREVIEW, + SPRITESHEET_EDITOR, + ALL + }; + struct Info { ImGuiMouseCursor cursor{ImGuiMouseCursor_None}; resource::icon::Type icon{}; ShortcutType shortcut{}; + AreaType areaType; const char* label{}; const char* tooltip{}; }; constexpr Info INFO[] = { - {ImGuiMouseCursor_Hand, resource::icon::PAN, SHORTCUT_PAN, "##Pan", + {ImGuiMouseCursor_Hand, resource::icon::PAN, SHORTCUT_PAN, ALL, "##Pan", "Use the pan tool.\nWill shift the view as the cursor is dragged.\nYou can also use the middle mouse button to " "pan at any time."}, - {ImGuiMouseCursor_ResizeAll, resource::icon::MOVE, SHORTCUT_MOVE, "##Move", + {ImGuiMouseCursor_ResizeAll, resource::icon::MOVE, SHORTCUT_MOVE, ALL, "##Move", "Use the move tool.\nAnimation Preview: Will move the position of the frame." "\nSpritesheet Editor: Will move the pivot, and holding right click will use the Crop functionality instead." "\nUse mouse or directional keys to change the value."}, - {ImGuiMouseCursor_Arrow, resource::icon::ROTATE, SHORTCUT_ROTATE, "##Rotate", + {ImGuiMouseCursor_Arrow, resource::icon::ROTATE, SHORTCUT_ROTATE, ANIMATION_PREVIEW, "##Rotate", "Use the rotate tool.\nWill rotate the selected item as the cursor is dragged, or directional keys are " "pressed.\n(Animation Preview only.)"}, - {ImGuiMouseCursor_ResizeNESW, resource::icon::SCALE, SHORTCUT_SCALE, "##Scale", + {ImGuiMouseCursor_ResizeNESW, resource::icon::SCALE, SHORTCUT_SCALE, ANIMATION_PREVIEW, "##Scale", "Use the scale tool.\nWill scale the selected item as the cursor is dragged, or directional keys are " "pressed.\n(Animation Preview only.)"}, - {ImGuiMouseCursor_Arrow, resource::icon::CROP, SHORTCUT_CROP, "##Crop", - "Use the crop tool.\nWill produce a crop rectangle based on how the cursor is dragged." - "\nAlternatively, you can use the arrow keys and Ctrl/Shift to move the size/position, respectively." + {ImGuiMouseCursor_Arrow, resource::icon::CROP, SHORTCUT_CROP, SPRITESHEET_EDITOR, "##Crop", + "Use the crop tool.\nWill produce a crop rectangle based on how the cursor is dragged, or directional keys are " + "pressed.\nHold CTRL with arrow keys to change position." "\nHolding right click will use the Move tool's functionality." "\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_Arrow, resource::icon::DRAW, SHORTCUT_DRAW, "##Draw", + {ImGuiMouseCursor_Arrow, resource::icon::DRAW, SHORTCUT_DRAW, SPRITESHEET_EDITOR, "##Draw", "Draws pixels onto the selected spritesheet, with the current color.\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_Arrow, resource::icon::ERASE, SHORTCUT_ERASE, "##Erase", + {ImGuiMouseCursor_Arrow, resource::icon::ERASE, SHORTCUT_ERASE, SPRITESHEET_EDITOR, "##Erase", "Erases pixels from the selected spritesheet.\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_Arrow, resource::icon::COLOR_PICKER, SHORTCUT_COLOR_PICKER, "##Color Picker", - "Selects a color from the canvas.\n(Spritesheet Editor only.)"}, + {ImGuiMouseCursor_Arrow, resource::icon::COLOR_PICKER, SHORTCUT_COLOR_PICKER, SPRITESHEET_EDITOR, + "##Color Picker", "Selects a color from the canvas.\n(Spritesheet Editor only.)"}, - {ImGuiMouseCursor_None, resource::icon::UNDO, SHORTCUT_UNDO, "##Undo", "Undoes the last action."}, + {ImGuiMouseCursor_None, resource::icon::UNDO, SHORTCUT_UNDO, ALL, "##Undo", "Undoes the last action."}, - {ImGuiMouseCursor_None, resource::icon::REDO, SHORTCUT_REDO, "##Redo", "Redoes the last action."}, + {ImGuiMouseCursor_None, resource::icon::REDO, SHORTCUT_REDO, ALL, "##Redo", "Redoes the last action."}, - {ImGuiMouseCursor_None, resource::icon::NONE, SHORTCUT_COLOR, "##Color", + {ImGuiMouseCursor_None, resource::icon::NONE, SHORTCUT_COLOR, ALL, "##Color", "Selects the color to be used for drawing.\n(Spritesheet Editor only.)"}, }; } \ No newline at end of file diff --git a/src/util/filesystem_.cpp b/src/util/filesystem_.cpp index e3c3783..7a665d6 100644 --- a/src/util/filesystem_.cpp +++ b/src/util/filesystem_.cpp @@ -4,6 +4,8 @@ #include #include +#include "string_.h" + namespace anm2ed::util::filesystem { std::string path_preferences_get() @@ -27,6 +29,13 @@ namespace anm2ed::util::filesystem return e == ("." + extension); } + std::filesystem::path path_lower_case_backslash_handle(std::filesystem::path& path) + { + if (path_is_exist(path)) return path; + if (path_is_exist(string::backslash_replace(path))) return path; + return string::to_lower(path); + } + WorkingDirectory::WorkingDirectory(const std::string& path, bool isFile) { previous = std::filesystem::current_path(); diff --git a/src/util/filesystem_.h b/src/util/filesystem_.h index 7a78ef3..c13fcf6 100644 --- a/src/util/filesystem_.h +++ b/src/util/filesystem_.h @@ -8,6 +8,7 @@ namespace anm2ed::util::filesystem std::string path_preferences_get(); bool path_is_exist(const std::string&); bool path_is_extension(const std::string&, const std::string&); + std::filesystem::path path_lower_case_backslash_handle(std::filesystem::path&); class WorkingDirectory { diff --git a/src/util/string_.cpp b/src/util/string_.cpp index c814326..2cef87f 100644 --- a/src/util/string_.cpp +++ b/src/util/string_.cpp @@ -19,13 +19,6 @@ namespace anm2ed::util::string return transformed; } - std::string backslash_replace_to_lower(const std::string& string) - { - std::string transformed = string; - transformed = backslash_replace(transformed); - return to_lower(transformed); - } - std::string quote(const std::string& string) { return "\"" + string + "\""; diff --git a/src/util/string_.h b/src/util/string_.h index ce4d623..c76668f 100644 --- a/src/util/string_.h +++ b/src/util/string_.h @@ -6,6 +6,5 @@ namespace anm2ed::util::string { std::string to_lower(const std::string&); std::string backslash_replace(const std::string&); - std::string backslash_replace_to_lower(const std::string&); bool to_bool(const std::string&); } diff --git a/src/util/vector_.h b/src/util/vector_.h index 49d4cca..eba0dd9 100644 --- a/src/util/vector_.h +++ b/src/util/vector_.h @@ -12,6 +12,15 @@ namespace anm2ed::util::vector return index >= 0 && index < (int)v.size() ? &v[index] : nullptr; } + template bool in_bounds(std::vector& v, int index) + { + return index >= 0 && index < (int)v.size(); + } + template void clamp_in_bounds(std::vector& v, int& index) + { + index = std::clamp(index, 0, (int)v.size() - 1); + } + template std::set move_indices(std::vector& v, std::vector& indices, int index) { if (indices.empty()) return {}; @@ -52,12 +61,4 @@ namespace anm2ed::util::vector return moveIndices; } - template bool in_bounds(std::vector& v, int& index) - { - return index >= 0 || index <= (int)v.size() - 1; - } - template void clamp_in_bounds(std::vector& v, int& index) - { - index = std::clamp(index, 0, (int)v.size() - 1); - } }