Files
anm2ed/src/anm2/anm2.cpp
T
2026-06-07 12:53:08 -04:00

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;
}
}