#include "anm2.h" #include #include #include #include #include #include #include #include "map_.h" #include "path_.h" #include "working_directory.h" using namespace anm2ed::types; using namespace anm2ed::util; using namespace tinyxml2; namespace anm2ed::anm2 { Spritesheet* Anm2::spritesheet_get(int id) { return map::find(content.spritesheets, id); } bool Anm2::spritesheet_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id) { Spritesheet spritesheet(directory, path); if (!spritesheet.is_valid()) return false; id = map::next_id_get(content.spritesheets); content.spritesheets[id] = std::move(spritesheet); return true; } bool Anm2::spritesheet_pack(int id, int padding) { const int packingPadding = std::max(0, padding); 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 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 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::max(); int bestLong = std::numeric_limits::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& items, int& packedWidth, int& packedHeight, std::unordered_map& 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::max(); int bestArea = std::numeric_limits::max(); int bestWidth{}; int bestHeight{}; std::unordered_map 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 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::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 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 + packingPadding * 2; int packHeight = size.y + packingPadding * 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 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 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 + packingPadding + x; int destinationY = destinationRect.y + packingPadding + 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 + packingPadding, rect.y + packingPadding}; } return true; } bool Anm2::regions_trim(int spritesheetID, const std::set& 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::max(); int contentMinY = std::numeric_limits::max(); int contentMaxX = std::numeric_limits::min(); int contentMaxY = std::numeric_limits::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::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(region.size.x / 2.0f), static_cast(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 Anm2::spritesheets_unused() { std::set used{}; for (auto& layer : content.layers | std::views::values) if (layer.is_spritesheet_valid()) used.insert(layer.spritesheetID); std::set unused{}; for (auto& id : content.spritesheets | std::views::keys) if (!used.contains(id)) unused.insert(id); return unused; } bool Anm2::spritesheets_merge(const std::set& 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 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> 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 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 Anm2::spritesheet_labels_get() { std::vector labels{}; for (auto& [id, spritesheet] : content.spritesheets) { auto pathString = path::to_utf8(spritesheet.path); labels.emplace_back(std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathString))); } return labels; } std::vector Anm2::spritesheet_ids_get() { std::vector ids{}; for (auto& [id, spritesheet] : content.spritesheets) ids.emplace_back(id); return ids; } std::vector 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 labels{}; labels.emplace_back(localize.get(BASIC_NONE)); for (auto id : spritesheet.regionOrder) labels.emplace_back(spritesheet.regions.at(id).name); return labels; } std::vector 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 ids{}; ids.emplace_back(-1); for (auto id : spritesheet.regionOrder) ids.emplace_back(id); return ids; } std::set Anm2::regions_unused(Spritesheet& spritesheet) { std::set used{}; for (auto& animation : animations.items) { for (auto& layerAnimation : animation.layerAnimations | std::views::values) { for (auto& frame : layerAnimation.frames) if (frame.regionID != -1) used.insert(frame.regionID); } } std::set unused{}; for (auto& id : spritesheet.regions | std::views::keys) if (!used.contains(id)) unused.insert(id); 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) { XMLDocument document{}; if (document.Parse(string.c_str()) == XML_SUCCESS) { int id{}; if (!document.FirstChildElement("Spritesheet")) { if (errorString) *errorString = "No valid spritesheet(s)."; return false; } WorkingDirectory workingDirectory(directory); for (auto element = document.FirstChildElement("Spritesheet"); element; element = element->NextSiblingElement("Spritesheet")) { auto spritesheet = Spritesheet(element, id); if (type == merge::APPEND) id = map::next_id_get(content.spritesheets); content.spritesheets[id] = std::move(spritesheet); } return true; } else if (errorString) *errorString = document.ErrorStr(); return false; } }