2015 lines
73 KiB
C++
2015 lines
73 KiB
C++
#include "anm2.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <format>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
|
|
#include "file.hpp"
|
|
#include "math.hpp"
|
|
#include "path.hpp"
|
|
#include "time.hpp"
|
|
#include "working_directory.hpp"
|
|
#include "xml.hpp"
|
|
|
|
using namespace anm2ed::util;
|
|
using namespace tinyxml2;
|
|
|
|
namespace anm2ed
|
|
{
|
|
#define ANM2_STRING_ATTRIBUTES \
|
|
X("Name", name) \
|
|
X("CreatedBy", createdBy) \
|
|
X("CreatedOn", createdOn) \
|
|
X("DefaultAnimation", defaultAnimation)
|
|
|
|
#define ANM2_PATH_ATTRIBUTES X("Path", path)
|
|
|
|
#define ANM2_INT_ATTRIBUTES \
|
|
X("Id", id) \
|
|
X("LayerId", layerId) \
|
|
X("NullId", nullId) \
|
|
X("SpritesheetId", spritesheetId) \
|
|
X("Fps", fps) \
|
|
X("Version", version) \
|
|
X("FrameNum", frameNum) \
|
|
X("Delay", duration) \
|
|
X("AtFrame", atFrame) \
|
|
X("EventId", eventId) \
|
|
X("RegionId", regionId) \
|
|
X("SoundId", soundId) \
|
|
X("GroupId", groupId)
|
|
|
|
#define ANM2_BOOL_ATTRIBUTES \
|
|
X("Loop", isLoop) \
|
|
X("Visible", isVisible) \
|
|
X("ShowRect", isShowRect) \
|
|
X("Expanded", isExpanded)
|
|
|
|
#define ANM2_FLOAT_ATTRIBUTES \
|
|
X("Rotation", rotation) \
|
|
X("XPivot", pivot.x) \
|
|
X("YPivot", pivot.y) \
|
|
X("XCrop", crop.x) \
|
|
X("YCrop", crop.y) \
|
|
X("XPosition", position.x) \
|
|
X("YPosition", position.y) \
|
|
X("Width", size.x) \
|
|
X("Height", size.y) \
|
|
X("XScale", scale.x) \
|
|
X("YScale", scale.y)
|
|
|
|
#define ANM2_COLOR_ATTRIBUTES \
|
|
X("RedTint", tint.r) \
|
|
X("GreenTint", tint.g) \
|
|
X("BlueTint", tint.b) \
|
|
X("AlphaTint", tint.a) \
|
|
X("RedOffset", colorOffset.r) \
|
|
X("GreenOffset", colorOffset.g) \
|
|
X("BlueOffset", colorOffset.b)
|
|
|
|
constexpr std::string_view ANIMATION_MERGED_SUFFIX = " (Merged)";
|
|
|
|
constexpr std::array<std::string_view, (std::size_t)ElementType::COUNT> ELEMENT_TAGS = {
|
|
#define X(symbol, tag) std::string_view{tag},
|
|
ANM2_ELEMENT_TYPES
|
|
#undef X
|
|
};
|
|
|
|
constexpr std::array<std::string_view, (std::size_t)Interpolation::COUNT> INTERPOLATION_VALUES = {
|
|
"", "", "EaseIn", "EaseOut", "EaseInOut"};
|
|
|
|
constexpr std::array<std::string_view, (std::size_t)Origin::COUNT> ORIGIN_VALUES = {"", "TopLeft", "Center"};
|
|
|
|
void groups_flatten(Element&);
|
|
bool is_track(const Element&);
|
|
float interpolation_factor(Interpolation, float);
|
|
|
|
bool anm2_document_load(Anm2& anm2, XMLDocument& document, std::string* errorString)
|
|
{
|
|
auto rootElement = document.RootElement();
|
|
if (!rootElement)
|
|
{
|
|
if (errorString) *errorString = "No root element.";
|
|
anm2.isValid = false;
|
|
return false;
|
|
}
|
|
|
|
anm2.root = element_read(rootElement);
|
|
groups_flatten(anm2.root);
|
|
anm2.region_frames_sync(true);
|
|
anm2.isValid = true;
|
|
return true;
|
|
}
|
|
|
|
ElementType element_type_get(std::string_view tag)
|
|
{
|
|
for (std::size_t i = 0; i < ELEMENT_TAGS.size(); ++i)
|
|
if (ELEMENT_TAGS[i] == tag) return (ElementType)i;
|
|
return ElementType::UNKNOWN;
|
|
}
|
|
|
|
std::string_view element_tag_get(ElementType type)
|
|
{
|
|
auto index = (std::size_t)type;
|
|
if (index >= ELEMENT_TAGS.size()) return {};
|
|
return ELEMENT_TAGS[index];
|
|
}
|
|
|
|
ElementType element_container_type_get(ElementType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case ElementType::SPRITESHEET:
|
|
return ElementType::SPRITESHEETS;
|
|
case ElementType::LAYER_ELEMENT:
|
|
return ElementType::LAYERS;
|
|
case ElementType::NULL_ELEMENT:
|
|
return ElementType::NULLS;
|
|
case ElementType::EVENT_ELEMENT:
|
|
return ElementType::EVENTS;
|
|
case ElementType::SOUND_ELEMENT:
|
|
return ElementType::SOUNDS;
|
|
case ElementType::ANIMATION:
|
|
return ElementType::ANIMATIONS;
|
|
default:
|
|
return ElementType::UNKNOWN;
|
|
}
|
|
}
|
|
|
|
Element element_make(ElementType type)
|
|
{
|
|
Element element{};
|
|
element.type = type;
|
|
element.tag = std::string(element_tag_get(type));
|
|
return element;
|
|
}
|
|
|
|
bool string_query(const XMLElement* element, const char* name, std::string& out)
|
|
{
|
|
if (!element) return false;
|
|
if (auto value = element->Attribute(name); value)
|
|
{
|
|
out = value;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool path_query(const XMLElement* element, const char* name, std::filesystem::path& out)
|
|
{
|
|
std::string value{};
|
|
if (!string_query(element, name, value)) return false;
|
|
out = path::from_utf8(value);
|
|
return true;
|
|
}
|
|
|
|
void path_set(XMLElement* element, const char* name, const std::filesystem::path& value)
|
|
{
|
|
if (!element || value.empty()) return;
|
|
auto valueUtf8 = path::to_utf8(value);
|
|
element->SetAttribute(name, valueUtf8.c_str());
|
|
}
|
|
|
|
float color_read(const XMLElement* element, const char* name, float fallback)
|
|
{
|
|
int value{};
|
|
if (!element || element->QueryIntAttribute(name, &value) != XML_SUCCESS) return fallback;
|
|
return math::uint8_to_float(value);
|
|
}
|
|
|
|
int color_write(float value) { return math::float_to_uint8(glm::clamp(value, 0.0f, 1.0f)); }
|
|
|
|
Interpolation interpolation_read(const XMLElement* element, const char* attribute)
|
|
{
|
|
bool isFallback{};
|
|
if (element) element->QueryBoolAttribute(attribute, &isFallback);
|
|
|
|
auto value = element ? element->Attribute(attribute) : nullptr;
|
|
if (!value) return Interpolation::NONE;
|
|
|
|
for (std::size_t i = 0; i < INTERPOLATION_VALUES.size(); ++i)
|
|
if (!INTERPOLATION_VALUES[i].empty() && INTERPOLATION_VALUES[i] == value) return (Interpolation)i;
|
|
|
|
return isFallback ? Interpolation::LINEAR : Interpolation::NONE;
|
|
}
|
|
|
|
Interpolation interpolation_read(const XMLElement* element) { return interpolation_read(element, "Interpolated"); }
|
|
|
|
void interpolation_write(XMLElement* element, Interpolation interpolation)
|
|
{
|
|
if (interpolation == Interpolation::NONE || interpolation == Interpolation::LINEAR)
|
|
{
|
|
element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR);
|
|
return;
|
|
}
|
|
|
|
auto value = INTERPOLATION_VALUES[(std::size_t)interpolation];
|
|
if (!value.empty()) element->SetAttribute("Interpolated", value.data());
|
|
}
|
|
|
|
Origin origin_read(const XMLElement* element)
|
|
{
|
|
auto value = element ? element->Attribute("Origin") : nullptr;
|
|
if (!value) return Origin::CUSTOM;
|
|
|
|
for (std::size_t i = 0; i < ORIGIN_VALUES.size(); ++i)
|
|
if (!ORIGIN_VALUES[i].empty() && ORIGIN_VALUES[i] == value) return (Origin)i;
|
|
|
|
return Origin::CUSTOM;
|
|
}
|
|
|
|
void origin_write(XMLElement* out, const Element& element)
|
|
{
|
|
auto origin = ORIGIN_VALUES[(std::size_t)element.origin];
|
|
if (!origin.empty())
|
|
{
|
|
out->SetAttribute("Origin", origin.data());
|
|
return;
|
|
}
|
|
|
|
out->SetAttribute("XPivot", element.pivot.x);
|
|
out->SetAttribute("YPivot", element.pivot.y);
|
|
}
|
|
|
|
void element_attributes_read(Element& out, const XMLElement* element)
|
|
{
|
|
#define X(attribute, member) string_query(element, attribute, out.member);
|
|
ANM2_STRING_ATTRIBUTES
|
|
#undef X
|
|
#define X(attribute, member) path_query(element, attribute, out.member);
|
|
ANM2_PATH_ATTRIBUTES
|
|
#undef X
|
|
#define X(attribute, member) element->QueryIntAttribute(attribute, &out.member);
|
|
ANM2_INT_ATTRIBUTES
|
|
#undef X
|
|
#define X(attribute, member) element->QueryBoolAttribute(attribute, &out.member);
|
|
ANM2_BOOL_ATTRIBUTES
|
|
#undef X
|
|
#define X(attribute, member) element->QueryFloatAttribute(attribute, &out.member);
|
|
ANM2_FLOAT_ATTRIBUTES
|
|
#undef X
|
|
#define X(attribute, member) out.member = color_read(element, attribute, out.member);
|
|
ANM2_COLOR_ATTRIBUTES
|
|
#undef X
|
|
|
|
out.interpolation = interpolation_read(element);
|
|
out.origin = origin_read(element);
|
|
if (out.type == ElementType::REGION && out.origin == Origin::TOP_LEFT) out.pivot = {};
|
|
if (out.type == ElementType::REGION && out.origin == Origin::CENTER) out.pivot = glm::ivec2(out.size / 2.0f);
|
|
}
|
|
|
|
bool bake_attributes_read(const XMLElement* element, Interpolation& interpolation, int& bakeDelay)
|
|
{
|
|
if (!element || !element->Attribute("BakeInterpolation")) return false;
|
|
|
|
interpolation = interpolation_read(element, "BakeInterpolation");
|
|
element->QueryIntAttribute("BakeDelay", &bakeDelay);
|
|
return bakeDelay >= FRAME_DURATION_MIN;
|
|
}
|
|
|
|
const XMLElement* bake_frames_skip(const XMLElement* element, int bakeDelay)
|
|
{
|
|
for (int i = 0; element && i < bakeDelay; ++i)
|
|
element = element->NextSiblingElement();
|
|
return element;
|
|
}
|
|
|
|
void element_children_read(Element& out, const XMLElement* element)
|
|
{
|
|
auto child = element->FirstChildElement();
|
|
while (child)
|
|
{
|
|
auto childType = element_type_get(child->Name() ? child->Name() : "");
|
|
if (childType == ElementType::FRAME && is_track(out))
|
|
{
|
|
Interpolation bakeInterpolation{};
|
|
int bakeDelay{};
|
|
if (bake_attributes_read(child, bakeInterpolation, bakeDelay))
|
|
{
|
|
auto restored = element_read(child);
|
|
restored.interpolation = bakeInterpolation;
|
|
restored.duration = bakeDelay;
|
|
out.children.push_back(std::move(restored));
|
|
child = bake_frames_skip(child, bakeDelay);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
out.children.push_back(element_read(child));
|
|
child = child->NextSiblingElement();
|
|
}
|
|
}
|
|
|
|
Element element_read(const XMLElement* element)
|
|
{
|
|
Element out{};
|
|
if (!element) return out;
|
|
|
|
out.tag = element->Name() ? element->Name() : "";
|
|
out.type = element_type_get(out.tag);
|
|
element_attributes_read(out, element);
|
|
|
|
element_children_read(out, element);
|
|
|
|
if (out.type == ElementType::TRIGGER && out.soundId != -1)
|
|
{
|
|
bool isSoundFound{};
|
|
for (const auto& child : out.children)
|
|
if (child.type == ElementType::SOUND_ELEMENT && child.id == out.soundId) isSoundFound = true;
|
|
|
|
if (!isSoundFound)
|
|
{
|
|
auto sound = element_make(ElementType::SOUND_ELEMENT);
|
|
sound.id = out.soundId;
|
|
out.children.insert(out.children.begin(), sound);
|
|
}
|
|
}
|
|
|
|
if (out.type == ElementType::TRIGGER)
|
|
for (const auto& child : out.children)
|
|
if (child.type == ElementType::SOUND_ELEMENT && child.id != -1) out.soundIds.push_back(child.id);
|
|
|
|
return out;
|
|
}
|
|
|
|
bool element_write_skip(const Element& element, ElementType parentType, Flags flags)
|
|
{
|
|
if (element.type == ElementType::SOUNDS && (!has_flag(flags, SERIALIZE_SOUNDS) || element.children.empty()))
|
|
return true;
|
|
if (element.type == ElementType::SOUND_ELEMENT && parentType == ElementType::TRIGGER &&
|
|
!has_flag(flags, SERIALIZE_SOUNDS))
|
|
return true;
|
|
if (element.type == ElementType::REGION && !has_flag(flags, SERIALIZE_REGIONS)) return true;
|
|
if (element.type == ElementType::GROUP && !has_flag(flags, SERIALIZE_GROUPS)) return true;
|
|
return false;
|
|
}
|
|
|
|
void frame_attributes_write(XMLElement* out, const Element& element, ElementType parentType, Flags flags)
|
|
{
|
|
if (parentType == ElementType::LAYER_ANIMATION)
|
|
{
|
|
bool isHasValidRegion = has_flag(flags, SERIALIZE_REGIONS) && element.regionId != -1;
|
|
bool isWriteRegionValues = has_flag(flags, SERIALIZE_REDUNDANT_FRAME_REGION_VALUES) || !isHasValidRegion;
|
|
|
|
if (isHasValidRegion) out->SetAttribute("RegionId", element.regionId);
|
|
if (isWriteRegionValues)
|
|
{
|
|
out->SetAttribute("XPivot", element.pivot.x);
|
|
out->SetAttribute("YPivot", element.pivot.y);
|
|
out->SetAttribute("XCrop", element.crop.x);
|
|
out->SetAttribute("YCrop", element.crop.y);
|
|
out->SetAttribute("Width", element.size.x);
|
|
out->SetAttribute("Height", element.size.y);
|
|
}
|
|
}
|
|
|
|
out->SetAttribute("XPosition", element.position.x);
|
|
out->SetAttribute("YPosition", element.position.y);
|
|
out->SetAttribute("Delay", element.duration);
|
|
out->SetAttribute("Visible", element.isVisible);
|
|
out->SetAttribute("XScale", element.scale.x);
|
|
out->SetAttribute("YScale", element.scale.y);
|
|
out->SetAttribute("RedTint", color_write(element.tint.r));
|
|
out->SetAttribute("GreenTint", color_write(element.tint.g));
|
|
out->SetAttribute("BlueTint", color_write(element.tint.b));
|
|
out->SetAttribute("AlphaTint", color_write(element.tint.a));
|
|
out->SetAttribute("RedOffset", color_write(element.colorOffset.r));
|
|
out->SetAttribute("GreenOffset", color_write(element.colorOffset.g));
|
|
out->SetAttribute("BlueOffset", color_write(element.colorOffset.b));
|
|
out->SetAttribute("Rotation", element.rotation);
|
|
interpolation_write(out, element.interpolation);
|
|
}
|
|
|
|
void element_attributes_write(XMLElement* out, const Element& element, ElementType parentType, Flags flags)
|
|
{
|
|
if (element.type == ElementType::INFO)
|
|
{
|
|
out->SetAttribute("CreatedBy", element.createdBy.c_str());
|
|
out->SetAttribute("CreatedOn", element.createdOn.c_str());
|
|
out->SetAttribute("Fps", element.fps);
|
|
out->SetAttribute("Version", element.version);
|
|
}
|
|
else if (element.type == ElementType::ANIMATIONS)
|
|
out->SetAttribute("DefaultAnimation", element.defaultAnimation.c_str());
|
|
else if (element.type == ElementType::SPRITESHEET)
|
|
{
|
|
out->SetAttribute("Id", element.id);
|
|
path_set(out, "Path", element.path);
|
|
}
|
|
else if (element.type == ElementType::REGION)
|
|
{
|
|
out->SetAttribute("Id", element.id);
|
|
out->SetAttribute("Name", element.name.c_str());
|
|
out->SetAttribute("XCrop", element.crop.x);
|
|
out->SetAttribute("YCrop", element.crop.y);
|
|
out->SetAttribute("Width", element.size.x);
|
|
out->SetAttribute("Height", element.size.y);
|
|
origin_write(out, element);
|
|
}
|
|
else if (element.type == ElementType::LAYER_ELEMENT)
|
|
{
|
|
out->SetAttribute("Id", element.id);
|
|
out->SetAttribute("Name", element.name.c_str());
|
|
out->SetAttribute("SpritesheetId", element.spritesheetId);
|
|
}
|
|
else if (element.type == ElementType::NULL_ELEMENT)
|
|
{
|
|
out->SetAttribute("Id", element.id);
|
|
out->SetAttribute("Name", element.name.c_str());
|
|
if (element.isShowRect) out->SetAttribute("ShowRect", element.isShowRect);
|
|
}
|
|
else if (element.type == ElementType::GROUP)
|
|
{
|
|
out->SetAttribute("Id", element.id);
|
|
out->SetAttribute("Name", element.name.c_str());
|
|
out->SetAttribute("Expanded", element.isExpanded);
|
|
out->SetAttribute("Visible", element.isVisible);
|
|
}
|
|
else if (element.type == ElementType::EVENT_ELEMENT)
|
|
{
|
|
out->SetAttribute("Id", element.id);
|
|
out->SetAttribute("Name", element.name.c_str());
|
|
}
|
|
else if (element.type == ElementType::SOUND_ELEMENT)
|
|
{
|
|
out->SetAttribute("Id", element.id);
|
|
if (parentType != ElementType::TRIGGER) path_set(out, "Path", element.path);
|
|
}
|
|
else if (element.type == ElementType::ANIMATION)
|
|
{
|
|
out->SetAttribute("Name", element.name.c_str());
|
|
out->SetAttribute("FrameNum", element.frameNum);
|
|
out->SetAttribute("Loop", element.isLoop);
|
|
}
|
|
else if (element.type == ElementType::LAYER_ANIMATION)
|
|
{
|
|
out->SetAttribute("LayerId", element.layerId);
|
|
out->SetAttribute("Visible", element.isVisible);
|
|
if (element.groupId != -1 && has_flag(flags, SERIALIZE_GROUPS)) out->SetAttribute("GroupId", element.groupId);
|
|
}
|
|
else if (element.type == ElementType::NULL_ANIMATION)
|
|
{
|
|
out->SetAttribute("NullId", element.nullId);
|
|
out->SetAttribute("Visible", element.isVisible);
|
|
if (element.groupId != -1 && has_flag(flags, SERIALIZE_GROUPS)) out->SetAttribute("GroupId", element.groupId);
|
|
}
|
|
else if (element.type == ElementType::FRAME)
|
|
frame_attributes_write(out, element, parentType, flags);
|
|
else if (element.type == ElementType::TRIGGER)
|
|
{
|
|
if (element.eventId != -1) out->SetAttribute("EventId", element.eventId);
|
|
out->SetAttribute("AtFrame", element.atFrame);
|
|
}
|
|
}
|
|
|
|
void bake_attributes_write(XMLElement* out, Interpolation interpolation, int bakeDelay)
|
|
{
|
|
auto value = INTERPOLATION_VALUES[(std::size_t)interpolation];
|
|
if (!value.empty()) out->SetAttribute("BakeInterpolation", value.data());
|
|
out->SetAttribute("BakeDelay", bakeDelay);
|
|
}
|
|
|
|
bool is_frame_bake_serialized(const Element& frame, Flags flags)
|
|
{
|
|
return has_flag(flags, SERIALIZE_BAKE_SPECIAL_INTERPOLATED_FRAMES) && frame.type == ElementType::FRAME &&
|
|
frame.interpolation != Interpolation::NONE && frame.interpolation != Interpolation::LINEAR;
|
|
}
|
|
|
|
XMLElement* element_to_xml(XMLDocument& document, const Element& element, ElementType parentType, Flags flags);
|
|
|
|
void baked_frames_insert(XMLDocument& document, XMLElement* out, const Element& track, int index, Flags flags)
|
|
{
|
|
const auto& original = track.children[index];
|
|
auto nextFrame = index + 1 < (int)track.children.size() && track.children[index + 1].type == ElementType::FRAME
|
|
? track.children[index + 1]
|
|
: original;
|
|
auto bakeDelay = std::max(original.duration, FRAME_DURATION_MIN);
|
|
|
|
for (int bakeIndex = 0; bakeIndex < bakeDelay; ++bakeIndex)
|
|
{
|
|
auto baked = original;
|
|
auto amount = interpolation_factor(original.interpolation, (float)bakeIndex / (float)bakeDelay);
|
|
baked.duration = FRAME_DURATION_MIN;
|
|
baked.interpolation = Interpolation::NONE;
|
|
baked.rotation = glm::mix(original.rotation, nextFrame.rotation, amount);
|
|
baked.position = glm::mix(original.position, nextFrame.position, amount);
|
|
baked.scale = glm::mix(original.scale, nextFrame.scale, amount);
|
|
baked.colorOffset = glm::mix(original.colorOffset, nextFrame.colorOffset, amount);
|
|
baked.tint = glm::mix(original.tint, nextFrame.tint, amount);
|
|
auto frame = element_to_xml(document, baked, track.type, flags);
|
|
if (bakeIndex == 0) bake_attributes_write(frame, original.interpolation, bakeDelay);
|
|
out->InsertEndChild(frame);
|
|
}
|
|
}
|
|
|
|
XMLElement* element_to_xml(XMLDocument& document, const Element& element, ElementType parentType, Flags flags)
|
|
{
|
|
auto tag = element.type == ElementType::UNKNOWN ? std::string_view(element.tag) : element_tag_get(element.type);
|
|
auto out = document.NewElement(tag.empty() ? element.tag.c_str() : tag.data());
|
|
element_attributes_write(out, element, parentType, flags);
|
|
|
|
for (int i = 0; i < (int)element.children.size(); ++i)
|
|
{
|
|
const auto& child = element.children[i];
|
|
if (element_write_skip(child, element.type, flags)) continue;
|
|
|
|
if (is_track(element) && is_frame_bake_serialized(child, flags))
|
|
baked_frames_insert(document, out, element, i, flags);
|
|
else
|
|
out->InsertEndChild(element_to_xml(document, child, element.type, flags));
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
XMLElement* element_to_xml(XMLDocument& document, const Element& element, Flags flags)
|
|
{
|
|
return element_to_xml(document, element, ElementType::UNKNOWN, flags);
|
|
}
|
|
|
|
std::string element_to_string(const Element& element, ElementType parentType, Flags flags)
|
|
{
|
|
XMLDocument document{};
|
|
document.InsertEndChild(element_to_xml(document, element, parentType, flags));
|
|
return xml::document_to_string(document);
|
|
}
|
|
|
|
std::string element_to_string(const Element& element, Flags flags)
|
|
{
|
|
return element_to_string(element, ElementType::UNKNOWN, flags);
|
|
}
|
|
|
|
void groups_flatten(Element& element)
|
|
{
|
|
auto container_flatten = [](Element& container, ElementType trackType)
|
|
{
|
|
int nextGroupId{};
|
|
for (const auto& item : container.children)
|
|
if (item.type == ElementType::GROUP) nextGroupId = std::max(nextGroupId, item.id + 1);
|
|
|
|
std::vector<Element> flattened{};
|
|
for (auto item : container.children)
|
|
{
|
|
if (item.type != ElementType::GROUP)
|
|
{
|
|
flattened.push_back(item);
|
|
continue;
|
|
}
|
|
|
|
if (item.id < 0) item.id = nextGroupId++;
|
|
auto groupId = item.id;
|
|
auto children = std::move(item.children);
|
|
item.children.clear();
|
|
flattened.push_back(item);
|
|
|
|
for (auto child : children)
|
|
{
|
|
if (child.type != trackType) continue;
|
|
child.groupId = groupId;
|
|
flattened.push_back(child);
|
|
}
|
|
}
|
|
|
|
std::set<int> groupIds{};
|
|
for (const auto& item : flattened)
|
|
if (item.type == ElementType::GROUP) groupIds.insert(item.id);
|
|
|
|
for (auto& item : flattened)
|
|
if (item.type == trackType && !groupIds.contains(item.groupId)) item.groupId = -1;
|
|
|
|
container.children = std::move(flattened);
|
|
};
|
|
|
|
if (element.type == ElementType::LAYER_ANIMATIONS)
|
|
container_flatten(element, ElementType::LAYER_ANIMATION);
|
|
else if (element.type == ElementType::NULL_ANIMATIONS)
|
|
container_flatten(element, ElementType::NULL_ANIMATION);
|
|
|
|
for (auto& child : element.children)
|
|
groups_flatten(child);
|
|
}
|
|
|
|
Element* child_first_get(Element& element, ElementType type)
|
|
{
|
|
for (auto& child : element.children)
|
|
if (child.type == type) return &child;
|
|
return nullptr;
|
|
}
|
|
|
|
const Element* child_first_get(const Element& element, ElementType type)
|
|
{
|
|
for (const auto& child : element.children)
|
|
if (child.type == type) return &child;
|
|
return nullptr;
|
|
}
|
|
|
|
Element* element_child_first_get(Element& element, ElementType type) { return child_first_get(element, type); }
|
|
|
|
const Element* element_child_first_get(const Element& element, ElementType type)
|
|
{
|
|
return child_first_get(element, type);
|
|
}
|
|
|
|
Element* child_id_get(Element& element, ElementType type, int id)
|
|
{
|
|
for (auto& child : element.children)
|
|
if (child.type == type && child.id == id) return &child;
|
|
return nullptr;
|
|
}
|
|
|
|
const Element* child_id_get(const Element& element, ElementType type, int id)
|
|
{
|
|
for (const auto& child : element.children)
|
|
if (child.type == type && child.id == id) return &child;
|
|
return nullptr;
|
|
}
|
|
|
|
Element* element_child_id_get(Element& element, ElementType type, int id) { return child_id_get(element, type, id); }
|
|
|
|
const Element* element_child_id_get(const Element& element, ElementType type, int id)
|
|
{
|
|
return child_id_get(element, type, id);
|
|
}
|
|
|
|
int element_child_max_id_get(const Element& element, ElementType type)
|
|
{
|
|
int maxId{-1};
|
|
for (const auto& child : element.children)
|
|
if (child.type == type) maxId = std::max(maxId, child.id);
|
|
return maxId;
|
|
}
|
|
|
|
int element_child_next_id_get(const Element& element, ElementType type)
|
|
{
|
|
return element_child_max_id_get(element, type) + 1;
|
|
}
|
|
|
|
bool element_child_id_erase(Element& element, ElementType type, int id)
|
|
{
|
|
for (auto it = element.children.begin(); it != element.children.end(); ++it)
|
|
{
|
|
if (it->type != type || it->id != id) continue;
|
|
element.children.erase(it);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Element* element_first_get(Element& element, ElementType type)
|
|
{
|
|
if (element.type == type) return &element;
|
|
for (auto& child : element.children)
|
|
if (auto result = element_first_get(child, type); result) return result;
|
|
return nullptr;
|
|
}
|
|
|
|
const Element* element_first_get(const Element& element, ElementType type)
|
|
{
|
|
if (element.type == type) return &element;
|
|
for (const auto& child : element.children)
|
|
if (auto result = element_first_get(child, type); result) return result;
|
|
return nullptr;
|
|
}
|
|
|
|
float interpolation_factor(Interpolation interpolation, float value)
|
|
{
|
|
value = glm::clamp(value, 0.0f, 1.0f);
|
|
if (interpolation == Interpolation::LINEAR) return value;
|
|
if (interpolation == Interpolation::EASE_IN) return value * value;
|
|
if (interpolation == Interpolation::EASE_OUT) return 1.0f - ((1.0f - value) * (1.0f - value));
|
|
if (interpolation == Interpolation::EASE_IN_OUT)
|
|
return value < 0.5f ? (2.0f * value * value) : (1.0f - std::pow(-2.0f * value + 2.0f, 2.0f) * 0.5f);
|
|
return 0.0f;
|
|
}
|
|
|
|
bool is_track(const Element& element)
|
|
{
|
|
return element.type == ElementType::ROOT_ANIMATION || element.type == ElementType::LAYER_ANIMATION ||
|
|
element.type == ElementType::NULL_ANIMATION || element.type == ElementType::TRIGGERS;
|
|
}
|
|
|
|
ElementType track_frame_type_get(const Element& track)
|
|
{
|
|
return track.type == ElementType::TRIGGERS ? ElementType::TRIGGER : ElementType::FRAME;
|
|
}
|
|
|
|
ElementType item_type_to_track_type_get(ItemType type)
|
|
{
|
|
if (type == ItemType::ROOT) return ElementType::ROOT_ANIMATION;
|
|
if (type == ItemType::LAYER) return ElementType::LAYER_ANIMATION;
|
|
if (type == ItemType::NULL_) return ElementType::NULL_ANIMATION;
|
|
if (type == ItemType::TRIGGER) return ElementType::TRIGGERS;
|
|
return ElementType::UNKNOWN;
|
|
}
|
|
|
|
ElementType item_type_to_container_type_get(ItemType type)
|
|
{
|
|
if (type == ItemType::LAYER) return ElementType::LAYER_ANIMATIONS;
|
|
if (type == ItemType::NULL_) return ElementType::NULL_ANIMATIONS;
|
|
return ElementType::UNKNOWN;
|
|
}
|
|
|
|
int track_id_get(const Element& track)
|
|
{
|
|
if (track.type == ElementType::LAYER_ANIMATION) return track.layerId;
|
|
if (track.type == ElementType::NULL_ANIMATION) return track.nullId;
|
|
return -1;
|
|
}
|
|
|
|
template <typename Callback> void tracks_each(Element& parent, ElementType trackType, Callback&& callback)
|
|
{
|
|
for (auto& child : parent.children)
|
|
{
|
|
if (child.type == trackType)
|
|
callback(child);
|
|
else if (child.type == ElementType::GROUP)
|
|
tracks_each(child, trackType, callback);
|
|
}
|
|
}
|
|
|
|
template <typename Callback> void tracks_each(const Element& parent, ElementType trackType, Callback&& callback)
|
|
{
|
|
for (const auto& child : parent.children)
|
|
{
|
|
if (child.type == trackType)
|
|
callback(child);
|
|
else if (child.type == ElementType::GROUP)
|
|
tracks_each(child, trackType, callback);
|
|
}
|
|
}
|
|
|
|
Element* track_find(Element& parent, ElementType trackType, int id)
|
|
{
|
|
for (auto& child : parent.children)
|
|
{
|
|
if (child.type == trackType && track_id_get(child) == id) return &child;
|
|
if (child.type == ElementType::GROUP)
|
|
if (auto found = track_find(child, trackType, id)) return found;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const Element* track_find(const Element& parent, ElementType trackType, int id)
|
|
{
|
|
for (const auto& child : parent.children)
|
|
{
|
|
if (child.type == trackType && track_id_get(child) == id) return &child;
|
|
if (child.type == ElementType::GROUP)
|
|
if (auto found = track_find(child, trackType, id)) return found;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool is_track_group_visible(const Element& container, const Element& track)
|
|
{
|
|
if (track.groupId == -1) return true;
|
|
for (const auto& child : container.children)
|
|
if (child.type == ElementType::GROUP && child.id == track.groupId) return child.isVisible;
|
|
return true;
|
|
}
|
|
|
|
Element* animation_item_get(Element& animation, ItemType type, int id)
|
|
{
|
|
auto trackType = item_type_to_track_type_get(type);
|
|
if (type == ItemType::ROOT || type == ItemType::TRIGGER) return child_first_get(animation, trackType);
|
|
|
|
auto container = child_first_get(animation, item_type_to_container_type_get(type));
|
|
return container ? track_find(*container, trackType, id) : nullptr;
|
|
}
|
|
|
|
const Element* animation_item_get(const Element& animation, ItemType type, int id)
|
|
{
|
|
auto trackType = item_type_to_track_type_get(type);
|
|
if (type == ItemType::ROOT || type == ItemType::TRIGGER) return child_first_get(animation, trackType);
|
|
|
|
auto container = child_first_get(animation, item_type_to_container_type_get(type));
|
|
return container ? track_find(*container, trackType, id) : nullptr;
|
|
}
|
|
|
|
Element* track_frame_get(Element& track, int index)
|
|
{
|
|
if (index < 0) return nullptr;
|
|
auto frameType = track_frame_type_get(track);
|
|
int frameIndex{};
|
|
for (auto& frame : track.children)
|
|
{
|
|
if (frame.type != frameType) continue;
|
|
if (frameIndex == index) return &frame;
|
|
++frameIndex;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const Element* track_frame_get(const Element& track, int index)
|
|
{
|
|
if (index < 0) return nullptr;
|
|
auto frameType = track_frame_type_get(track);
|
|
int frameIndex{};
|
|
for (const auto& frame : track.children)
|
|
{
|
|
if (frame.type != frameType) continue;
|
|
if (frameIndex == index) return &frame;
|
|
++frameIndex;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Element frame_generate(const Element& track, float time)
|
|
{
|
|
auto frame = element_make(track_frame_type_get(track));
|
|
frame.isVisible = false;
|
|
if (track.children.empty()) return frame;
|
|
|
|
time = std::max(time, 0.0f);
|
|
|
|
const Element* frameNext = nullptr;
|
|
int durationCurrent{};
|
|
int durationNext{};
|
|
auto frameType = track_frame_type_get(track);
|
|
|
|
for (int i = 0; i < (int)track.children.size(); ++i)
|
|
{
|
|
const auto& iFrame = track.children[i];
|
|
if (iFrame.type != frameType) continue;
|
|
|
|
if (frameType == ElementType::TRIGGER)
|
|
{
|
|
if ((int)time == iFrame.atFrame)
|
|
{
|
|
frame = iFrame;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
frame = iFrame;
|
|
durationNext += frame.duration;
|
|
|
|
if (time >= durationCurrent && time < durationNext)
|
|
{
|
|
for (int next = i + 1; next < (int)track.children.size(); ++next)
|
|
if (track.children[next].type == ElementType::FRAME)
|
|
{
|
|
frameNext = &track.children[next];
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
durationCurrent += frame.duration;
|
|
}
|
|
|
|
if (frameType != ElementType::TRIGGER && frame.interpolation != Interpolation::NONE && frameNext &&
|
|
frame.duration > 1)
|
|
{
|
|
auto amount =
|
|
interpolation_factor(frame.interpolation, (time - durationCurrent) / (durationNext - durationCurrent));
|
|
frame.rotation = glm::mix(frame.rotation, frameNext->rotation, amount);
|
|
frame.position = glm::mix(frame.position, frameNext->position, amount);
|
|
frame.scale = glm::mix(frame.scale, frameNext->scale, amount);
|
|
frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, amount);
|
|
frame.tint = glm::mix(frame.tint, frameNext->tint, amount);
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
int frame_index_from_at_frame_get(const Element& track, int atFrame)
|
|
{
|
|
int index{};
|
|
for (const auto& frame : track.children)
|
|
{
|
|
if (frame.type != ElementType::TRIGGER) continue;
|
|
if (frame.atFrame == atFrame) return index;
|
|
++index;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int frame_index_from_time_get(const Element& track, float time)
|
|
{
|
|
if (track.type == ElementType::TRIGGERS) return frame_index_from_at_frame_get(track, (int)time);
|
|
|
|
auto frameType = track_frame_type_get(track);
|
|
int frameCount{};
|
|
for (const auto& frame : track.children)
|
|
if (frame.type == frameType) ++frameCount;
|
|
if (frameCount == 0) return -1;
|
|
if (time <= 0.0f) return 0;
|
|
|
|
float duration{};
|
|
int index{};
|
|
for (const auto& frame : track.children)
|
|
{
|
|
if (frame.type != frameType) continue;
|
|
duration += frame.duration;
|
|
if (time < duration) return index;
|
|
++index;
|
|
}
|
|
|
|
return frameCount - 1;
|
|
}
|
|
|
|
float frame_time_from_index_get(const Element& track, int index)
|
|
{
|
|
if (index < 0) return 0.0f;
|
|
auto frameType = track_frame_type_get(track);
|
|
float time{};
|
|
int frameIndex{};
|
|
for (const auto& frame : track.children)
|
|
{
|
|
if (frame.type != frameType) continue;
|
|
if (frameIndex == index) return frameType == ElementType::TRIGGER ? (float)frame.atFrame : time;
|
|
time += frame.duration;
|
|
++frameIndex;
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
void frame_bake(Element& track, int index, int interval, bool isRoundScale, bool isRoundRotation)
|
|
{
|
|
auto frame = track_frame_get(track, index);
|
|
if (!frame) return;
|
|
|
|
auto original = *frame;
|
|
if (original.duration <= FRAME_DURATION_MIN)
|
|
{
|
|
frame->interpolation = Interpolation::NONE;
|
|
return;
|
|
}
|
|
|
|
auto nextFrame = track_frame_get(track, index + 1);
|
|
auto next = nextFrame ? *nextFrame : original;
|
|
int duration{};
|
|
int insertIndex = index;
|
|
interval = std::max(interval, FRAME_DURATION_MIN);
|
|
|
|
while (duration < original.duration)
|
|
{
|
|
auto baked = original;
|
|
auto amount = interpolation_factor(original.interpolation, (float)duration / original.duration);
|
|
baked.duration = std::min(interval, original.duration - duration);
|
|
baked.interpolation = Interpolation::NONE;
|
|
baked.rotation = glm::mix(original.rotation, next.rotation, amount);
|
|
baked.position = glm::mix(original.position, next.position, amount);
|
|
baked.scale = glm::mix(original.scale, next.scale, amount);
|
|
baked.colorOffset = glm::mix(original.colorOffset, next.colorOffset, amount);
|
|
baked.tint = glm::mix(original.tint, next.tint, amount);
|
|
if (isRoundScale) baked.scale = glm::vec2(glm::ivec2(baked.scale));
|
|
if (isRoundRotation) baked.rotation = (int)baked.rotation;
|
|
|
|
if (insertIndex == index)
|
|
track.children[insertIndex] = baked;
|
|
else
|
|
track.children.insert(track.children.begin() + insertIndex, baked);
|
|
|
|
duration += baked.duration;
|
|
++insertIndex;
|
|
}
|
|
}
|
|
|
|
void frames_generate_from_grid(Element& track, glm::ivec2 startPosition, glm::ivec2 size, glm::ivec2 pivot,
|
|
int columns, int count, int duration)
|
|
{
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
auto frame = element_make(ElementType::FRAME);
|
|
frame.duration = duration;
|
|
frame.pivot = pivot;
|
|
frame.size = size;
|
|
frame.crop = startPosition + glm::ivec2(size.x * (i % columns), size.y * (i / columns));
|
|
track.children.emplace_back(frame);
|
|
}
|
|
}
|
|
|
|
void frames_sort_by_at_frame(Element& track)
|
|
{
|
|
std::sort(track.children.begin(), track.children.end(),
|
|
[](const Element& a, const Element& b) { return a.atFrame < b.atFrame; });
|
|
}
|
|
|
|
bool frames_deserialize(Element& track, const std::string& string, int start, std::set<int>& indices,
|
|
std::string* errorString)
|
|
{
|
|
XMLDocument document{};
|
|
if (document.Parse(string.c_str()) != XML_SUCCESS)
|
|
{
|
|
if (errorString) *errorString = document.ErrorStr();
|
|
return false;
|
|
}
|
|
|
|
auto frameType = track_frame_type_get(track);
|
|
auto first =
|
|
frameType == ElementType::TRIGGER ? document.FirstChildElement("Trigger") : document.FirstChildElement("Frame");
|
|
if (!first)
|
|
{
|
|
if (errorString) *errorString = frameType == ElementType::TRIGGER ? "No valid trigger(s)." : "No valid frame(s).";
|
|
return false;
|
|
}
|
|
|
|
if (frameType == ElementType::FRAME)
|
|
{
|
|
start = std::clamp(start, 0, (int)track.children.size());
|
|
int count{};
|
|
for (auto element = first; element; element = element->NextSiblingElement("Frame"))
|
|
{
|
|
auto frame = element_read(element);
|
|
if (frame.type != ElementType::FRAME) continue;
|
|
auto index = start + count;
|
|
track.children.insert(track.children.begin() + index, frame);
|
|
indices.insert(index);
|
|
++count;
|
|
}
|
|
return !indices.empty();
|
|
}
|
|
|
|
auto has_conflict = [&](int value)
|
|
{
|
|
for (auto& trigger : track.children)
|
|
if (trigger.type == ElementType::TRIGGER && trigger.atFrame == value) return true;
|
|
return false;
|
|
};
|
|
|
|
std::vector<int> atFrames{};
|
|
int count{};
|
|
for (auto element = first; element; element = element->NextSiblingElement("Trigger"))
|
|
{
|
|
auto trigger = element_read(element);
|
|
if (trigger.type != ElementType::TRIGGER) continue;
|
|
trigger.atFrame = start + count;
|
|
while (has_conflict(trigger.atFrame))
|
|
++trigger.atFrame;
|
|
atFrames.push_back(trigger.atFrame);
|
|
track.children.push_back(trigger);
|
|
++count;
|
|
}
|
|
frames_sort_by_at_frame(track);
|
|
for (auto atFrame : atFrames)
|
|
if (auto index = frame_index_from_at_frame_get(track, atFrame); index != -1) indices.insert(index);
|
|
return !indices.empty();
|
|
}
|
|
|
|
void frames_change(Element& track, FrameChange change, ItemType itemType, ChangeType changeType,
|
|
const std::set<int>& selection)
|
|
{
|
|
const auto clamp_identity = [](auto value) { return value; };
|
|
const auto clamp01 = [](auto value) { return glm::clamp(value, 0.0f, 1.0f); };
|
|
const auto clamp_duration = [](int value) { return std::max(FRAME_DURATION_MIN, value); };
|
|
|
|
if (selection.empty()) return;
|
|
|
|
auto apply_scalar_with_clamp = [&](auto& target, const auto& optionalValue, auto clampFunc)
|
|
{
|
|
if (!optionalValue) return;
|
|
auto value = *optionalValue;
|
|
|
|
switch (changeType)
|
|
{
|
|
case ChangeType::ADJUST:
|
|
target = clampFunc(value);
|
|
break;
|
|
case ChangeType::ADD:
|
|
target = clampFunc(target + value);
|
|
break;
|
|
case ChangeType::SUBTRACT:
|
|
target = clampFunc(target - value);
|
|
break;
|
|
case ChangeType::MULTIPLY:
|
|
target = clampFunc(target * value);
|
|
break;
|
|
case ChangeType::DIVIDE:
|
|
if (value == decltype(value){}) return;
|
|
target = clampFunc(target / value);
|
|
break;
|
|
}
|
|
};
|
|
|
|
auto apply_scalar = [&](auto& target, const auto& optionalValue)
|
|
{ apply_scalar_with_clamp(target, optionalValue, clamp_identity); };
|
|
|
|
for (auto index : selection)
|
|
{
|
|
auto frame = track_frame_get(track, index);
|
|
if (!frame) continue;
|
|
|
|
if (change.isVisible) frame->isVisible = *change.isVisible;
|
|
if (change.interpolation) frame->interpolation = *change.interpolation;
|
|
if (change.isFlipX) frame->scale.x = -frame->scale.x;
|
|
if (change.isFlipY) frame->scale.y = -frame->scale.y;
|
|
|
|
apply_scalar(frame->rotation, change.rotation);
|
|
apply_scalar_with_clamp(frame->duration, change.duration, clamp_duration);
|
|
|
|
if (itemType == ItemType::LAYER)
|
|
{
|
|
apply_scalar(frame->crop.x, change.cropX);
|
|
apply_scalar(frame->crop.y, change.cropY);
|
|
apply_scalar(frame->pivot.x, change.pivotX);
|
|
apply_scalar(frame->pivot.y, change.pivotY);
|
|
apply_scalar(frame->size.x, change.sizeX);
|
|
apply_scalar(frame->size.y, change.sizeY);
|
|
if (change.regionId) frame->regionId = *change.regionId;
|
|
}
|
|
|
|
apply_scalar(frame->position.x, change.positionX);
|
|
apply_scalar(frame->position.y, change.positionY);
|
|
apply_scalar(frame->scale.x, change.scaleX);
|
|
apply_scalar(frame->scale.y, change.scaleY);
|
|
apply_scalar_with_clamp(frame->colorOffset.x, change.colorOffsetR, clamp01);
|
|
apply_scalar_with_clamp(frame->colorOffset.y, change.colorOffsetG, clamp01);
|
|
apply_scalar_with_clamp(frame->colorOffset.z, change.colorOffsetB, clamp01);
|
|
apply_scalar_with_clamp(frame->tint.x, change.tintR, clamp01);
|
|
apply_scalar_with_clamp(frame->tint.y, change.tintG, clamp01);
|
|
apply_scalar_with_clamp(frame->tint.z, change.tintB, clamp01);
|
|
apply_scalar_with_clamp(frame->tint.w, change.tintA, clamp01);
|
|
}
|
|
}
|
|
|
|
bool is_special_interpolated_frames(const Element& element)
|
|
{
|
|
if (element.type == ElementType::FRAME && element.interpolation != Interpolation::NONE &&
|
|
element.interpolation != Interpolation::LINEAR)
|
|
return true;
|
|
|
|
for (const auto& child : element.children)
|
|
if (is_special_interpolated_frames(child)) return true;
|
|
return false;
|
|
}
|
|
|
|
void track_frames_bake(Element& track, int interval, bool isRoundScale, bool isRoundRotation)
|
|
{
|
|
for (int index = (int)track.children.size() - 1; index >= 0; --index)
|
|
{
|
|
auto original = track.children[index];
|
|
if (original.type != ElementType::FRAME) continue;
|
|
if (original.interpolation == Interpolation::NONE || original.interpolation == Interpolation::LINEAR) continue;
|
|
|
|
if (original.duration <= FRAME_DURATION_MIN)
|
|
{
|
|
track.children[index].interpolation = Interpolation::NONE;
|
|
continue;
|
|
}
|
|
|
|
auto nextFrame = index + 1 < (int)track.children.size() && track.children[index + 1].type == ElementType::FRAME
|
|
? track.children[index + 1]
|
|
: original;
|
|
int duration{};
|
|
int insertIndex = index;
|
|
|
|
while (duration < original.duration)
|
|
{
|
|
auto baked = original;
|
|
float amount = interpolation_factor(original.interpolation, (float)duration / original.duration);
|
|
baked.duration = std::min(interval, original.duration - duration);
|
|
baked.interpolation = Interpolation::NONE;
|
|
baked.rotation = glm::mix(original.rotation, nextFrame.rotation, amount);
|
|
baked.position = glm::mix(original.position, nextFrame.position, amount);
|
|
baked.scale = glm::mix(original.scale, nextFrame.scale, amount);
|
|
baked.colorOffset = glm::mix(original.colorOffset, nextFrame.colorOffset, amount);
|
|
baked.tint = glm::mix(original.tint, nextFrame.tint, amount);
|
|
if (isRoundScale) baked.scale = glm::vec2(glm::ivec2(baked.scale));
|
|
if (isRoundRotation) baked.rotation = (int)baked.rotation;
|
|
|
|
if (insertIndex == index)
|
|
track.children[insertIndex] = baked;
|
|
else
|
|
track.children.insert(track.children.begin() + insertIndex, baked);
|
|
|
|
duration += baked.duration;
|
|
++insertIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
void special_interpolated_frames_bake(Element& element, int interval, bool isRoundScale, bool isRoundRotation)
|
|
{
|
|
if (is_track(element))
|
|
{
|
|
track_frames_bake(element, interval, isRoundScale, isRoundRotation);
|
|
return;
|
|
}
|
|
|
|
for (auto& child : element.children)
|
|
special_interpolated_frames_bake(child, interval, isRoundScale, isRoundRotation);
|
|
}
|
|
|
|
void layer_animation_ids_remap(Element& element, const std::unordered_map<int, int>& remap)
|
|
{
|
|
if (element.type == ElementType::LAYER_ANIMATION)
|
|
if (auto it = remap.find(element.layerId); it != remap.end()) element.layerId = it->second;
|
|
|
|
for (auto& child : element.children)
|
|
layer_animation_ids_remap(child, remap);
|
|
}
|
|
|
|
Anm2::Anm2()
|
|
{
|
|
root = element_make(ElementType::ANIMATED_ACTOR);
|
|
|
|
auto info = element_make(ElementType::INFO);
|
|
info.createdOn = time::get("%m/%d/%Y %I:%M:%S %p");
|
|
|
|
auto content = element_make(ElementType::CONTENT);
|
|
content.children.push_back(element_make(ElementType::SPRITESHEETS));
|
|
content.children.push_back(element_make(ElementType::LAYERS));
|
|
content.children.push_back(element_make(ElementType::NULLS));
|
|
content.children.push_back(element_make(ElementType::EVENTS));
|
|
|
|
root.children.push_back(std::move(info));
|
|
root.children.push_back(std::move(content));
|
|
root.children.push_back(element_make(ElementType::ANIMATIONS));
|
|
}
|
|
|
|
Anm2::Anm2(const std::filesystem::path& path, std::string* errorString) : Anm2() { load(path, errorString); }
|
|
|
|
bool Anm2::load(const std::filesystem::path& path, std::string* errorString)
|
|
{
|
|
XMLDocument document{};
|
|
File file(path, "rb");
|
|
if (!file)
|
|
{
|
|
if (errorString) *errorString = "File not found.";
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
|
|
if (document.LoadFile(file.get()) != XML_SUCCESS)
|
|
{
|
|
if (errorString) *errorString = document.ErrorStr();
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
|
|
return anm2_document_load(*this, document, errorString);
|
|
}
|
|
|
|
bool Anm2::load_string(std::string_view string, std::string* errorString)
|
|
{
|
|
XMLDocument document{};
|
|
if (document.Parse(string.data(), string.size()) != XML_SUCCESS)
|
|
{
|
|
if (errorString) *errorString = document.ErrorStr();
|
|
isValid = false;
|
|
return false;
|
|
}
|
|
|
|
return anm2_document_load(*this, document, errorString);
|
|
}
|
|
|
|
bool Anm2::save(const std::filesystem::path& path, std::string* errorString, Options options) const
|
|
{
|
|
XMLDocument document{};
|
|
document.InsertFirstChild(to_element(document, options));
|
|
|
|
File file(path, "wb");
|
|
if (!file)
|
|
{
|
|
if (errorString) *errorString = "File permissions.";
|
|
return false;
|
|
}
|
|
|
|
if (document.SaveFile(file.get()) != XML_SUCCESS)
|
|
{
|
|
if (errorString) *errorString = document.ErrorStr();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string Anm2::to_string(Options options) const
|
|
{
|
|
XMLDocument document{};
|
|
document.InsertEndChild(to_element(document, options));
|
|
return xml::document_to_string(document);
|
|
}
|
|
|
|
XMLElement* Anm2::to_element(XMLDocument& document, Options options) const
|
|
{
|
|
auto serialized = normalized_for_serialize();
|
|
serialized.region_frames_sync(true);
|
|
return element_to_xml(document, serialized.root, ElementType::UNKNOWN, options.flags);
|
|
}
|
|
|
|
std::uint64_t Anm2::hash(Options options) const { return std::hash<std::string>{}(to_string(options)); }
|
|
|
|
bool Anm2::is_special_interpolated_frames() const { return ::anm2ed::is_special_interpolated_frames(root); }
|
|
|
|
void Anm2::special_interpolated_frames_bake(int interval, bool isRoundScale, bool isRoundRotation)
|
|
{
|
|
::anm2ed::special_interpolated_frames_bake(root, std::max(interval, FRAME_DURATION_MIN), isRoundScale,
|
|
isRoundRotation);
|
|
}
|
|
|
|
void Anm2::region_frames_sync(bool isClearInvalid)
|
|
{
|
|
auto content = child_first_get(root, ElementType::CONTENT);
|
|
auto layers = content ? child_first_get(*content, ElementType::LAYERS) : nullptr;
|
|
auto spritesheets = content ? child_first_get(*content, ElementType::SPRITESHEETS) : nullptr;
|
|
auto animations = element_first_get(root, ElementType::ANIMATIONS);
|
|
if (!layers || !spritesheets || !animations) return;
|
|
|
|
for (auto& animation : animations->children)
|
|
{
|
|
if (animation.type != ElementType::ANIMATION) continue;
|
|
auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS);
|
|
if (!layerAnimations) continue;
|
|
|
|
tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION, [&](Element& layerAnimation)
|
|
{
|
|
auto layer = child_id_get(*layers, ElementType::LAYER_ELEMENT, layerAnimation.layerId);
|
|
auto spritesheet =
|
|
layer ? child_id_get(*spritesheets, ElementType::SPRITESHEET, layer->spritesheetId) : nullptr;
|
|
if (!spritesheet) return;
|
|
|
|
for (auto& frame : layerAnimation.children)
|
|
{
|
|
if (frame.type != ElementType::FRAME || frame.regionId == -1) continue;
|
|
auto region = child_id_get(*spritesheet, ElementType::REGION, frame.regionId);
|
|
if (!region)
|
|
{
|
|
if (isClearInvalid) frame.regionId = -1;
|
|
continue;
|
|
}
|
|
|
|
frame.crop = region->crop;
|
|
frame.size = region->size;
|
|
frame.pivot = region->pivot;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Anm2 Anm2::normalized_for_serialize() const
|
|
{
|
|
auto normalized = *this;
|
|
groups_flatten(normalized.root);
|
|
|
|
auto content = child_first_get(normalized.root, ElementType::CONTENT);
|
|
if (content) std::erase_if(content->children, [](const Element& element) { return element.tag == "Groups"; });
|
|
auto layers = content ? child_first_get(*content, ElementType::LAYERS) : nullptr;
|
|
if (!layers) return normalized;
|
|
|
|
std::unordered_map<int, int> remap{};
|
|
int nextId{};
|
|
for (auto& layer : layers->children)
|
|
{
|
|
if (layer.type != ElementType::LAYER_ELEMENT) continue;
|
|
remap[layer.id] = nextId;
|
|
layer.id = nextId++;
|
|
}
|
|
|
|
layer_animation_ids_remap(normalized.root, remap);
|
|
return normalized;
|
|
}
|
|
|
|
Element* Anm2::element_get(ElementType type)
|
|
{
|
|
if (type == ElementType::ANIMATED_ACTOR) return &root;
|
|
if (type == ElementType::ANIMATIONS) return element_first_get(root, type);
|
|
|
|
auto content = child_first_get(root, ElementType::CONTENT);
|
|
if (!content) return nullptr;
|
|
if (type == ElementType::CONTENT) return content;
|
|
if (auto container = child_first_get(*content, type)) return container;
|
|
|
|
switch (type)
|
|
{
|
|
case ElementType::SPRITESHEETS:
|
|
case ElementType::LAYERS:
|
|
case ElementType::NULLS:
|
|
case ElementType::EVENTS:
|
|
case ElementType::SOUNDS:
|
|
content->children.push_back(element_make(type));
|
|
return &content->children.back();
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return element_first_get(root, type);
|
|
}
|
|
|
|
const Element* Anm2::element_get(ElementType type) const
|
|
{
|
|
if (type == ElementType::ANIMATED_ACTOR) return &root;
|
|
if (type == ElementType::ANIMATIONS) return element_first_get(root, type);
|
|
|
|
auto content = child_first_get(root, ElementType::CONTENT);
|
|
if (!content) return nullptr;
|
|
if (type == ElementType::CONTENT) return content;
|
|
if (auto container = child_first_get(*content, type)) return container;
|
|
return element_first_get(root, type);
|
|
}
|
|
|
|
Element* Anm2::element_get(ElementType type, int id)
|
|
{
|
|
if (type == ElementType::ANIMATION)
|
|
{
|
|
auto animations = element_get(ElementType::ANIMATIONS);
|
|
if (!animations || id < 0) return nullptr;
|
|
int current{};
|
|
for (auto& animation : animations->children)
|
|
{
|
|
if (animation.type != ElementType::ANIMATION) continue;
|
|
if (current == id) return &animation;
|
|
++current;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
auto container = element_get(element_container_type_get(type));
|
|
return container ? child_id_get(*container, type, id) : nullptr;
|
|
}
|
|
|
|
const Element* Anm2::element_get(ElementType type, int id) const
|
|
{
|
|
if (type == ElementType::ANIMATION)
|
|
{
|
|
auto animations = element_get(ElementType::ANIMATIONS);
|
|
if (!animations || id < 0) return nullptr;
|
|
int current{};
|
|
for (const auto& animation : animations->children)
|
|
{
|
|
if (animation.type != ElementType::ANIMATION) continue;
|
|
if (current == id) return &animation;
|
|
++current;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
auto container = element_get(element_container_type_get(type));
|
|
return container ? child_id_get(*container, type, id) : nullptr;
|
|
}
|
|
|
|
Element* Anm2::element_get(int animationIndex, ItemType type, int id)
|
|
{
|
|
auto animation = element_get(ElementType::ANIMATION, animationIndex);
|
|
return animation ? animation_item_get(*animation, type, id) : nullptr;
|
|
}
|
|
|
|
const Element* Anm2::element_get(int animationIndex, ItemType type, int id) const
|
|
{
|
|
auto animation = element_get(ElementType::ANIMATION, animationIndex);
|
|
return animation ? animation_item_get(*animation, type, id) : nullptr;
|
|
}
|
|
|
|
Element* Anm2::element_get(int animationIndex, ItemType type, int frameIndex, int id)
|
|
{
|
|
auto item = element_get(animationIndex, type, id);
|
|
return item ? track_frame_get(*item, frameIndex) : nullptr;
|
|
}
|
|
|
|
const Element* Anm2::element_get(int animationIndex, ItemType type, int frameIndex, int id) const
|
|
{
|
|
auto item = element_get(animationIndex, type, id);
|
|
return item ? track_frame_get(*item, frameIndex) : nullptr;
|
|
}
|
|
|
|
std::set<int> Anm2::element_unused(ElementType type) const
|
|
{
|
|
std::set<int> used{};
|
|
|
|
if (type == ElementType::SPRITESHEET)
|
|
{
|
|
if (auto layers = element_get(ElementType::LAYERS))
|
|
for (const auto& layer : layers->children)
|
|
if (layer.type == ElementType::LAYER_ELEMENT && layer.spritesheetId != -1) used.insert(layer.spritesheetId);
|
|
}
|
|
else if (auto animations = element_get(ElementType::ANIMATIONS))
|
|
for (const auto& animation : animations->children)
|
|
{
|
|
if (animation.type != ElementType::ANIMATION) continue;
|
|
if (type == ElementType::EVENT_ELEMENT)
|
|
{
|
|
auto triggers = child_first_get(animation, ElementType::TRIGGERS);
|
|
if (!triggers) continue;
|
|
for (const auto& trigger : triggers->children)
|
|
if (trigger.type == ElementType::TRIGGER && trigger.eventId != -1) used.insert(trigger.eventId);
|
|
}
|
|
else if (type == ElementType::SOUND_ELEMENT)
|
|
{
|
|
auto triggers = child_first_get(animation, ElementType::TRIGGERS);
|
|
if (!triggers) continue;
|
|
for (const auto& trigger : triggers->children)
|
|
{
|
|
if (trigger.type != ElementType::TRIGGER) continue;
|
|
for (auto id : trigger.soundIds)
|
|
used.insert(id);
|
|
}
|
|
}
|
|
else if (type == ElementType::LAYER_ELEMENT || type == ElementType::NULL_ELEMENT)
|
|
{
|
|
auto containerType =
|
|
type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATIONS : ElementType::NULL_ANIMATIONS;
|
|
auto tracks = child_first_get(animation, containerType);
|
|
if (!tracks) continue;
|
|
auto trackType =
|
|
type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATION : ElementType::NULL_ANIMATION;
|
|
tracks_each(*tracks, trackType,
|
|
[&](const Element& track)
|
|
{
|
|
if (track.type == ElementType::LAYER_ANIMATION)
|
|
used.insert(track.layerId);
|
|
else if (track.type == ElementType::NULL_ANIMATION)
|
|
used.insert(track.nullId);
|
|
});
|
|
}
|
|
}
|
|
|
|
std::set<int> unused{};
|
|
if (auto container = element_get(element_container_type_get(type)))
|
|
for (const auto& element : container->children)
|
|
if (element.type == type && !used.contains(element.id)) unused.insert(element.id);
|
|
return unused;
|
|
}
|
|
|
|
std::set<int> Anm2::element_unused(ElementType type, const Element& animation) const
|
|
{
|
|
if (type != ElementType::LAYER_ELEMENT && type != ElementType::NULL_ELEMENT) return {};
|
|
|
|
std::set<int> used{};
|
|
auto containerType =
|
|
type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATIONS : ElementType::NULL_ANIMATIONS;
|
|
if (auto tracks = child_first_get(animation, containerType))
|
|
{
|
|
auto trackType = type == ElementType::LAYER_ELEMENT ? ElementType::LAYER_ANIMATION : ElementType::NULL_ANIMATION;
|
|
tracks_each(*tracks, trackType,
|
|
[&](const Element& track)
|
|
{
|
|
if (track.type == ElementType::LAYER_ANIMATION)
|
|
used.insert(track.layerId);
|
|
else if (track.type == ElementType::NULL_ANIMATION)
|
|
used.insert(track.nullId);
|
|
});
|
|
}
|
|
|
|
std::set<int> unused{};
|
|
if (auto container = element_get(element_container_type_get(type)))
|
|
for (const auto& element : container->children)
|
|
if (element.type == type && !used.contains(element.id)) unused.insert(element.id);
|
|
return unused;
|
|
}
|
|
|
|
std::set<int> Anm2::element_unused(ElementType type, int parentId) const
|
|
{
|
|
if (type != ElementType::REGION) return {};
|
|
|
|
std::set<int> used{};
|
|
auto animations = element_first_get(root, ElementType::ANIMATIONS);
|
|
if (animations)
|
|
for (const auto& animation : animations->children)
|
|
{
|
|
if (animation.type != ElementType::ANIMATION) continue;
|
|
auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS);
|
|
if (!layerAnimations) continue;
|
|
tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION, [&](const Element& layerAnimation)
|
|
{
|
|
for (const auto& frame : layerAnimation.children)
|
|
if (frame.type == ElementType::FRAME && frame.regionId != -1) used.insert(frame.regionId);
|
|
});
|
|
}
|
|
|
|
std::set<int> unused{};
|
|
if (auto spritesheet = element_get(ElementType::SPRITESHEET, parentId))
|
|
for (const auto& region : spritesheet->children)
|
|
if (region.type == ElementType::REGION && !used.contains(region.id)) unused.insert(region.id);
|
|
return unused;
|
|
}
|
|
|
|
bool Anm2::deserialize(ElementType type, const std::string& string, bool isAppend, std::string* errorString,
|
|
const std::filesystem::path& directory)
|
|
{
|
|
XMLDocument document{};
|
|
if (document.Parse(string.c_str()) != XML_SUCCESS)
|
|
{
|
|
if (errorString) *errorString = document.ErrorStr();
|
|
return false;
|
|
}
|
|
|
|
auto tag = element_tag_get(type);
|
|
if (tag.empty() || !document.FirstChildElement(tag.data()))
|
|
{
|
|
if (errorString) *errorString = std::format("No valid {}(s).", tag);
|
|
return false;
|
|
}
|
|
|
|
auto containerType = element_container_type_get(type);
|
|
auto container = element_get(containerType);
|
|
if (!container)
|
|
{
|
|
if (errorString) *errorString = std::format("No {} container.", element_tag_get(containerType));
|
|
return false;
|
|
}
|
|
|
|
std::optional<WorkingDirectory> workingDirectory{};
|
|
if ((type == ElementType::SOUND_ELEMENT || type == ElementType::SPRITESHEET) && !directory.empty())
|
|
workingDirectory.emplace(directory);
|
|
|
|
for (auto xmlElement = document.FirstChildElement(tag.data()); xmlElement;
|
|
xmlElement = xmlElement->NextSiblingElement(tag.data()))
|
|
{
|
|
auto element = element_read(xmlElement);
|
|
if (element.type != type) continue;
|
|
if (isAppend)
|
|
element.id = element_child_next_id_get(*container, type);
|
|
else
|
|
element_child_id_erase(*container, type, element.id);
|
|
if (type == ElementType::SOUND_ELEMENT || type == ElementType::SPRITESHEET)
|
|
element.path = path::backslash_handle(element.path);
|
|
container->children.push_back(element);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Anm2::regions_deserialize(int spritesheetId, const std::string& string, bool isAppend, std::string* errorString)
|
|
{
|
|
XMLDocument document{};
|
|
if (document.Parse(string.c_str()) != XML_SUCCESS)
|
|
{
|
|
if (errorString) *errorString = document.ErrorStr();
|
|
return false;
|
|
}
|
|
|
|
if (!document.FirstChildElement("Region"))
|
|
{
|
|
if (errorString) *errorString = "No valid region(s).";
|
|
return false;
|
|
}
|
|
|
|
auto spritesheet = element_get(ElementType::SPRITESHEET, spritesheetId);
|
|
if (!spritesheet)
|
|
{
|
|
if (errorString) *errorString = "No spritesheet.";
|
|
return false;
|
|
}
|
|
|
|
for (auto element = document.FirstChildElement("Region"); element; element = element->NextSiblingElement("Region"))
|
|
{
|
|
auto region = element_read(element);
|
|
if (region.type != ElementType::REGION) continue;
|
|
if (isAppend)
|
|
region.id = element_child_next_id_get(*spritesheet, ElementType::REGION);
|
|
else
|
|
element_child_id_erase(*spritesheet, ElementType::REGION, region.id);
|
|
spritesheet->children.push_back(region);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Element Anm2::frame_effective(int layerId, const Element& frame) const
|
|
{
|
|
auto resolved = frame;
|
|
if (frame.regionId == -1) return resolved;
|
|
|
|
auto layer = element_get(ElementType::LAYER_ELEMENT, layerId);
|
|
if (!layer) return resolved;
|
|
|
|
auto spritesheet = element_get(ElementType::SPRITESHEET, layer->spritesheetId);
|
|
if (!spritesheet) return resolved;
|
|
|
|
auto region = element_child_id_get(*spritesheet, ElementType::REGION, frame.regionId);
|
|
if (!region) return resolved;
|
|
|
|
resolved.crop = region->crop;
|
|
resolved.size = region->size;
|
|
resolved.pivot = region->pivot;
|
|
return resolved;
|
|
}
|
|
|
|
glm::vec4 Anm2::animation_rect(const Element& animation, bool isRootTransform) const
|
|
{
|
|
constexpr glm::ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
|
|
|
|
float minX = std::numeric_limits<float>::infinity();
|
|
float minY = std::numeric_limits<float>::infinity();
|
|
float maxX = -std::numeric_limits<float>::infinity();
|
|
float maxY = -std::numeric_limits<float>::infinity();
|
|
bool isAny{};
|
|
|
|
for (float t = 0.0f; t < (float)animation.frameNum; t += 1.0f)
|
|
{
|
|
glm::mat4 transform(1.0f);
|
|
|
|
auto root = animation_item_get(animation, ItemType::ROOT);
|
|
if (isRootTransform && root)
|
|
{
|
|
auto rootFrame = frame_generate(*root, t);
|
|
transform *= math::quad_model_parent_get(rootFrame.position, {}, math::percent_to_unit(rootFrame.scale),
|
|
rootFrame.rotation);
|
|
}
|
|
|
|
auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS);
|
|
if (!layerAnimations) continue;
|
|
|
|
tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION, [&](const Element& layerAnimation)
|
|
{
|
|
if (!layerAnimation.isVisible || !is_track_group_visible(*layerAnimations, layerAnimation)) return;
|
|
|
|
auto frame = frame_effective(layerAnimation.layerId, frame_generate(layerAnimation, t));
|
|
if (frame.size == glm::vec2() || !frame.isVisible) return;
|
|
|
|
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
|
|
math::percent_to_unit(frame.scale), frame.rotation);
|
|
for (auto& corner : CORNERS)
|
|
{
|
|
auto world = layerTransform * glm::vec4(corner, 0.0f, 1.0f);
|
|
minX = std::min(minX, world.x);
|
|
minY = std::min(minY, world.y);
|
|
maxX = std::max(maxX, world.x);
|
|
maxY = std::max(maxY, world.y);
|
|
isAny = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!isAny) return glm::vec4(-1.0f);
|
|
return {minX, minY, maxX - minX, maxY - minY};
|
|
}
|
|
|
|
Element* animation_container_get(Element& animation, ElementType type)
|
|
{
|
|
if (auto container = child_first_get(animation, type)) return container;
|
|
animation.children.push_back(element_make(type));
|
|
return &animation.children.back();
|
|
}
|
|
|
|
int Anm2::layer_animation_add(int animationIndex, int id, int insertBeforeId, std::string name, int spritesheetId,
|
|
types::destination::Type destination)
|
|
{
|
|
auto layers = element_get(ElementType::LAYERS);
|
|
if (!layers) return -1;
|
|
|
|
id = id == -1 ? element_child_next_id_get(*layers, ElementType::LAYER_ELEMENT) : id;
|
|
auto layer = element_child_id_get(*layers, ElementType::LAYER_ELEMENT, id);
|
|
if (!layer)
|
|
{
|
|
layers->children.push_back(element_make(ElementType::LAYER_ELEMENT));
|
|
layer = &layers->children.back();
|
|
layer->id = id;
|
|
}
|
|
|
|
if (!name.empty()) layer->name = name;
|
|
layer->spritesheetId = element_get(ElementType::SPRITESHEET, spritesheetId) ? spritesheetId : 0;
|
|
|
|
auto add = [&](Element& animation)
|
|
{
|
|
if (animation_item_get(animation, ItemType::LAYER, id)) return;
|
|
auto layerAnimations = animation_container_get(animation, ElementType::LAYER_ANIMATIONS);
|
|
auto item = element_make(ElementType::LAYER_ANIMATION);
|
|
item.layerId = id;
|
|
|
|
if (insertBeforeId != -1)
|
|
for (auto it = layerAnimations->children.begin(); it != layerAnimations->children.end(); ++it)
|
|
if (it->type == ElementType::LAYER_ANIMATION && it->layerId == insertBeforeId)
|
|
{
|
|
layerAnimations->children.insert(it, item);
|
|
return;
|
|
}
|
|
|
|
layerAnimations->children.push_back(item);
|
|
};
|
|
|
|
if (destination == types::destination::ALL)
|
|
{
|
|
if (auto animations = element_get(ElementType::ANIMATIONS))
|
|
for (auto& animation : animations->children)
|
|
if (animation.type == ElementType::ANIMATION) add(animation);
|
|
}
|
|
else if (auto animation = element_get(ElementType::ANIMATION, animationIndex))
|
|
add(*animation);
|
|
|
|
return id;
|
|
}
|
|
|
|
int Anm2::null_animation_add(int animationIndex, int id, std::string name, bool isShowRect,
|
|
types::destination::Type destination)
|
|
{
|
|
auto nulls = element_get(ElementType::NULLS);
|
|
if (!nulls) return -1;
|
|
|
|
id = id == -1 ? element_child_next_id_get(*nulls, ElementType::NULL_ELEMENT) : id;
|
|
auto null = element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id);
|
|
if (!null)
|
|
{
|
|
nulls->children.push_back(element_make(ElementType::NULL_ELEMENT));
|
|
null = &nulls->children.back();
|
|
null->id = id;
|
|
}
|
|
|
|
if (!name.empty()) null->name = name;
|
|
null->isShowRect = isShowRect;
|
|
|
|
auto add = [&](Element& animation)
|
|
{
|
|
if (animation_item_get(animation, ItemType::NULL_, id)) return;
|
|
auto nullAnimations = animation_container_get(animation, ElementType::NULL_ANIMATIONS);
|
|
auto item = element_make(ElementType::NULL_ANIMATION);
|
|
item.nullId = id;
|
|
nullAnimations->children.push_back(item);
|
|
};
|
|
|
|
if (destination == types::destination::ALL)
|
|
{
|
|
if (auto animations = element_get(ElementType::ANIMATIONS))
|
|
for (auto& animation : animations->children)
|
|
if (animation.type == ElementType::ANIMATION) add(animation);
|
|
}
|
|
else if (auto animation = element_get(ElementType::ANIMATION, animationIndex))
|
|
add(*animation);
|
|
|
|
return id;
|
|
}
|
|
|
|
bool Anm2::animations_deserialize(const std::string& string, int start, std::set<int>& indices,
|
|
std::string* errorString)
|
|
{
|
|
XMLDocument document{};
|
|
if (document.Parse(string.c_str()) != XML_SUCCESS)
|
|
{
|
|
if (errorString) *errorString = document.ErrorStr();
|
|
return false;
|
|
}
|
|
|
|
if (!document.FirstChildElement("Animation"))
|
|
{
|
|
if (errorString) *errorString = "No valid animation(s).";
|
|
return false;
|
|
}
|
|
|
|
auto animations = element_get(ElementType::ANIMATIONS);
|
|
if (!animations)
|
|
{
|
|
if (errorString) *errorString = "No animations container.";
|
|
return false;
|
|
}
|
|
|
|
start = std::clamp(start, 0, (int)animations->children.size());
|
|
int count{};
|
|
for (auto element = document.FirstChildElement("Animation"); element;
|
|
element = element->NextSiblingElement("Animation"))
|
|
{
|
|
auto index = start + count;
|
|
animations->children.insert(animations->children.begin() + index, element_read(element));
|
|
indices.insert(index);
|
|
++count;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int track_length_get(const Element& track)
|
|
{
|
|
int length{};
|
|
if (track.type == ElementType::TRIGGERS)
|
|
{
|
|
for (const auto& trigger : track.children)
|
|
if (trigger.type == ElementType::TRIGGER) length = std::max(length, trigger.atFrame);
|
|
return length;
|
|
}
|
|
|
|
for (const auto& frame : track.children)
|
|
if (frame.type == ElementType::FRAME) length += frame.duration;
|
|
return length;
|
|
}
|
|
|
|
int animation_length_get(const Element& animation)
|
|
{
|
|
int length{};
|
|
if (auto rootAnimation = child_first_get(animation, ElementType::ROOT_ANIMATION))
|
|
length = std::max(length, track_length_get(*rootAnimation));
|
|
if (auto layerAnimations = child_first_get(animation, ElementType::LAYER_ANIMATIONS))
|
|
tracks_each(*layerAnimations, ElementType::LAYER_ANIMATION,
|
|
[&](const Element& track) { length = std::max(length, track_length_get(track)); });
|
|
if (auto nullAnimations = child_first_get(animation, ElementType::NULL_ANIMATIONS))
|
|
tracks_each(*nullAnimations, ElementType::NULL_ANIMATION,
|
|
[&](const Element& track) { length = std::max(length, track_length_get(track)); });
|
|
if (auto triggers = child_first_get(animation, ElementType::TRIGGERS))
|
|
length = std::max(length, track_length_get(*triggers));
|
|
return std::max(length, FRAME_DURATION_MIN);
|
|
}
|
|
|
|
Element* track_get(Element& tracks, const Element& source)
|
|
{
|
|
for (auto& track : tracks.children)
|
|
{
|
|
if (track.type == ElementType::GROUP)
|
|
if (auto result = track_get(track, source)) return result;
|
|
if (track.type != source.type) continue;
|
|
if (track.type == ElementType::LAYER_ANIMATION && track.layerId == source.layerId) return &track;
|
|
if (track.type == ElementType::NULL_ANIMATION && track.nullId == source.nullId) return &track;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void track_merge(Element& destination, const Element& source, types::merge::Type type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case types::merge::APPEND:
|
|
destination.children.insert(destination.children.end(), source.children.begin(), source.children.end());
|
|
break;
|
|
case types::merge::PREPEND:
|
|
destination.children.insert(destination.children.begin(), source.children.begin(), source.children.end());
|
|
break;
|
|
case types::merge::REPLACE:
|
|
if (destination.children.size() < source.children.size()) destination.children.resize(source.children.size());
|
|
for (int i = 0; i < (int)source.children.size(); ++i)
|
|
destination.children[i] = source.children[i];
|
|
break;
|
|
case types::merge::IGNORE:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void animation_tracks_merge(Element& destination, const Element& source, ElementType containerType,
|
|
types::merge::Type type)
|
|
{
|
|
auto sourceTracks = child_first_get(source, containerType);
|
|
if (!sourceTracks) return;
|
|
|
|
auto destinationTracks = child_first_get(destination, containerType);
|
|
if (!destinationTracks)
|
|
{
|
|
destination.children.push_back(element_make(containerType));
|
|
destinationTracks = &destination.children.back();
|
|
}
|
|
|
|
for (const auto& sourceTrack : sourceTracks->children)
|
|
{
|
|
if (sourceTrack.type == ElementType::GROUP)
|
|
destinationTracks->children.push_back(sourceTrack);
|
|
else if (auto destinationTrack = track_get(*destinationTracks, sourceTrack))
|
|
track_merge(*destinationTrack, sourceTrack, type);
|
|
else
|
|
destinationTracks->children.push_back(sourceTrack);
|
|
}
|
|
}
|
|
|
|
int Anm2::animations_merge(int target, std::set<int>& sources, types::merge::Type type, bool isDeleteAfter)
|
|
{
|
|
auto animations = element_get(ElementType::ANIMATIONS);
|
|
auto targetAnimation = element_get(ElementType::ANIMATION, target);
|
|
if (!animations || !targetAnimation) return target;
|
|
|
|
if (!targetAnimation->name.ends_with(ANIMATION_MERGED_SUFFIX))
|
|
targetAnimation->name += std::string(ANIMATION_MERGED_SUFFIX);
|
|
|
|
for (auto index : sources)
|
|
{
|
|
if (index == target) continue;
|
|
auto source = element_get(ElementType::ANIMATION, index);
|
|
targetAnimation = element_get(ElementType::ANIMATION, target);
|
|
if (!source || !targetAnimation) continue;
|
|
|
|
if (auto sourceRoot = child_first_get(*source, ElementType::ROOT_ANIMATION))
|
|
{
|
|
auto targetRoot = child_first_get(*targetAnimation, ElementType::ROOT_ANIMATION);
|
|
if (!targetRoot)
|
|
{
|
|
targetAnimation->children.push_back(element_make(ElementType::ROOT_ANIMATION));
|
|
targetRoot = &targetAnimation->children.back();
|
|
}
|
|
track_merge(*targetRoot, *sourceRoot, type);
|
|
}
|
|
|
|
animation_tracks_merge(*targetAnimation, *source, ElementType::LAYER_ANIMATIONS, type);
|
|
animation_tracks_merge(*targetAnimation, *source, ElementType::NULL_ANIMATIONS, type);
|
|
|
|
if (auto sourceTriggers = child_first_get(*source, ElementType::TRIGGERS))
|
|
{
|
|
auto targetTriggers = child_first_get(*targetAnimation, ElementType::TRIGGERS);
|
|
if (!targetTriggers)
|
|
{
|
|
targetAnimation->children.push_back(element_make(ElementType::TRIGGERS));
|
|
targetTriggers = &targetAnimation->children.back();
|
|
}
|
|
track_merge(*targetTriggers, *sourceTriggers, type);
|
|
}
|
|
}
|
|
|
|
int finalIndex = target;
|
|
if (isDeleteAfter)
|
|
for (auto it = sources.rbegin(); it != sources.rend(); ++it)
|
|
{
|
|
auto source = *it;
|
|
if (source == target || source < 0 || source >= (int)animations->children.size()) continue;
|
|
animations->children.erase(animations->children.begin() + source);
|
|
if (source < finalIndex) --finalIndex;
|
|
}
|
|
|
|
if (auto finalAnimation = element_get(ElementType::ANIMATION, finalIndex))
|
|
finalAnimation->frameNum = animation_length_get(*finalAnimation);
|
|
return finalIndex;
|
|
}
|
|
}
|