Mega Region Update.

This commit is contained in:
2026-02-05 21:34:42 -05:00
parent 00bff4a91f
commit 64d6a1d95a
45 changed files with 1590 additions and 205 deletions
+12 -7
View File
@@ -78,34 +78,37 @@ namespace anm2ed::anm2
}
}
XMLElement* Animation::to_element(XMLDocument& document)
XMLElement* Animation::to_element(XMLDocument& document, Flags flags)
{
auto element = document.NewElement("Animation");
element->SetAttribute("Name", name.c_str());
element->SetAttribute("FrameNum", frameNum);
element->SetAttribute("Loop", isLoop);
rootAnimation.serialize(document, element, ROOT);
rootAnimation.serialize(document, element, ROOT, -1, flags);
auto layerAnimationsElement = document.NewElement("LayerAnimations");
for (auto& i : layerOrder)
{
Item& layerAnimation = layerAnimations.at(i);
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i);
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i, flags);
}
element->InsertEndChild(layerAnimationsElement);
auto nullAnimationsElement = document.NewElement("NullAnimations");
for (auto& [id, nullAnimation] : nullAnimations)
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id);
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id, flags);
element->InsertEndChild(nullAnimationsElement);
triggers.serialize(document, element, TRIGGER);
triggers.serialize(document, element, TRIGGER, -1, flags);
return element;
}
void Animation::serialize(XMLDocument& document, XMLElement* parent) { parent->InsertEndChild(to_element(document)); }
void Animation::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
{
parent->InsertEndChild(to_element(document, flags));
}
std::string Animation::to_string()
{
@@ -158,6 +161,8 @@ namespace anm2ed::anm2
for (auto& [id, layerAnimation] : layerAnimations)
{
if (!layerAnimation.isVisible) continue;
auto frame = layerAnimation.frame_generate(t, LAYER);
if (frame.size == vec2() || !frame.isVisible) continue;
@@ -179,4 +184,4 @@ namespace anm2ed::anm2
if (!any) return vec4(-1.0f);
return {minX, minY, maxX - minX, maxY - minY};
}
}
}
+3 -3
View File
@@ -26,12 +26,12 @@ namespace anm2ed::anm2
Animation(tinyxml2::XMLElement*);
Item* item_get(Type, int = -1);
void item_remove(Type, int = -1);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
std::string to_string();
int length();
void fit_length();
glm::vec4 rect(bool);
};
}
}
+5 -5
View File
@@ -16,18 +16,18 @@ namespace anm2ed::anm2
items.push_back(Animation(child));
}
XMLElement* Animations::to_element(XMLDocument& document)
XMLElement* Animations::to_element(XMLDocument& document, Flags flags)
{
auto element = document.NewElement("Animations");
element->SetAttribute("DefaultAnimation", defaultAnimation.c_str());
for (auto& animation : items)
animation.serialize(document, element);
animation.serialize(document, element, flags);
return element;
}
void Animations::serialize(XMLDocument& document, XMLElement* parent)
void Animations::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
{
parent->InsertEndChild(to_element(document));
parent->InsertEndChild(to_element(document, flags));
}
int Animations::length()
@@ -40,4 +40,4 @@ namespace anm2ed::anm2
return length;
}
}
}
+3 -3
View File
@@ -13,8 +13,8 @@ namespace anm2ed::anm2
Animations() = default;
Animations(tinyxml2::XMLElement*);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
int length();
};
}
}
+7 -7
View File
@@ -82,22 +82,22 @@ namespace anm2ed::anm2
region_frames_sync(*this, true);
}
XMLElement* Anm2::to_element(XMLDocument& document)
XMLElement* Anm2::to_element(XMLDocument& document, Flags flags)
{
region_frames_sync(*this, true);
auto element = document.NewElement("AnimatedActor");
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
content.serialize(document, element, flags);
animations.serialize(document, element, flags);
return element;
}
bool Anm2::serialize(const std::filesystem::path& path, std::string* errorString)
bool Anm2::serialize(const std::filesystem::path& path, std::string* errorString, Flags flags)
{
XMLDocument document;
document.InsertFirstChild(to_element(document));
document.InsertFirstChild(to_element(document, flags));
File file(path, "wb");
if (!file)
@@ -114,10 +114,10 @@ namespace anm2ed::anm2
return true;
}
std::string Anm2::to_string()
std::string Anm2::to_string(Flags flags)
{
XMLDocument document{};
document.InsertEndChild(to_element(document));
document.InsertEndChild(to_element(document, flags));
return xml::document_to_string(document);
}
+7 -3
View File
@@ -31,22 +31,26 @@ namespace anm2ed::anm2
Animations animations{};
Anm2();
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
bool serialize(const std::filesystem::path&, std::string* = nullptr);
std::string to_string();
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
bool serialize(const std::filesystem::path&, std::string* = nullptr, Flags = 0);
std::string to_string(Flags = 0);
Anm2(const std::filesystem::path&, std::string* = nullptr);
uint64_t hash();
Spritesheet* spritesheet_get(int);
bool spritesheet_add(const std::filesystem::path&, const std::filesystem::path&, int&);
bool spritesheet_pack(int);
bool regions_trim(int, const std::set<int>&);
std::vector<std::string> spritesheet_labels_get();
std::vector<int> spritesheet_ids_get();
std::set<int> spritesheets_unused();
bool spritesheets_merge(const std::set<int>&, SpritesheetMergeOrigin, bool, origin::Type);
bool spritesheets_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type type,
std::string*);
std::vector<std::string> region_labels_get(Spritesheet&);
std::vector<int> region_ids_get(Spritesheet&);
std::set<int> regions_unused(Spritesheet&);
void scan_and_set_regions();
void layer_add(int&);
std::set<int> layers_unused();
+557 -3
View File
@@ -1,6 +1,12 @@
#include "anm2.h"
#include <algorithm>
#include <cmath>
#include <format>
#include <limits>
#include <ranges>
#include <unordered_map>
#include <vector>
#include "map_.h"
#include "path_.h"
@@ -23,6 +29,367 @@ namespace anm2ed::anm2
return true;
}
bool Anm2::spritesheet_pack(int id)
{
constexpr int PACKING_PADDING = 1;
struct RectI
{
int x{};
int y{};
int w{};
int h{};
};
struct PackItem
{
int regionID{-1};
int srcX{};
int srcY{};
int width{};
int height{};
int packWidth{};
int packHeight{};
};
class MaxRectsPacker
{
int width{};
int height{};
std::vector<RectI> freeRects{};
static bool intersects(const RectI& a, const RectI& b)
{
return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y);
}
static bool contains(const RectI& a, const RectI& b)
{
return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
}
void split_free_rects(const RectI& used)
{
std::vector<RectI> next{};
next.reserve(freeRects.size() * 2);
for (auto& free : freeRects)
{
if (!intersects(free, used))
{
next.push_back(free);
continue;
}
if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h});
if (used.x + used.w < free.x + free.w)
next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h});
if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y});
if (used.y + used.h < free.y + free.h)
next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)});
}
freeRects = std::move(next);
}
void prune_free_rects()
{
for (int i = 0; i < (int)freeRects.size(); i++)
{
if (freeRects[i].w <= 0 || freeRects[i].h <= 0)
{
freeRects.erase(freeRects.begin() + i--);
continue;
}
for (int j = i + 1; j < (int)freeRects.size();)
{
if (contains(freeRects[i], freeRects[j]))
freeRects.erase(freeRects.begin() + j);
else if (contains(freeRects[j], freeRects[i]))
{
freeRects.erase(freeRects.begin() + i);
i--;
break;
}
else
j++;
}
}
}
public:
MaxRectsPacker(int width, int height) : width(width), height(height), freeRects({{0, 0, width, height}}) {}
bool insert(int width, int height, RectI& result)
{
int bestShort = std::numeric_limits<int>::max();
int bestLong = std::numeric_limits<int>::max();
RectI best{};
bool found{};
for (auto& free : freeRects)
{
if (width > free.w || height > free.h) continue;
int leftOverW = free.w - width;
int leftOverH = free.h - height;
int shortSide = std::min(leftOverW, leftOverH);
int longSide = std::max(leftOverW, leftOverH);
if (shortSide < bestShort || (shortSide == bestShort && longSide < bestLong))
{
bestShort = shortSide;
bestLong = longSide;
best = {free.x, free.y, width, height};
found = true;
}
}
if (!found) return false;
result = best;
split_free_rects(best);
prune_free_rects();
return true;
}
};
auto pack_regions = [&](const std::vector<PackItem>& items, int& packedWidth, int& packedHeight,
std::unordered_map<int, RectI>& packedRects)
{
if (items.empty()) return false;
int maxWidth{};
int maxHeight{};
int sumWidth{};
int sumHeight{};
int64_t totalArea{};
for (auto& item : items)
{
maxWidth = std::max(maxWidth, item.packWidth);
maxHeight = std::max(maxHeight, item.packHeight);
sumWidth += item.packWidth;
sumHeight += item.packHeight;
totalArea += (int64_t)item.packWidth * item.packHeight;
}
if (maxWidth <= 0 || maxHeight <= 0) return false;
int bestSquareDelta = std::numeric_limits<int>::max();
int bestArea = std::numeric_limits<int>::max();
int bestWidth{};
int bestHeight{};
std::unordered_map<int, RectI> bestRects{};
int startWidth = maxWidth;
int endWidth = std::max(startWidth, sumWidth);
int step = std::max(1, (endWidth - startWidth) / 512);
for (int candidateWidth = startWidth; candidateWidth <= endWidth; candidateWidth += step)
{
int candidateHeightMin = std::max(maxHeight, (int)std::ceil((double)totalArea / candidateWidth));
bool isValid{};
int usedWidth{};
int usedHeight{};
std::unordered_map<int, RectI> candidateRects{};
// Grow candidate height until this width can actually fit all rectangles.
for (int candidateHeight = candidateHeightMin; candidateHeight <= sumHeight; candidateHeight++)
{
MaxRectsPacker packer(candidateWidth, candidateHeight);
candidateRects.clear();
isValid = true;
usedWidth = 0;
usedHeight = 0;
for (auto& item : items)
{
RectI rect{};
if (!packer.insert(item.packWidth, item.packHeight, rect))
{
isValid = false;
break;
}
candidateRects[item.regionID] = rect;
usedWidth = std::max(usedWidth, rect.x + rect.w);
usedHeight = std::max(usedHeight, rect.y + rect.h);
}
if (isValid) break;
}
if (!isValid) continue;
int area = usedWidth * usedHeight;
int squareDelta = std::abs(usedWidth - usedHeight);
if (squareDelta < bestSquareDelta || (squareDelta == bestSquareDelta && area < bestArea))
{
bestSquareDelta = squareDelta;
bestArea = area;
bestWidth = usedWidth;
bestHeight = usedHeight;
bestRects = std::move(candidateRects);
if (bestArea == totalArea && bestSquareDelta == 0) break;
}
}
if (bestArea == std::numeric_limits<int>::max()) return false;
packedWidth = bestWidth;
packedHeight = bestHeight;
packedRects = std::move(bestRects);
return true;
};
if (!content.spritesheets.contains(id)) return false;
auto& spritesheet = content.spritesheets.at(id);
if (!spritesheet.texture.is_valid() || spritesheet.texture.pixels.empty()) return false;
if (spritesheet.regions.empty()) return false;
std::vector<PackItem> items{};
items.reserve(spritesheet.regions.size());
for (auto& [regionID, region] : spritesheet.regions)
{
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
auto size = glm::max(maxPoint - minPoint, glm::ivec2(1));
int packWidth = size.x + PACKING_PADDING * 2;
int packHeight = size.y + PACKING_PADDING * 2;
items.push_back({regionID, minPoint.x, minPoint.y, size.x, size.y, packWidth, packHeight});
}
std::sort(items.begin(), items.end(), [](const PackItem& a, const PackItem& b)
{
int areaA = a.width * a.height;
int areaB = b.width * b.height;
if (areaA != areaB) return areaA > areaB;
return a.regionID < b.regionID;
});
int packedWidth{};
int packedHeight{};
std::unordered_map<int, RectI> packedRects{};
if (!pack_regions(items, packedWidth, packedHeight, packedRects)) return false;
if (packedWidth <= 0 || packedHeight <= 0) return false;
auto textureSize = spritesheet.texture.size;
auto& sourcePixels = spritesheet.texture.pixels;
std::vector<uint8_t> packedPixels((size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0);
for (auto& item : items)
{
if (!packedRects.contains(item.regionID)) continue;
auto destinationRect = packedRects.at(item.regionID);
for (int y = 0; y < item.height; y++)
{
for (int x = 0; x < item.width; x++)
{
int sourceX = item.srcX + x;
int sourceY = item.srcY + y;
int destinationX = destinationRect.x + PACKING_PADDING + x;
int destinationY = destinationRect.y + PACKING_PADDING + y;
if (sourceX < 0 || sourceY < 0 || sourceX >= textureSize.x || sourceY >= textureSize.y) continue;
if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight)
continue;
auto sourceIndex = ((size_t)sourceY * textureSize.x + sourceX) * resource::texture::CHANNELS;
auto destinationIndex =
((size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS;
std::copy_n(sourcePixels.data() + sourceIndex, resource::texture::CHANNELS,
packedPixels.data() + destinationIndex);
}
}
}
spritesheet.texture = resource::Texture(packedPixels.data(), {packedWidth, packedHeight});
for (auto& [regionID, region] : spritesheet.regions)
if (packedRects.contains(regionID))
{
auto& rect = packedRects.at(regionID);
region.crop = {rect.x + PACKING_PADDING, rect.y + PACKING_PADDING};
}
return true;
}
bool Anm2::regions_trim(int spritesheetID, const std::set<int>& ids)
{
auto spritesheet = spritesheet_get(spritesheetID);
if (!spritesheet || !spritesheet->texture.is_valid() || spritesheet->texture.pixels.empty() || ids.empty())
return false;
auto& texture = spritesheet->texture;
bool changed{};
for (auto id : ids)
{
if (!spritesheet->regions.contains(id)) continue;
auto& region = spritesheet->regions.at(id);
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
int minX = std::max(0, minPoint.x);
int minY = std::max(0, minPoint.y);
int maxX = std::min(texture.size.x, maxPoint.x);
int maxY = std::min(texture.size.y, maxPoint.y);
if (minX >= maxX || minY >= maxY) continue;
int contentMinX = std::numeric_limits<int>::max();
int contentMinY = std::numeric_limits<int>::max();
int contentMaxX = std::numeric_limits<int>::min();
int contentMaxY = std::numeric_limits<int>::min();
for (int y = minY; y < maxY; y++)
{
for (int x = minX; x < maxX; x++)
{
auto index = ((size_t)y * texture.size.x + x) * resource::texture::CHANNELS;
if (index + resource::texture::CHANNELS > texture.pixels.size()) continue;
auto r = texture.pixels[index + 0];
auto g = texture.pixels[index + 1];
auto b = texture.pixels[index + 2];
auto a = texture.pixels[index + 3];
if (r == 0 && g == 0 && b == 0 && a == 0) continue;
contentMinX = std::min(contentMinX, x);
contentMinY = std::min(contentMinY, y);
contentMaxX = std::max(contentMaxX, x);
contentMaxY = std::max(contentMaxY, y);
}
}
if (contentMinX == std::numeric_limits<int>::max()) continue;
auto newCrop = glm::vec2(contentMinX, contentMinY);
auto newSize = glm::vec2(contentMaxX - contentMinX + 1, contentMaxY - contentMinY + 1);
if (region.crop != newCrop || region.size != newSize)
{
auto previousCrop = region.crop;
region.crop = newCrop;
region.size = newSize;
if (region.origin == Spritesheet::Region::TOP_LEFT)
region.pivot = {};
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
region.pivot = {static_cast<int>(region.size.x / 2.0f), static_cast<int>(region.size.y / 2.0f)};
else
// Preserve the same texture-space pivot location when trimming shifts region crop.
region.pivot -= (region.crop - previousCrop);
changed = true;
}
}
return changed;
}
std::set<int> Anm2::spritesheets_unused()
{
std::set<int> used{};
@@ -36,6 +403,117 @@ namespace anm2ed::anm2
return unused;
}
bool Anm2::spritesheets_merge(const std::set<int>& ids, SpritesheetMergeOrigin mergeOrigin, bool isMakeRegions,
origin::Type regionOrigin)
{
if (ids.size() < 2) return false;
auto baseId = *ids.begin();
if (!content.spritesheets.contains(baseId)) return false;
for (auto id : ids)
if (!content.spritesheets.contains(id)) return false;
auto& base = content.spritesheets.at(baseId);
if (!base.texture.is_valid()) return false;
std::unordered_map<int, glm::ivec2> offsets{};
offsets[baseId] = {};
auto mergedTexture = base.texture;
for (auto id : ids)
{
if (id == baseId) continue;
auto& spritesheet = content.spritesheets.at(id);
if (!spritesheet.texture.is_valid()) return false;
offsets[id] = mergeOrigin == APPEND_RIGHT ? glm::ivec2(mergedTexture.size.x, 0)
: glm::ivec2(0, mergedTexture.size.y);
mergedTexture = resource::Texture::merge_append(mergedTexture, spritesheet.texture,
mergeOrigin == APPEND_RIGHT);
}
base.texture = std::move(mergedTexture);
std::unordered_map<int, std::unordered_map<int, int>> regionIdMap{};
if (isMakeRegions)
{
if (base.regionOrder.size() != base.regions.size())
{
base.regionOrder.clear();
base.regionOrder.reserve(base.regions.size());
for (auto id : base.regions | std::views::keys)
base.regionOrder.push_back(id);
}
for (auto id : ids)
{
if (id == baseId) continue;
auto& source = content.spritesheets.at(id);
auto sheetOffset = offsets.at(id);
auto locationRegionID = map::next_id_get(base.regions);
auto sourceFilename = path::to_utf8(source.path.stem());
auto locationRegionName = sourceFilename.empty() ? std::format("#{}", id) : sourceFilename;
auto locationRegionPivot =
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(source.texture.size) * 0.5f : glm::vec2();
base.regions[locationRegionID] = {
.name = locationRegionName,
.crop = sheetOffset,
.pivot = glm::ivec2(locationRegionPivot),
.size = source.texture.size,
.origin = regionOrigin,
};
base.regionOrder.push_back(locationRegionID);
for (auto& [sourceRegionID, sourceRegion] : source.regions)
{
auto destinationRegionID = map::next_id_get(base.regions);
auto destinationRegion = sourceRegion;
destinationRegion.crop += sheetOffset;
base.regions[destinationRegionID] = destinationRegion;
base.regionOrder.push_back(destinationRegionID);
regionIdMap[id][sourceRegionID] = destinationRegionID;
}
}
}
std::unordered_map<int, int> layerSpritesheetBefore{};
for (auto& [layerID, layer] : content.layers)
{
if (!ids.contains(layer.spritesheetID)) continue;
layerSpritesheetBefore[layerID] = layer.spritesheetID;
layer.spritesheetID = baseId;
}
for (auto& animation : animations.items)
{
for (auto& [layerID, item] : animation.layerAnimations)
{
if (!layerSpritesheetBefore.contains(layerID)) continue;
auto sourceSpritesheetID = layerSpritesheetBefore.at(layerID);
if (sourceSpritesheetID == baseId) continue;
for (auto& frame : item.frames)
{
if (frame.regionID == -1) continue;
if (isMakeRegions && regionIdMap.contains(sourceSpritesheetID) &&
regionIdMap.at(sourceSpritesheetID).contains(frame.regionID))
frame.regionID = regionIdMap.at(sourceSpritesheetID).at(frame.regionID);
else
frame.regionID = -1;
}
}
}
for (auto id : ids)
if (id != baseId) content.spritesheets.erase(id);
return true;
}
std::vector<std::string> Anm2::spritesheet_labels_get()
{
std::vector<std::string> labels{};
@@ -57,18 +535,60 @@ namespace anm2ed::anm2
std::vector<std::string> Anm2::region_labels_get(Spritesheet& spritesheet)
{
auto rebuild_order = [&]()
{
spritesheet.regionOrder.clear();
spritesheet.regionOrder.reserve(spritesheet.regions.size());
for (auto id : spritesheet.regions | std::views::keys)
spritesheet.regionOrder.push_back(id);
};
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
rebuild_order();
else
{
bool isOrderValid = true;
for (auto id : spritesheet.regionOrder)
if (!spritesheet.regions.contains(id))
{
isOrderValid = false;
break;
}
if (!isOrderValid) rebuild_order();
}
std::vector<std::string> labels{};
labels.emplace_back(localize.get(BASIC_NONE));
for (auto& region : spritesheet.regions | std::views::values)
labels.emplace_back(region.name);
for (auto id : spritesheet.regionOrder)
labels.emplace_back(spritesheet.regions.at(id).name);
return labels;
}
std::vector<int> Anm2::region_ids_get(Spritesheet& spritesheet)
{
auto rebuild_order = [&]()
{
spritesheet.regionOrder.clear();
spritesheet.regionOrder.reserve(spritesheet.regions.size());
for (auto id : spritesheet.regions | std::views::keys)
spritesheet.regionOrder.push_back(id);
};
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
rebuild_order();
else
{
bool isOrderValid = true;
for (auto id : spritesheet.regionOrder)
if (!spritesheet.regions.contains(id))
{
isOrderValid = false;
break;
}
if (!isOrderValid) rebuild_order();
}
std::vector<int> ids{};
ids.emplace_back(-1);
for (auto& id : spritesheet.regions | std::views::keys)
for (auto id : spritesheet.regionOrder)
ids.emplace_back(id);
return ids;
}
@@ -93,6 +613,40 @@ namespace anm2ed::anm2
return unused;
}
void Anm2::scan_and_set_regions()
{
for (auto& animation : animations.items)
{
for (auto& [layerID, item] : animation.layerAnimations)
{
auto layer = map::find(content.layers, layerID);
if (!layer) continue;
auto spritesheet = spritesheet_get(layer->spritesheetID);
if (!spritesheet || spritesheet->regions.empty()) continue;
for (auto& frame : item.frames)
{
if (frame.regionID != -1) continue;
auto frameCrop = glm::ivec2(frame.crop);
auto frameSize = glm::ivec2(frame.size);
auto framePivot = glm::ivec2(frame.pivot);
for (auto& [regionID, region] : spritesheet->regions)
{
if (glm::ivec2(region.crop) == frameCrop && glm::ivec2(region.size) == frameSize &&
glm::ivec2(region.pivot) == framePivot)
{
frame.regionID = regionID;
break;
}
}
}
}
}
}
bool Anm2::spritesheets_deserialize(const std::string& string, const std::filesystem::path& directory,
merge::Type type, std::string* errorString)
{
+30 -1
View File
@@ -6,6 +6,7 @@
#include <glm/glm/vec2.hpp>
#include <glm/glm/vec3.hpp>
#include <glm/glm/vec4.hpp>
#include <unordered_map>
namespace anm2ed::anm2
{
@@ -86,4 +87,32 @@ namespace anm2ed::anm2
MULTIPLY,
DIVIDE
};
}
enum Compatibility
{
ISAAC,
ANM2ED,
ANM2ED_LIMITED,
COUNT
};
enum SpritesheetMergeOrigin
{
APPEND_RIGHT,
APPEND_BOTTOM
};
enum Flag
{
NO_SOUNDS = 1 << 0,
NO_REGIONS = 1 << 1,
FRAME_NO_REGION_VALUES = 1 << 2
};
typedef int Flags;
inline bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; }
inline const std::unordered_map<Compatibility, Flags> COMPATIBILITY_FLAGS = {
{ISAAC, NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES}, {ANM2ED, 0}, {ANM2ED_LIMITED, FRAME_NO_REGION_VALUES}};
}
+3 -3
View File
@@ -29,13 +29,13 @@ namespace anm2ed::anm2
sounds.emplace(id, Sound(child, id));
}
void Content::serialize(XMLDocument& document, XMLElement* parent)
void Content::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
{
auto element = document.NewElement("Content");
auto spritesheetsElement = document.NewElement("Spritesheets");
for (auto& [id, spritesheet] : spritesheets)
spritesheet.serialize(document, spritesheetsElement, id);
spritesheet.serialize(document, spritesheetsElement, id, flags);
element->InsertEndChild(spritesheetsElement);
auto layersElement = document.NewElement("Layers");
@@ -53,7 +53,7 @@ namespace anm2ed::anm2
event.serialize(document, eventsElement, id);
element->InsertEndChild(eventsElement);
if (!sounds.empty())
if (!has_flag(flags, NO_SOUNDS) && !sounds.empty())
{
auto soundsElement = document.NewElement("Sounds");
for (auto& [id, sound] : sounds)
+2 -2
View File
@@ -20,7 +20,7 @@ namespace anm2ed::anm2
Content() = default;
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
Content(tinyxml2::XMLElement*);
};
}
}
+28 -18
View File
@@ -76,7 +76,7 @@ namespace anm2ed::anm2
}
}
XMLElement* Frame::to_element(XMLDocument& document, Type type)
XMLElement* Frame::to_element(XMLDocument& document, Type type, Flags flags)
{
auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame");
@@ -101,15 +101,23 @@ namespace anm2ed::anm2
element->SetAttribute("Interpolated", isInterpolated);
break;
case LAYER:
if (regionID != -1) element->SetAttribute("RegionId", regionID);
{
bool noRegions = has_flag(flags, NO_REGIONS);
bool frameNoRegionValues = has_flag(flags, FRAME_NO_REGION_VALUES);
bool writeRegionValues = !frameNoRegionValues || noRegions;
if (!noRegions && regionID != -1) element->SetAttribute("RegionId", regionID);
element->SetAttribute("XPosition", position.x);
element->SetAttribute("YPosition", position.y);
element->SetAttribute("XPivot", pivot.x);
element->SetAttribute("YPivot", pivot.y);
element->SetAttribute("XCrop", crop.x);
element->SetAttribute("YCrop", crop.y);
element->SetAttribute("Width", size.x);
element->SetAttribute("Height", size.y);
if (writeRegionValues)
{
element->SetAttribute("XPivot", pivot.x);
element->SetAttribute("YPivot", pivot.y);
element->SetAttribute("XCrop", crop.x);
element->SetAttribute("YCrop", crop.y);
element->SetAttribute("Width", size.x);
element->SetAttribute("Height", size.y);
}
element->SetAttribute("XScale", scale.x);
element->SetAttribute("YScale", scale.y);
element->SetAttribute("Delay", duration);
@@ -124,15 +132,17 @@ namespace anm2ed::anm2
element->SetAttribute("Rotation", rotation);
element->SetAttribute("Interpolated", isInterpolated);
break;
}
case TRIGGER:
if (eventID != -1) element->SetAttribute("EventId", eventID);
for (auto& id : soundIDs)
{
if (id == -1) continue;
auto soundChild = element->InsertNewChildElement("Sound");
soundChild->SetAttribute("Id", id);
}
if (!has_flag(flags, NO_SOUNDS))
for (auto& id : soundIDs)
{
if (id == -1) continue;
auto soundChild = element->InsertNewChildElement("Sound");
soundChild->SetAttribute("Id", id);
}
element->SetAttribute("AtFrame", atFrame);
break;
@@ -143,15 +153,15 @@ namespace anm2ed::anm2
return element;
}
void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type)
void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type, Flags flags)
{
parent->InsertEndChild(to_element(document, type));
parent->InsertEndChild(to_element(document, type, flags));
}
std::string Frame::to_string(Type type)
std::string Frame::to_string(Type type, Flags flags)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, type));
document.InsertEndChild(to_element(document, type, flags));
return xml::document_to_string(document);
}
+3 -3
View File
@@ -33,9 +33,9 @@ namespace anm2ed::anm2
Frame() = default;
Frame(tinyxml2::XMLElement*, Type);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type);
std::string to_string(Type type);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, Flags = 0);
std::string to_string(Type type, Flags = 0);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, Flags = 0);
void shorten();
void extend();
};
+6 -6
View File
@@ -23,7 +23,7 @@ namespace anm2ed::anm2
frames.push_back(Frame(child, type));
}
XMLElement* Item::to_element(XMLDocument& document, Type type, int id)
XMLElement* Item::to_element(XMLDocument& document, Type type, int id, Flags flags)
{
auto element = document.NewElement(TYPE_ITEM_STRINGS[type]);
@@ -34,20 +34,20 @@ namespace anm2ed::anm2
if (type == TRIGGER) frames_sort_by_at_frame();
for (auto& frame : frames)
frame.serialize(document, element, type);
frame.serialize(document, element, type, flags);
return element;
}
void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id)
void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id, Flags flags)
{
parent->InsertEndChild(to_element(document, type, id));
parent->InsertEndChild(to_element(document, type, id, flags));
}
std::string Item::to_string(Type type, int id)
std::string Item::to_string(Type type, int id, Flags flags)
{
XMLDocument document{};
document.InsertEndChild(to_element(document, type, id));
document.InsertEndChild(to_element(document, type, id, flags));
return xml::document_to_string(document);
}
+3 -3
View File
@@ -15,9 +15,9 @@ namespace anm2ed::anm2
Item() = default;
Item(tinyxml2::XMLElement*, Type, int* = nullptr);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, int);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1);
std::string to_string(Type, int = -1);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, int, Flags = 0);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1, Flags = 0);
std::string to_string(Type, int = -1, Flags = 0);
int length(Type);
Frame frame_generate(float, Type);
void frames_change(FrameChange, anm2::Type, ChangeType, std::set<int>&);
+107 -28
View File
@@ -1,11 +1,13 @@
#include "spritesheet.h"
#include <algorithm>
#include <ranges>
#include <vector>
#include "map_.h"
#include "path_.h"
#include "working_directory.h"
#include "xml_.h"
#include "map_.h"
using namespace anm2ed::resource;
using namespace anm2ed::util;
@@ -14,6 +16,31 @@ using namespace tinyxml2;
namespace anm2ed::anm2
{
namespace
{
const char* origin_to_string(Spritesheet::Region::Origin origin)
{
switch (origin)
{
case Spritesheet::Region::TOP_LEFT:
return "TopLeft";
case Spritesheet::Region::ORIGIN_CENTER:
return "Center";
case Spritesheet::Region::CUSTOM:
default:
return nullptr;
}
}
Spritesheet::Region::Origin origin_from_string(const char* originString)
{
if (!originString) return Spritesheet::Region::CUSTOM;
if (std::string(originString) == "TopLeft") return Spritesheet::Region::TOP_LEFT;
if (std::string(originString) == "Center") return Spritesheet::Region::ORIGIN_CENTER;
return Spritesheet::Region::CUSTOM;
}
}
Spritesheet::Spritesheet(XMLElement* element, int& id)
{
if (!element) return;
@@ -31,14 +58,27 @@ namespace anm2ed::anm2
int id{};
child->QueryIntAttribute("Id", &id);
xml::query_string_attribute(child, "Name", &region.name);
child->QueryFloatAttribute("CropX", &region.crop.x);
child->QueryFloatAttribute("CropY", &region.crop.y);
child->QueryFloatAttribute("PivotX", &region.pivot.x);
child->QueryFloatAttribute("PivotY", &region.pivot.y);
child->QueryFloatAttribute("XCrop", &region.crop.x);
child->QueryFloatAttribute("YCrop", &region.crop.y);
child->QueryFloatAttribute("Width", &region.size.x);
child->QueryFloatAttribute("Height", &region.size.y);
region.origin = origin_from_string(child->Attribute("Origin"));
if (region.origin == Spritesheet::Region::TOP_LEFT)
region.pivot = {};
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)};
else
{
child->QueryFloatAttribute("XPivot", &region.pivot.x);
child->QueryFloatAttribute("YPivot", &region.pivot.y);
}
regions.emplace(id, std::move(region));
}
regionOrder.clear();
regionOrder.reserve(regions.size());
for (auto id : regions | std::views::keys)
regionOrder.push_back(id);
}
Spritesheet::Spritesheet(const std::filesystem::path& directory, const std::filesystem::path& path)
@@ -49,32 +89,50 @@ namespace anm2ed::anm2
texture = Texture(this->path);
}
XMLElement* Spritesheet::to_element(XMLDocument& document, int id)
XMLElement* Spritesheet::to_element(XMLDocument& document, int id, Flags flags)
{
auto element = document.NewElement("Spritesheet");
element->SetAttribute("Id", id);
auto pathString = path::to_utf8(path);
element->SetAttribute("Path", pathString.c_str());
for (auto [i, region] : regions)
if (!has_flag(flags, NO_REGIONS))
{
auto regionElement = element->InsertNewChildElement("Region");
regionElement->SetAttribute("Id", i);
regionElement->SetAttribute("Name", region.name.c_str());
regionElement->SetAttribute("CropX", region.crop.x);
regionElement->SetAttribute("CropY", region.crop.y);
regionElement->SetAttribute("PivotX", region.pivot.x);
regionElement->SetAttribute("PivotY", region.pivot.y);
regionElement->SetAttribute("Width", region.size.x);
regionElement->SetAttribute("Height", region.size.y);
if (regionOrder.size() != regions.size())
{
regionOrder.clear();
regionOrder.reserve(regions.size());
for (auto id : regions | std::views::keys)
regionOrder.push_back(id);
}
for (auto id : regionOrder)
{
if (!regions.contains(id)) continue;
auto& region = regions.at(id);
auto regionElement = element->InsertNewChildElement("Region");
regionElement->SetAttribute("Id", id);
regionElement->SetAttribute("Name", region.name.c_str());
regionElement->SetAttribute("XCrop", region.crop.x);
regionElement->SetAttribute("YCrop", region.crop.y);
regionElement->SetAttribute("Width", region.size.x);
regionElement->SetAttribute("Height", region.size.y);
if (auto originString = origin_to_string(region.origin); originString)
regionElement->SetAttribute("Origin", originString);
else
{
regionElement->SetAttribute("XPivot", region.pivot.x);
regionElement->SetAttribute("YPivot", region.pivot.y);
}
}
}
return element;
}
void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id)
void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id, Flags flags)
{
parent->InsertEndChild(to_element(document, id));
parent->InsertEndChild(to_element(document, id, flags));
}
std::string Spritesheet::to_string(int id)
@@ -93,12 +151,17 @@ namespace anm2ed::anm2
auto& region = regions.at(id);
element->SetAttribute("Id", id);
element->SetAttribute("Name", region.name.c_str());
element->SetAttribute("CropX", region.crop.x);
element->SetAttribute("CropY", region.crop.y);
element->SetAttribute("PivotX", region.pivot.x);
element->SetAttribute("PivotY", region.pivot.y);
element->SetAttribute("XCrop", region.crop.x);
element->SetAttribute("YCrop", region.crop.y);
element->SetAttribute("Width", region.size.x);
element->SetAttribute("Height", region.size.y);
if (auto originString = origin_to_string(region.origin); originString)
element->SetAttribute("Origin", originString);
else
{
element->SetAttribute("XPivot", region.pivot.x);
element->SetAttribute("YPivot", region.pivot.y);
}
document.InsertEndChild(element);
return xml::document_to_string(document);
@@ -118,20 +181,30 @@ namespace anm2ed::anm2
return false;
}
for (auto element = document.FirstChildElement("Region"); element; element = element->NextSiblingElement("Region"))
for (auto element = document.FirstChildElement("Region"); element;
element = element->NextSiblingElement("Region"))
{
Region region{};
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &region.name);
element->QueryFloatAttribute("CropX", &region.crop.x);
element->QueryFloatAttribute("CropY", &region.crop.y);
element->QueryFloatAttribute("PivotX", &region.pivot.x);
element->QueryFloatAttribute("PivotY", &region.pivot.y);
element->QueryFloatAttribute("XCrop", &region.crop.x);
element->QueryFloatAttribute("YCrop", &region.crop.y);
element->QueryFloatAttribute("Width", &region.size.x);
element->QueryFloatAttribute("Height", &region.size.y);
region.origin = origin_from_string(element->Attribute("Origin"));
if (region.origin == Spritesheet::Region::TOP_LEFT)
region.pivot = {};
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
region.pivot = glm::ivec2(region.size / 2.0f);
else
{
element->QueryFloatAttribute("XPivot", &region.pivot.x);
element->QueryFloatAttribute("YPivot", &region.pivot.y);
}
if (type == merge::APPEND) id = map::next_id_get(regions);
regions[id] = std::move(region);
if (std::find(regionOrder.begin(), regionOrder.end(), id) == regionOrder.end()) regionOrder.push_back(id);
}
return true;
@@ -149,7 +222,13 @@ namespace anm2ed::anm2
return texture.write_png(this->path);
}
void Spritesheet::reload(const std::filesystem::path& directory) { *this = Spritesheet(directory, this->path); }
void Spritesheet::reload(const std::filesystem::path& directory, const std::filesystem::path& path)
{
WorkingDirectory workingDirectory(directory);
this->path = !path.empty() ? path::make_relative(path) : this->path;
this->path = path::lower_case_backslash_handle(this->path);
texture = Texture(this->path);
}
bool Spritesheet::is_valid() { return texture.is_valid(); }
}
+13 -3
View File
@@ -3,11 +3,14 @@
#include <filesystem>
#include <map>
#include <set>
#include <vector>
#include <string>
#include <tinyxml2/tinyxml2.h>
#include "texture.h"
#include "anm2_type.h"
#include "types.h"
#include "origin.h"
namespace anm2ed::anm2
{
@@ -16,27 +19,34 @@ namespace anm2ed::anm2
public:
struct Region
{
using Origin = origin::Type;
static constexpr Origin TOP_LEFT = origin::TOP_LEFT;
static constexpr Origin ORIGIN_CENTER = origin::ORIGIN_CENTER;
static constexpr Origin CUSTOM = origin::CUSTOM;
std::string name{};
glm::vec2 crop{};
glm::vec2 pivot{};
glm::vec2 size{};
Origin origin{CUSTOM};
};
std::filesystem::path path{};
resource::Texture texture;
std::map<int, Region> regions{};
std::vector<int> regionOrder{};
Spritesheet() = default;
Spritesheet(tinyxml2::XMLElement*, int&);
Spritesheet(const std::filesystem::path&, const std::filesystem::path& = {});
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int, Flags = 0);
std::string to_string(int id);
std::string region_to_string(int id);
bool regions_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
bool save(const std::filesystem::path&, const std::filesystem::path& = {});
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
void reload(const std::filesystem::path&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int, Flags = 0);
void reload(const std::filesystem::path&, const std::filesystem::path& = {});
bool is_valid();
};
}