Mega Region Update.
This commit is contained in:
+12
-7
@@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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}};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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", ®ion.name);
|
||||
child->QueryFloatAttribute("CropX", ®ion.crop.x);
|
||||
child->QueryFloatAttribute("CropY", ®ion.crop.y);
|
||||
child->QueryFloatAttribute("PivotX", ®ion.pivot.x);
|
||||
child->QueryFloatAttribute("PivotY", ®ion.pivot.y);
|
||||
child->QueryFloatAttribute("XCrop", ®ion.crop.x);
|
||||
child->QueryFloatAttribute("YCrop", ®ion.crop.y);
|
||||
child->QueryFloatAttribute("Width", ®ion.size.x);
|
||||
child->QueryFloatAttribute("Height", ®ion.size.y);
|
||||
region.origin = origin_from_string(child->Attribute("Origin"));
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)};
|
||||
else
|
||||
{
|
||||
child->QueryFloatAttribute("XPivot", ®ion.pivot.x);
|
||||
child->QueryFloatAttribute("YPivot", ®ion.pivot.y);
|
||||
}
|
||||
regions.emplace(id, std::move(region));
|
||||
}
|
||||
|
||||
regionOrder.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", ®ion.name);
|
||||
element->QueryFloatAttribute("CropX", ®ion.crop.x);
|
||||
element->QueryFloatAttribute("CropY", ®ion.crop.y);
|
||||
element->QueryFloatAttribute("PivotX", ®ion.pivot.x);
|
||||
element->QueryFloatAttribute("PivotY", ®ion.pivot.y);
|
||||
element->QueryFloatAttribute("XCrop", ®ion.crop.x);
|
||||
element->QueryFloatAttribute("YCrop", ®ion.crop.y);
|
||||
element->QueryFloatAttribute("Width", ®ion.size.x);
|
||||
element->QueryFloatAttribute("Height", ®ion.size.y);
|
||||
region.origin = origin_from_string(element->Attribute("Origin"));
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = glm::ivec2(region.size / 2.0f);
|
||||
else
|
||||
{
|
||||
element->QueryFloatAttribute("XPivot", ®ion.pivot.x);
|
||||
element->QueryFloatAttribute("YPivot", ®ion.pivot.y);
|
||||
}
|
||||
|
||||
if (type == merge::APPEND) id = map::next_id_get(regions);
|
||||
regions[id] = std::move(region);
|
||||
if (std::find(regionOrder.begin(), regionOrder.end(), id) == regionOrder.end()) regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -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
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user