From 64d6a1d95abf0de4d3b53368f5f9356f9d3d1c45 Mon Sep 17 00:00:00 2001 From: shweet Date: Thu, 5 Feb 2026 21:34:42 -0500 Subject: [PATCH] Mega Region Update. --- src/anm2/animation.cpp | 19 +- src/anm2/animation.h | 6 +- src/anm2/animations.cpp | 10 +- src/anm2/animations.h | 6 +- src/anm2/anm2.cpp | 14 +- src/anm2/anm2.h | 10 +- src/anm2/anm2_spritesheets.cpp | 560 +++++++++++++++++++++++- src/anm2/anm2_type.h | 31 +- src/anm2/content.cpp | 6 +- src/anm2/content.h | 4 +- src/anm2/frame.cpp | 46 +- src/anm2/frame.h | 6 +- src/anm2/item.cpp | 12 +- src/anm2/item.h | 6 +- src/anm2/spritesheet.cpp | 135 ++++-- src/anm2/spritesheet.h | 16 +- src/canvas.cpp | 16 + src/canvas.h | 2 + src/document.cpp | 18 +- src/document.h | 4 +- src/imgui/documents.cpp | 7 +- src/imgui/imgui_.cpp | 4 +- src/imgui/taskbar.cpp | 38 +- src/imgui/window/animation_preview.cpp | 6 +- src/imgui/window/animations.cpp | 10 +- src/imgui/window/events.cpp | 13 + src/imgui/window/layers.cpp | 15 + src/imgui/window/nulls.cpp | 15 + src/imgui/window/regions.cpp | 180 +++++++- src/imgui/window/sounds.cpp | 33 +- src/imgui/window/spritesheet_editor.cpp | 206 +++++++-- src/imgui/window/spritesheet_editor.h | 1 + src/imgui/window/spritesheets.cpp | 168 ++++++- src/imgui/window/spritesheets.h | 2 + src/imgui/window/timeline.cpp | 26 +- src/imgui/wizard/configure.cpp | 12 + src/imgui/wizard/render_animation.cpp | 3 + src/manager.cpp | 13 +- src/manager.h | 6 +- src/resource/strings.h | 57 ++- src/resource/texture.cpp | 30 ++ src/resource/texture.h | 1 + src/settings.h | 9 +- src/util/origin.h | 11 + workshop/metadata.xml | 2 +- 45 files changed, 1590 insertions(+), 205 deletions(-) create mode 100644 src/util/origin.h diff --git a/src/anm2/animation.cpp b/src/anm2/animation.cpp index dd3ef18..5dd221d 100644 --- a/src/anm2/animation.cpp +++ b/src/anm2/animation.cpp @@ -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}; } -} \ No newline at end of file +} diff --git a/src/anm2/animation.h b/src/anm2/animation.h index 84814d2..9725a90 100644 --- a/src/anm2/animation.h +++ b/src/anm2/animation.h @@ -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); }; -} \ No newline at end of file +} diff --git a/src/anm2/animations.cpp b/src/anm2/animations.cpp index 1eaaed4..10d9208 100644 --- a/src/anm2/animations.cpp +++ b/src/anm2/animations.cpp @@ -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; } -} \ No newline at end of file +} diff --git a/src/anm2/animations.h b/src/anm2/animations.h index a2c4fe5..2db0454 100644 --- a/src/anm2/animations.h +++ b/src/anm2/animations.h @@ -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(); }; -} \ No newline at end of file +} diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp index f0599ab..9c80f54 100644 --- a/src/anm2/anm2.cpp +++ b/src/anm2/anm2.cpp @@ -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); } diff --git a/src/anm2/anm2.h b/src/anm2/anm2.h index 094e664..0bbe11c 100644 --- a/src/anm2/anm2.h +++ b/src/anm2/anm2.h @@ -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&); std::vector spritesheet_labels_get(); std::vector spritesheet_ids_get(); std::set spritesheets_unused(); + bool spritesheets_merge(const std::set&, SpritesheetMergeOrigin, bool, origin::Type); bool spritesheets_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type type, std::string*); std::vector region_labels_get(Spritesheet&); std::vector region_ids_get(Spritesheet&); std::set regions_unused(Spritesheet&); + void scan_and_set_regions(); void layer_add(int&); std::set layers_unused(); diff --git a/src/anm2/anm2_spritesheets.cpp b/src/anm2/anm2_spritesheets.cpp index 43057ed..e4aa2f3 100644 --- a/src/anm2/anm2_spritesheets.cpp +++ b/src/anm2/anm2_spritesheets.cpp @@ -1,6 +1,12 @@ #include "anm2.h" +#include +#include +#include +#include #include +#include +#include #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 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 + 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 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 + 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& 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{}; @@ -36,6 +403,117 @@ namespace anm2ed::anm2 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{}; @@ -57,18 +535,60 @@ namespace anm2ed::anm2 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& 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 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.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) { diff --git a/src/anm2/anm2_type.h b/src/anm2/anm2_type.h index 715735f..7ac3615 100644 --- a/src/anm2/anm2_type.h +++ b/src/anm2/anm2_type.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace anm2ed::anm2 { @@ -86,4 +87,32 @@ namespace anm2ed::anm2 MULTIPLY, DIVIDE }; -} \ No newline at end of file + + 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 = { + {ISAAC, NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES}, {ANM2ED, 0}, {ANM2ED_LIMITED, FRAME_NO_REGION_VALUES}}; +} diff --git a/src/anm2/content.cpp b/src/anm2/content.cpp index 1269761..6d0e25a 100644 --- a/src/anm2/content.cpp +++ b/src/anm2/content.cpp @@ -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) diff --git a/src/anm2/content.h b/src/anm2/content.h index 330c433..551aa44 100644 --- a/src/anm2/content.h +++ b/src/anm2/content.h @@ -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*); }; -} \ No newline at end of file +} diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp index b2e0590..736c882 100644 --- a/src/anm2/frame.cpp +++ b/src/anm2/frame.cpp @@ -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); } diff --git a/src/anm2/frame.h b/src/anm2/frame.h index ea4e49c..c642296 100644 --- a/src/anm2/frame.h +++ b/src/anm2/frame.h @@ -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(); }; diff --git a/src/anm2/item.cpp b/src/anm2/item.cpp index 2425e91..6c1afd7 100644 --- a/src/anm2/item.cpp +++ b/src/anm2/item.cpp @@ -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); } diff --git a/src/anm2/item.h b/src/anm2/item.h index 4d9f97d..5e1e082 100644 --- a/src/anm2/item.h +++ b/src/anm2/item.h @@ -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&); diff --git a/src/anm2/spritesheet.cpp b/src/anm2/spritesheet.cpp index 7d6524a..bd717a8 100644 --- a/src/anm2/spritesheet.cpp +++ b/src/anm2/spritesheet.cpp @@ -1,11 +1,13 @@ #include "spritesheet.h" +#include #include +#include +#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(); } } diff --git a/src/anm2/spritesheet.h b/src/anm2/spritesheet.h index 7e8e2b1..5805bee 100644 --- a/src/anm2/spritesheet.h +++ b/src/anm2/spritesheet.h @@ -3,11 +3,14 @@ #include #include #include +#include #include #include #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 regions{}; + std::vector 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(); }; } diff --git a/src/canvas.cpp b/src/canvas.cpp index 23aa8c0..6ef189b 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -212,6 +212,22 @@ namespace anm2ed glUseProgram(0); } + void Canvas::rect_fill_render(Shader& shader, const mat4& transform, const mat4& model, vec4 color) const + { + glUseProgram(shader.id); + + glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform)); + if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_MODEL); location != -1) + glUniformMatrix4fv(location, 1, GL_FALSE, value_ptr(model)); + glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color)); + + glBindVertexArray(rectVAO); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + glBindVertexArray(0); + glUseProgram(0); + } + void Canvas::zoom_set(float& zoom, vec2& pan, vec2 focus, float step) const { auto zoomFactor = math::percent_to_unit(zoom); diff --git a/src/canvas.h b/src/canvas.h index 85cddcd..79a2cc6 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -56,6 +56,8 @@ namespace anm2ed glm::vec4 = glm::vec4(1.0f)) const; void texture_render(resource::Shader&, GLuint&, glm::mat4 = {1.0f}, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}, float* = (float*)TEXTURE_VERTICES) const; + void rect_fill_render(resource::Shader&, const glm::mat4&, const glm::mat4&, + glm::vec4 = glm::vec4(1.0f)) const; void rect_render(resource::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f), float dashLength = DASH_LENGTH, float dashGap = DASH_GAP, float dashOffset = DASH_OFFSET) const; void zoom_set(float&, glm::vec2&, glm::vec2, float) const; diff --git a/src/document.cpp b/src/document.cpp index 518d01d..9159558 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -17,6 +17,16 @@ using namespace glm; namespace anm2ed { + namespace + { + anm2::Flags serialize_flags_get(anm2::Compatibility compatibility) + { + if (auto it = anm2::COMPATIBILITY_FLAGS.find(compatibility); it != anm2::COMPATIBILITY_FLAGS.end()) + return it->second; + return 0; + } + } + Document::Document(Anm2& anm2, const std::filesystem::path& path) { this->anm2 = std::move(anm2); @@ -95,13 +105,13 @@ namespace anm2ed return *this; } - bool Document::save(const std::filesystem::path& path, std::string* errorString) + bool Document::save(const std::filesystem::path& path, std::string* errorString, anm2::Compatibility compatibility) { this->path = !path.empty() ? path : this->path; auto absolutePath = this->path; auto absolutePathUtf8 = path::to_utf8(absolutePath); - if (anm2.serialize(absolutePath, errorString)) + if (anm2.serialize(absolutePath, errorString, serialize_flags_get(compatibility))) { toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePathUtf8))); logger.info( @@ -138,11 +148,11 @@ namespace anm2ed return restorePath; } - bool Document::autosave(std::string* errorString) + bool Document::autosave(std::string* errorString, anm2::Compatibility compatibility) { auto autosavePath = autosave_path_get(); auto autosavePathUtf8 = path::to_utf8(autosavePath); - if (anm2.serialize(autosavePath, errorString)) + if (anm2.serialize(autosavePath, errorString, serialize_flags_get(compatibility))) { autosaveHash = hash; lastAutosaveTime = 0.0f; diff --git a/src/document.h b/src/document.h index e85d544..dd0f91d 100644 --- a/src/document.h +++ b/src/document.h @@ -73,7 +73,7 @@ namespace anm2ed Document& operator=(const Document&) = delete; Document(Document&&) noexcept; Document& operator=(Document&&) noexcept; - bool save(const std::filesystem::path& = {}, std::string* = nullptr); + bool save(const std::filesystem::path& = {}, std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED); void hash_set(); void clean(); void change(ChangeType); @@ -91,7 +91,7 @@ namespace anm2ed void spritesheet_add(const std::filesystem::path&); void sound_add(const std::filesystem::path&); - bool autosave(std::string* = nullptr); + bool autosave(std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED); std::filesystem::path autosave_path_get(); std::filesystem::path path_from_autosave_get(std::filesystem::path&); diff --git a/src/imgui/documents.cpp b/src/imgui/documents.cpp index 414bdb5..33e22f1 100644 --- a/src/imgui/documents.cpp +++ b/src/imgui/documents.cpp @@ -35,7 +35,8 @@ namespace anm2ed::imgui if (isDirty) { document.lastAutosaveTime += ImGui::GetIO().DeltaTime; - if (document.lastAutosaveTime > time::SECOND_M) manager.autosave(document); + if (document.lastAutosaveTime > time::SECOND_M) + manager.autosave(document, (anm2::Compatibility)settings.fileCompatibility); } } @@ -139,9 +140,10 @@ namespace anm2ed::imgui closePopup.close(); }; + shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(BASIC_YES), widgetSize)) { - manager.save(closeDocumentIndex); + manager.save(closeDocumentIndex, {}, (anm2::Compatibility)settings.fileCompatibility); manager.close(closeDocumentIndex); close(); } @@ -156,6 +158,7 @@ namespace anm2ed::imgui ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) { isQuitting = false; diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index 59bed74..7aa98df 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -499,7 +499,9 @@ namespace anm2ed::imgui bool shortcut(ImGuiKeyChord chord, shortcut::Type type) { - if (ImGui::GetTopMostPopupModal() != nullptr) return false; + if (ImGui::GetTopMostPopupModal() != nullptr && + (type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET)) + return false; int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal : ImGuiInputFlags_RouteFocused; diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index 3c8dd26..1955c47 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -7,8 +7,11 @@ #include +#include "document.h" +#include "log.h" #include "path_.h" #include "strings.h" +#include "toast.h" #include "types.h" using namespace anm2ed::resource; @@ -24,6 +27,18 @@ namespace anm2ed::imgui auto animation = document ? document->animation_get() : nullptr; auto item = document ? document->item_get() : nullptr; auto frames = document ? &document->frames : nullptr; + bool hasRegions = false; + if (document) + { + for (auto& spritesheet : document->anm2.content.spritesheets | std::views::values) + { + if (!spritesheet.regions.empty()) + { + hasRegions = true; + break; + } + } + } if (ImGui::BeginMainMenuBar()) { @@ -59,7 +74,7 @@ namespace anm2ed::imgui if (settings.fileIsWarnOverwrite) overwritePopup.open(); else - manager.save(document->path); + manager.save(document->path, (anm2::Compatibility)settings.fileCompatibility); } if (ImGui::MenuItem(localize.get(LABEL_SAVE_AS), settings.shortcutSaveAs.c_str(), false, document)) @@ -85,7 +100,7 @@ namespace anm2ed::imgui if (dialog.is_selected(Dialog::ANM2_SAVE)) { - manager.save(dialog.path); + manager.save(dialog.path, (anm2::Compatibility)settings.fileCompatibility); dialog.reset(); } @@ -94,16 +109,29 @@ namespace anm2ed::imgui if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID), nullptr, false, item && document->reference.itemType == anm2::LAYER)) generatePopup.open(); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_WIZARD_GENERATE_ANIMATION_FROM_GRID)); if (ImGui::MenuItem(localize.get(LABEL_CHANGE_ALL_FRAME_PROPERTIES), nullptr, false, frames && !frames->selection.empty() && document->reference.itemType != anm2::TRIGGER)) changePopup.open(); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_WIZARD_CHANGE_ALL_FRAME_PROPERTIES)); + + if (ImGui::MenuItem(localize.get(LABEL_SCAN_AND_SET_REGIONS), nullptr, false, document && hasRegions)) + { + DOCUMENT_EDIT_PTR(document, localize.get(EDIT_SCAN_AND_SET_REGIONS), Document::FRAMES, + document->anm2.scan_and_set_regions()); + toasts.push(localize.get(TOAST_SCAN_AND_SET_REGIONS)); + logger.info(localize.get(TOAST_SCAN_AND_SET_REGIONS, anm2ed::ENGLISH)); + } + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_WIZARD_SCAN_AND_SET_REGIONS)); ImGui::Separator(); if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_RENDER_ANIMATION), nullptr, false, animation && manager.isAbleToRecord)) renderPopup.open(); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_WIZARD_RENDER_ANIMATION)); + ImGui::EndMenu(); } @@ -212,7 +240,7 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(BASIC_YES), widgetSize)) { - manager.save(); + manager.save({}, (anm2::Compatibility)settings.fileCompatibility); overwritePopup.close(); } @@ -232,9 +260,9 @@ namespace anm2ed::imgui if (settings.fileIsWarnOverwrite) overwritePopup.open(); else - manager.save(); + manager.save({}, (anm2::Compatibility)settings.fileCompatibility); } if (shortcut(manager.chords[SHORTCUT_SAVE_AS], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_SAVE); if (shortcut(manager.chords[SHORTCUT_EXIT], shortcut::GLOBAL)) isQuitting = true; } -} \ No newline at end of file +} diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index 1287235..4e98ff2 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -565,6 +565,7 @@ namespace anm2ed::imgui auto crop = frame.crop; auto size = frame.size; auto pivot = frame.pivot; + if (frame.regionID != -1) { auto regionIt = spritesheet->regions.find(frame.regionID); @@ -576,8 +577,8 @@ namespace anm2ed::imgui } } - auto layerModel = math::quad_model_get(size, frame.position, pivot, - math::percent_to_unit(frame.scale), frame.rotation); + auto layerModel = + math::quad_model_get(size, frame.position, pivot, math::percent_to_unit(frame.scale), frame.rotation); auto layerTransform = sampleTransform * layerModel; auto uvMin = crop / texSize; @@ -931,6 +932,7 @@ namespace anm2ed::imgui ImGui::TextUnformatted(localize.get(TEXT_RECORDING_PROGRESS)); + shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), ImVec2(ImGui::GetContentRegionAvail().x, 0))) { renderFrames.clear(); diff --git a/src/imgui/window/animations.cpp b/src/imgui/window/animations.cpp index df67422..9d55ffc 100644 --- a/src/imgui/window/animations.cpp +++ b/src/imgui/window/animations.cpp @@ -187,7 +187,15 @@ namespace anm2ed::imgui std::set indices{}; std::string errorString{}; if (anm2.animations_deserialize(clipboardText, start, indices, &errorString)) - selection = indices; + { + if (!indices.empty()) + { + auto index = *indices.rbegin(); + selection = {index}; + reference = {index}; + newAnimationSelectedIndex = index; + } + } else { toasts.push( diff --git a/src/imgui/window/events.cpp b/src/imgui/window/events.cpp index fe49599..d7ed3f8 100644 --- a/src/imgui/window/events.cpp +++ b/src/imgui/window/events.cpp @@ -78,10 +78,23 @@ namespace anm2ed::imgui auto behavior = [&]() { + auto maxEventIdBefore = anm2.content.events.empty() ? -1 : anm2.content.events.rbegin()->first; std::string errorString{}; document.snapshot(localize.get(EDIT_PASTE_EVENTS)); if (anm2.events_deserialize(clipboard.get(), merge::APPEND, &errorString)) + { + if (!anm2.content.events.empty()) + { + auto maxEventIdAfter = anm2.content.events.rbegin()->first; + if (maxEventIdAfter > maxEventIdBefore) + { + newEventId = maxEventIdAfter; + selection = {maxEventIdAfter}; + reference = maxEventIdAfter; + } + } document.change(Document::EVENTS); + } else { toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_EVENTS_FAILED), std::make_format_args(errorString))); diff --git a/src/imgui/window/layers.cpp b/src/imgui/window/layers.cpp index 5fda040..0c2a5de 100644 --- a/src/imgui/window/layers.cpp +++ b/src/imgui/window/layers.cpp @@ -53,10 +53,23 @@ namespace anm2ed::imgui auto behavior = [&]() { + auto maxLayerIdBefore = anm2.content.layers.empty() ? -1 : anm2.content.layers.rbegin()->first; std::string errorString{}; document.snapshot(localize.get(EDIT_PASTE_LAYERS)); if (anm2.layers_deserialize(clipboard.get(), merge::APPEND, &errorString)) + { + if (!anm2.content.layers.empty()) + { + auto maxLayerIdAfter = anm2.content.layers.rbegin()->first; + if (maxLayerIdAfter > maxLayerIdBefore) + { + newLayerId = maxLayerIdAfter; + selection = {maxLayerIdAfter}; + reference = maxLayerIdAfter; + } + } document.change(Document::NULLS); + } else { toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_LAYERS_FAILED), std::make_format_args(errorString))); @@ -180,6 +193,7 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); + shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) { if (reference == -1) @@ -210,6 +224,7 @@ namespace anm2ed::imgui ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.layer_properties_close(); manager.layer_properties_end(); diff --git a/src/imgui/window/nulls.cpp b/src/imgui/window/nulls.cpp index fd2ab4f..4dad0ca 100644 --- a/src/imgui/window/nulls.cpp +++ b/src/imgui/window/nulls.cpp @@ -52,10 +52,23 @@ namespace anm2ed::imgui auto behavior = [&]() { + auto maxNullIdBefore = anm2.content.nulls.empty() ? -1 : anm2.content.nulls.rbegin()->first; std::string errorString{}; document.snapshot(localize.get(EDIT_PASTE_NULLS)); if (anm2.nulls_deserialize(clipboard.get(), merge::APPEND, &errorString)) + { + if (!anm2.content.nulls.empty()) + { + auto maxNullIdAfter = anm2.content.nulls.rbegin()->first; + if (maxNullIdAfter > maxNullIdBefore) + { + newNullId = maxNullIdAfter; + selection = {maxNullIdAfter}; + reference = maxNullIdAfter; + } + } document.change(Document::NULLS); + } else { toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_NULLS_FAILED), std::make_format_args(errorString))); @@ -177,6 +190,7 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); + shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) { if (reference == -1) @@ -207,6 +221,7 @@ namespace anm2ed::imgui ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.null_properties_close(); ImGui::EndPopup(); diff --git a/src/imgui/window/regions.cpp b/src/imgui/window/regions.cpp index 1ab9063..f8803d4 100644 --- a/src/imgui/window/regions.cpp +++ b/src/imgui/window/regions.cpp @@ -1,5 +1,6 @@ #include "regions.h" +#include #include #include @@ -12,6 +13,7 @@ #include "path_.h" #include "strings.h" #include "toast.h" +#include "vector_.h" #include "../../util/map_.h" @@ -51,12 +53,32 @@ namespace anm2ed::imgui if (frame.regionID == id) frame.regionID = -1; spritesheet->regions.erase(id); + auto& order = spritesheet->regionOrder; + order.erase(std::remove(order.begin(), order.end(), id), order.end()); } }; DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_REGIONS), Document::SPRITESHEETS, behavior()); }; + auto trim = [&]() + { + if (!spritesheet || selection.empty()) return; + + auto behavior = [&]() + { + if (anm2.regions_trim(spritesheetReference, selection)) + { + if (reference != -1 && !selection.contains(reference)) reference = *selection.begin(); + document.reference = {document.reference.animationIndex}; + frame.reference = -1; + frame.selection.clear(); + } + }; + + DOCUMENT_EDIT(document, localize.get(EDIT_TRIM_REGIONS), Document::SPRITESHEETS, behavior()); + }; + auto copy = [&]() { if (!spritesheet || selection.empty()) return; @@ -73,10 +95,23 @@ namespace anm2ed::imgui auto behavior = [&]() { + auto maxRegionIdBefore = spritesheet->regions.empty() ? -1 : spritesheet->regions.rbegin()->first; std::string errorString{}; document.snapshot(localize.get(EDIT_PASTE_REGIONS)); if (spritesheet->regions_deserialize(clipboard.get(), merge::APPEND, &errorString)) + { + if (!spritesheet->regions.empty()) + { + auto maxRegionIdAfter = spritesheet->regions.rbegin()->first; + if (maxRegionIdAfter > maxRegionIdBefore) + { + newRegionId = maxRegionIdAfter; + selection = {maxRegionIdAfter}; + reference = maxRegionIdAfter; + } + } document.change(Document::SPRITESHEETS); + } else { toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED), std::make_format_args(errorString))); @@ -127,6 +162,8 @@ namespace anm2ed::imgui properties_open(*selection.begin()); if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open(); if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); + if (ImGui::MenuItem(localize.get(BASIC_TRIM), nullptr, false, !selection.empty())) trim(); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIM_REGIONS)); ImGui::Separator(); @@ -153,17 +190,51 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (ImGui::BeginChild("##Regions Child", childSize, ImGuiChildFlags_Borders)) { - auto regionChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 4); + auto regionChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); - selection.start(spritesheet->regions.size()); + 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(); + } + + selection.set_index_map(&spritesheet->regionOrder); + selection.start(spritesheet->regionOrder.size()); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + for (auto& id : spritesheet->regionOrder) + selection.insert(id); + } + if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear(); bool isValid = spritesheet->is_valid(); auto& texture = isValid ? spritesheet->texture : resources.icons[icon::NONE]; auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - for (auto& [id, region] : spritesheet->regions) + for (int i = 0; i < (int)spritesheet->regionOrder.size(); i++) { + int id = spritesheet->regionOrder[i]; + auto regionIt = spritesheet->regions.find(id); + if (regionIt == spritesheet->regions.end()) continue; + auto& region = regionIt->second; + auto isNewRegion = newRegionId == id; auto nameCStr = region.name.c_str(); auto isSelected = selection.contains(id); auto isReferenced = id == reference; @@ -174,7 +245,7 @@ namespace anm2ed::imgui { auto cursorPos = ImGui::GetCursorPos(); - ImGui::SetNextItemSelectionUserData(id); + ImGui::SetNextItemSelectionUserData(i); ImGui::SetNextItemStorageID(id); if (ImGui::Selectable("##Region Selectable", isSelected, 0, regionChildSize)) { @@ -194,8 +265,13 @@ namespace anm2ed::imgui auto scale = glm::min(maxPreviewSize.x / previewSize.x, maxPreviewSize.y / previewSize.y); previewSize *= scale; } - auto uvMin = region.crop / vec2(texture.size); - auto uvMax = (region.crop + region.size) / vec2(texture.size); + vec2 uvMin{}; + vec2 uvMax{1.0f, 1.0f}; + if (isValid) + { + uvMin = region.crop / vec2(texture.size); + uvMax = (region.crop + region.size) / vec2(texture.size); + } auto textWidth = ImGui::CalcTextSize(nameCStr).x; auto tooltipPadding = style.WindowPadding.x * 4.0f; @@ -233,15 +309,70 @@ namespace anm2ed::imgui ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y)) .c_str()); - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y)) - .c_str()); + if (region.origin == anm2::Spritesheet::Region::CUSTOM) + { + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y)) + .c_str()); + } + else + { + StringType originString = LABEL_REGION_ORIGIN_CENTER; + if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT; + auto originLabel = localize.get(originString); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str()); + } } ImGui::EndChild(); ImGui::EndTooltip(); } ImGui::PopStyleVar(2); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + if (ImGui::BeginDragDropSource()) + { + static std::vector dragDropSelection{}; + dragDropSelection.assign(selection.begin(), selection.end()); + ImGui::SetDragDropPayload("Region Drag Drop", dragDropSelection.data(), + dragDropSelection.size() * sizeof(int)); + + for (auto regionId : dragDropSelection) + { + auto dragIt = spritesheet->regions.find(regionId); + if (dragIt == spritesheet->regions.end()) continue; + ImGui::TextUnformatted(dragIt->second.name.c_str()); + } + ImGui::EndDragDropSource(); + } + + ImGui::PopStyleVar(2); + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload("Region Drag Drop")) + { + auto payloadIds = (int*)(payload->Data); + int payloadCount = (int)(payload->DataSize / sizeof(int)); + std::vector indices{}; + indices.reserve(payloadCount); + for (int payloadIndex = 0; payloadIndex < payloadCount; payloadIndex++) + { + int payloadId = payloadIds[payloadIndex]; + int index = vector::find_index(spritesheet->regionOrder, payloadId); + if (index != -1) indices.push_back(index); + } + if (!indices.empty()) + { + std::sort(indices.begin(), indices.end()); + DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_REGIONS), Document::SPRITESHEETS, + vector::move_indices(spritesheet->regionOrder, indices, i)); + } + } + ImGui::EndDragDropTarget(); + } + auto imageSize = to_imvec2(vec2(regionChildSize.y)); auto aspectRatio = region.size.y != 0.0f ? (float)region.size.x / region.size.y : 1.0f; @@ -257,12 +388,18 @@ namespace anm2ed::imgui regionChildSize.y - regionChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, nameCStr)).c_str()); + ImGui::TextUnformatted(nameCStr); if (isReferenced) ImGui::PopFont(); } ImGui::EndChild(); + + if (isNewRegion) + { + ImGui::SetScrollHereY(0.5f); + newRegionId = -1; + } + ImGui::PopID(); } @@ -298,28 +435,43 @@ namespace anm2ed::imgui if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto childSize = child_size_get(4); + auto childSize = child_size_get(5); auto& region = reference == -1 ? editRegion : spritesheet->regions.at(reference); if (propertiesPopup.isJustOpened) editRegion = anm2::Spritesheet::Region{}; if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders)) { + const char* originOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT), + localize.get(LABEL_REGION_ORIGIN_CENTER), + localize.get(LABEL_REGION_ORIGIN_CUSTOM)}; + if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); input_text_string(localize.get(BASIC_NAME), ®ion.name); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); - ImGui::DragFloat2(localize.get(BASIC_CROP), value_ptr(region.crop), DRAG_SPEED, 0.0f, 0.0f, math::vec2_format_get(region.crop)); ImGui::DragFloat2(localize.get(BASIC_SIZE), value_ptr(region.size), DRAG_SPEED, 0.0f, 0.0f, math::vec2_format_get(region.size)); + ImGui::BeginDisabled(region.origin != anm2::Spritesheet::Region::CUSTOM); ImGui::DragFloat2(localize.get(BASIC_PIVOT), value_ptr(region.pivot), DRAG_SPEED, 0.0f, 0.0f, math::vec2_format_get(region.pivot)); + ImGui::EndDisabled(); + + if (ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), (int*)®ion.origin, originOptions, + IM_ARRAYSIZE(originOptions))) + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION_PROPERTIES_ORIGIN)); + + if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) + region.pivot = {}; + else if (region.origin == anm2::Spritesheet::Region::ORIGIN_CENTER) + region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)}; } ImGui::EndChild(); auto widgetSize = widget_size_with_row_get(2); + shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize)) { if (reference == -1) @@ -328,6 +480,7 @@ namespace anm2ed::imgui { auto id = map::next_id_get(spritesheet->regions); spritesheet->regions[id] = region; + spritesheet->regionOrder.push_back(id); selection = {id}; newRegionId = id; }; @@ -353,6 +506,7 @@ namespace anm2ed::imgui ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) propertiesPopup.close(); ImGui::EndPopup(); diff --git a/src/imgui/window/sounds.cpp b/src/imgui/window/sounds.cpp index b016ccb..343a815 100644 --- a/src/imgui/window/sounds.cpp +++ b/src/imgui/window/sounds.cpp @@ -130,10 +130,23 @@ namespace anm2ed::imgui auto behavior = [&]() { + auto maxSoundIdBefore = anm2.content.sounds.empty() ? -1 : anm2.content.sounds.rbegin()->first; std::string errorString{}; document.snapshot(localize.get(TOAST_SOUNDS_PASTE)); if (anm2.sounds_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString)) + { + if (!anm2.content.sounds.empty()) + { + auto maxSoundIdAfter = anm2.content.sounds.rbegin()->first; + if (maxSoundIdAfter > maxSoundIdBefore) + { + newSoundId = maxSoundIdAfter; + selection = {maxSoundIdAfter}; + reference = maxSoundIdAfter; + } + } document.change(Document::SOUNDS); + } else { toasts.push(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR), std::make_format_args(errorString))); @@ -200,9 +213,17 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); selection.start(anm2.content.sounds.size()); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + for (auto& id : anm2.content.sounds | std::views::keys) + selection.insert(id); + } + if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear(); for (auto& [id, sound] : anm2.content.sounds) { + auto isNewSound = newSoundId == id; ImGui::PushID(id); if (ImGui::BeginChild("##Sound Child", soundChildSize, ImGuiChildFlags_Borders)) @@ -222,11 +243,6 @@ namespace anm2ed::imgui if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) play(sound); } if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) open_directory(sound); - if (newSoundId == id) - { - ImGui::SetScrollHereY(0.5f); - newSoundId = -1; - } auto textWidth = ImGui::CalcTextSize(pathString.c_str()).x; auto tooltipPadding = style.WindowPadding.x * 4.0f; @@ -267,6 +283,13 @@ namespace anm2ed::imgui } ImGui::EndChild(); + + if (isNewSound) + { + ImGui::SetScrollHereY(0.5f); + newSoundId = -1; + } + ImGui::PopID(); } diff --git a/src/imgui/window/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp index 034216b..01423da 100644 --- a/src/imgui/window/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -42,6 +42,7 @@ namespace anm2ed::imgui auto& tool = settings.tool; auto& shaderGrid = resources.shaders[shader::GRID]; auto& shaderTexture = resources.shaders[shader::TEXTURE]; + auto& shaderLine = resources.shaders[shader::LINE]; auto& dashedShader = resources.shaders[shader::DASHED]; auto& frames = document.frames.selection; auto& regionReference = document.region.reference; @@ -178,11 +179,18 @@ namespace anm2ed::imgui ImGui::EndChild(); auto drawList = ImGui::GetCurrentWindow()->DrawList; + size_set(to_vec2(ImGui::GetContentRegionAvail())); + auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto min = ImGui::GetCursorScreenPos(); auto max = to_imvec2(to_vec2(min) + size); - size_set(to_vec2(ImGui::GetContentRegionAvail())); + auto mouseScreenPos = ImGui::GetIO().MousePos; + bool isMouseOverCanvas = mouseScreenPos.x >= min.x && mouseScreenPos.x <= max.x && mouseScreenPos.y >= min.y && + mouseScreenPos.y <= max.y; + auto hoverMousePos = vec2(); + if (isMouseOverCanvas) + hoverMousePos = position_translate(zoom, pan, to_ivec2(mouseScreenPos) - to_ivec2(cursorScreenPos)); bind(); viewport_set(); @@ -207,32 +215,73 @@ namespace anm2ed::imgui rect_render(dashedShader, spritesheetTransform, spritesheetModel, color::WHITE, BORDER_DASH_LENGTH, BORDER_DASH_GAP, BORDER_DASH_OFFSET); - if (frame && reference.itemID > -1 && - anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet) + if (hoveredRegionId != -1) { - auto cropModel = math::quad_model_get(frame->size, frame->crop); - auto cropTransform = transform * cropModel; - rect_render(dashedShader, cropTransform, cropModel, color::RED); - - auto pivotTransform = - transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); - texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR); - } - else if (regionReference != -1) - { - auto regionIt = spritesheet->regions.find(regionReference); + auto regionIt = spritesheet->regions.find(hoveredRegionId); if (regionIt != spritesheet->regions.end()) { auto& region = regionIt->second; auto cropModel = math::quad_model_get(region.size, region.crop); auto cropTransform = transform * cropModel; - rect_render(dashedShader, cropTransform, cropModel, color::RED); + rect_fill_render(shaderLine, cropTransform, cropModel, vec4(1.0f, 1.0f, 1.0f, 0.5f)); + } + } + + int highlightedRegionId = -1; + if (frame && reference.itemID > -1 && + anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet && frame->regionID != -1 && + spritesheet->regions.contains(frame->regionID)) + { + highlightedRegionId = frame->regionID; + } + else if (regionReference != -1 && spritesheet->regions.contains(regionReference)) + { + highlightedRegionId = regionReference; + } + + auto draw_region_rect = [&](anm2::Spritesheet::Region& region, vec4 regionColor) + { + auto cropModel = math::quad_model_get(region.size, region.crop); + auto cropTransform = transform * cropModel; + rect_render(dashedShader, cropTransform, cropModel, regionColor, BORDER_DASH_LENGTH, BORDER_DASH_GAP, + BORDER_DASH_OFFSET); + }; + + for (auto& [id, region] : spritesheet->regions) + { + if (id == highlightedRegionId) continue; + draw_region_rect(region, color::WHITE); + + auto pivotTransform = + transform * math::quad_model_get(PIVOT_SIZE, region.crop + region.pivot, PIVOT_SIZE * 0.5f); + texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color::WHITE); + } + + if (highlightedRegionId != -1) + { + auto regionIt = spritesheet->regions.find(highlightedRegionId); + if (regionIt != spritesheet->regions.end()) + { + auto& region = regionIt->second; + draw_region_rect(region, color::RED); auto pivotTransform = transform * math::quad_model_get(PIVOT_SIZE, region.crop + region.pivot, PIVOT_SIZE * 0.5f); texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR); } } + + bool isFrameOnSpritesheet = + frame && reference.itemID > -1 && anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet; + if (isFrameOnSpritesheet && frame->regionID == -1) + { + auto frameModel = math::quad_model_get(frame->size, frame->crop); + auto frameTransform = transform * frameModel; + rect_render(shaderLine, frameTransform, frameModel, color::RED); + + auto pivotTransform = transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); + texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR); + } } unbind(); @@ -320,6 +369,8 @@ namespace anm2ed::imgui auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST) { item->frames_change(frameChange, reference.itemType, changeType, frames); }; + auto clamp_vec2_to_int = [](const vec2& value) + { return vec2(ivec2(value)); }; auto region_set_all = [&](const vec2& crop, const vec2& size) { if (!spritesheet) return; @@ -327,8 +378,8 @@ namespace anm2ed::imgui { auto it = spritesheet->regions.find(id); if (it == spritesheet->regions.end()) continue; - it->second.crop = crop; - it->second.size = size; + it->second.crop = clamp_vec2_to_int(crop); + it->second.size = clamp_vec2_to_int(size); } }; auto region_offset_all = [&](const vec2& delta) @@ -338,7 +389,8 @@ namespace anm2ed::imgui { auto it = spritesheet->regions.find(id); if (it == spritesheet->regions.end()) continue; - it->second.crop += delta; + it->second.crop = clamp_vec2_to_int(it->second.crop + delta); + it->second.size = clamp_vec2_to_int(it->second.size); } }; @@ -348,18 +400,32 @@ namespace anm2ed::imgui if (tool == tool::DRAW && isMouseRightDown) useTool = tool::ERASE; if (tool == tool::ERASE && isMouseRightDown) useTool = tool::DRAW; + hoveredRegionId = -1; + + if (useTool == tool::PAN && spritesheet && spritesheet->texture.is_valid() && isMouseOverCanvas) + { + for (auto& [id, region] : spritesheet->regions) + { + auto minPoint = glm::min(region.crop, region.crop + region.size); + auto maxPoint = glm::max(region.crop, region.crop + region.size); + if (hoverMousePos.x >= minPoint.x && hoverMousePos.x <= maxPoint.x && hoverMousePos.y >= minPoint.y && + hoverMousePos.y <= maxPoint.y) + { + hoveredRegionId = id; + break; + } + } + } + auto& toolInfo = tool::INFO[useTool]; auto& areaType = toolInfo.areaType; bool isAreaAllowed = areaType == tool::ALL || areaType == tool::SPRITESHEET_EDITOR; bool isFrameRequired = !(useTool == tool::PAN || useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER); - bool isRegionInUse = - frame && frame->regionID != -1 && (useTool == tool::CROP || useTool == tool::MOVE); - bool isFrameAvailable = - !isFrameRequired || - (frame && !isRegionInUse) || - (useTool == tool::CROP && !frame && !regionSelection.empty()) || - (useTool == tool::MOVE && !frame && regionReference != -1); + bool isRegionInUse = frame && frame->regionID != -1 && (useTool == tool::CROP || useTool == tool::MOVE); + bool isFrameAvailable = !isFrameRequired || (frame && !isRegionInUse) || + (useTool == tool::CROP && !frame && !regionSelection.empty()) || + (useTool == tool::MOVE && !frame && regionReference != -1); bool isSpritesheetRequired = useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER; bool isSpritesheetAvailable = !isSpritesheetRequired || (spritesheet && spritesheet->texture.is_valid()); auto cursor = (isAreaAllowed && isFrameAvailable && isSpritesheetAvailable) ? toolInfo.cursor @@ -370,6 +436,29 @@ namespace anm2ed::imgui switch (useTool) { case tool::PAN: + if (isMouseLeftClicked && hoveredRegionId != -1) + { + regionReference = hoveredRegionId; + regionSelection = {hoveredRegionId}; + if (frame && reference.itemID > -1 && + anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet) + { + DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_REGION), Document::FRAMES, + { + frame->regionID = hoveredRegionId; + if (spritesheet) + { + auto regionIt = spritesheet->regions.find(hoveredRegionId); + if (regionIt != spritesheet->regions.end()) + { + frame->crop = regionIt->second.crop; + frame->size = regionIt->second.size; + frame->pivot = regionIt->second.pivot; + } + } + }); + } + } if (isMouseDown || isMouseMiddleDown) pan += mouseDelta; break; case tool::MOVE: @@ -382,8 +471,9 @@ namespace anm2ed::imgui auto& region = regionIt->second; if (isBegin) document.snapshot(localize.get(EDIT_REGION_MOVE)); - if (isMouseDown) - region.pivot = ivec2(mousePos) - ivec2(region.crop); + bool isPivotEdited = isMouseDown || isLeftPressed || isRightPressed || isUpPressed || isDownPressed; + if (isPivotEdited) region.origin = anm2::Spritesheet::Region::CUSTOM; + if (isMouseDown) region.pivot = ivec2(mousePos) - ivec2(region.crop); if (isLeftPressed) region.pivot.x -= step; if (isRightPressed) region.pivot.x += step; if (isUpPressed) region.pivot.y -= step; @@ -466,14 +556,14 @@ namespace anm2ed::imgui auto& region = it->second; auto minPoint = glm::min(region.crop, region.crop + region.size); auto maxPoint = glm::max(region.crop, region.crop + region.size); - region.crop = minPoint; - region.size = maxPoint - minPoint; + region.crop = clamp_vec2_to_int(minPoint); + region.size = clamp_vec2_to_int(maxPoint - minPoint); if (isGridSnap) { auto [snapMin, snapMax] = snap_rect(region.crop, region.crop + region.size); - region.crop = snapMin; - region.size = snapMax - snapMin; + region.crop = clamp_vec2_to_int(snapMin); + region.size = clamp_vec2_to_int(snapMax - snapMin); } } } @@ -483,14 +573,12 @@ namespace anm2ed::imgui auto it = spritesheet->regions.find(*regionSelection.begin()); if (it != spritesheet->regions.end()) { - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_CROP), - std::make_format_args(it->second.crop.x, it->second.crop.y)) - .c_str()); - ImGui::TextUnformatted( - std::vformat(localize.get(FORMAT_SIZE), - std::make_format_args(it->second.size.x, it->second.size.y)) - .c_str()); + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_CROP), + std::make_format_args(it->second.crop.x, it->second.crop.y)) + .c_str()); + ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_SIZE), + std::make_format_args(it->second.size.x, it->second.size.y)) + .c_str()); } ImGui::EndTooltip(); } @@ -593,6 +681,42 @@ namespace anm2ed::imgui break; } + if (tool == tool::PAN && hoveredRegionId != -1 && spritesheet) + { + auto regionIt = spritesheet->regions.find(hoveredRegionId); + if (regionIt != spritesheet->regions.end()) + { + if (ImGui::BeginTooltip()) + { + auto& region = regionIt->second; + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); + ImGui::TextUnformatted(region.name.c_str()); + ImGui::PopFont(); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_ID), std::make_format_args(hoveredRegionId)).c_str()); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region.crop.x, region.crop.y)).c_str()); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y)).c_str()); + if (region.origin == anm2::Spritesheet::Region::CUSTOM) + { + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y)) + .c_str()); + } + else + { + StringType originString = LABEL_REGION_ORIGIN_CENTER; + if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT; + auto originLabel = localize.get(originString); + ImGui::TextUnformatted( + std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str()); + } + ImGui::EndTooltip(); + } + } + } + if ((isMouseDown || isKeyDown) && useTool != tool::PAN) { if (!isAreaAllowed && areaType == tool::ANIMATION_PREVIEW) @@ -617,8 +741,7 @@ namespace anm2ed::imgui { if (isRegionInUse) ImGui::TextUnformatted(localize.get(TEXT_REGION_IN_USE)); - else - if (useTool == tool::CROP) + else if (useTool == tool::CROP) ImGui::TextUnformatted(localize.get(TEXT_SELECT_FRAME_OR_REGION)); else ImGui::TextUnformatted(localize.get(TEXT_SELECT_FRAME)); @@ -681,4 +804,5 @@ namespace anm2ed::imgui settings.editorStartZoom = zoom; ImGui::End(); } + } diff --git a/src/imgui/window/spritesheet_editor.h b/src/imgui/window/spritesheet_editor.h index 0829acf..e1a1da5 100644 --- a/src/imgui/window/spritesheet_editor.h +++ b/src/imgui/window/spritesheet_editor.h @@ -17,6 +17,7 @@ namespace anm2ed::imgui float checkerSyncZoom{}; bool isCheckerPanInitialized{}; bool hasPendingZoomPanAdjust{}; + int hoveredRegionId{-1}; public: SpritesheetEditor(); diff --git a/src/imgui/window/spritesheets.cpp b/src/imgui/window/spritesheets.cpp index 559458d..e1206d9 100644 --- a/src/imgui/window/spritesheets.cpp +++ b/src/imgui/window/spritesheets.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "document.h" #include "log.h" @@ -27,9 +28,24 @@ namespace anm2ed::imgui auto& reference = document.spritesheet.reference; auto& region = document.region; auto style = ImGui::GetStyle(); + std::function pack{}; auto add_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_OPEN); }; auto replace_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_REPLACE); }; + auto merge_open = [&]() + { + if (selection.size() <= 1) return; + mergeSelection = selection; + mergePopup.open(); + }; + auto pack_open = [&]() + { + if (selection.size() != 1) return; + auto id = *selection.begin(); + if (!anm2.content.spritesheets.contains(id)) return; + if (anm2.content.spritesheets.at(id).regions.empty()) return; + if (pack) pack(); + }; auto add = [&](const std::filesystem::path& path) { @@ -87,7 +103,7 @@ namespace anm2ed::imgui { auto& id = *selection.begin(); anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - spritesheet = anm2::Spritesheet(document.directory_get(), path); + spritesheet.reload(document.directory_get(), path); auto pathString = path::to_utf8(spritesheet.path); toasts.push(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET), std::make_format_args(id, pathString))); logger.info(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET, anm2ed::ENGLISH), @@ -120,6 +136,55 @@ namespace anm2ed::imgui } }; + auto merge = [&]() + { + if (mergeSelection.size() <= 1) return; + + auto behavior = [&]() + { + auto baseID = *mergeSelection.begin(); + if (anm2.spritesheets_merge(mergeSelection, (anm2::SpritesheetMergeOrigin)settings.mergeSpritesheetsOrigin, + settings.mergeSpritesheetsIsMakeRegions, + (origin::Type)settings.mergeSpritesheetsRegionOrigin)) + { + selection = {baseID}; + reference = baseID; + region.reference = -1; + region.selection.clear(); + toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS)); + logger.info(localize.get(TOAST_MERGE_SPRITESHEETS, anm2ed::ENGLISH)); + } + else + { + toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED)); + logger.error(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED, anm2ed::ENGLISH)); + } + }; + + DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_SPRITESHEETS), Document::ALL, behavior()); + }; + pack = [&]() + { + if (selection.size() != 1) return; + + auto behavior = [&]() + { + auto id = *selection.begin(); + if (anm2.spritesheet_pack(id)) + { + toasts.push(localize.get(TOAST_PACK_SPRITESHEET)); + logger.info(localize.get(TOAST_PACK_SPRITESHEET, anm2ed::ENGLISH)); + } + else + { + toasts.push(localize.get(TOAST_PACK_SPRITESHEET_FAILED)); + logger.error(localize.get(TOAST_PACK_SPRITESHEET_FAILED, anm2ed::ENGLISH)); + } + }; + + DOCUMENT_EDIT(document, localize.get(EDIT_PACK_SPRITESHEET), Document::SPRITESHEETS, behavior()); + }; + auto open_directory = [&](anm2::Spritesheet& spritesheet) { if (spritesheet.path.empty()) return; @@ -148,10 +213,25 @@ namespace anm2ed::imgui auto behavior = [&]() { + auto maxSpritesheetIdBefore = anm2.content.spritesheets.empty() ? -1 : anm2.content.spritesheets.rbegin()->first; std::string errorString{}; document.snapshot(localize.get(EDIT_PASTE_SPRITESHEETS)); if (anm2.spritesheets_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString)) + { + if (!anm2.content.spritesheets.empty()) + { + auto maxSpritesheetIdAfter = anm2.content.spritesheets.rbegin()->first; + if (maxSpritesheetIdAfter > maxSpritesheetIdBefore) + { + newSpritesheetId = maxSpritesheetIdAfter; + selection = {maxSpritesheetIdAfter}; + reference = maxSpritesheetIdAfter; + region.reference = -1; + region.selection.clear(); + } + } document.change(Document::SPRITESHEETS); + } else { toasts.push( @@ -191,8 +271,17 @@ namespace anm2ed::imgui if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open(); if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused(); + bool isPackable = + selection.size() == 1 && anm2.content.spritesheets.contains(*selection.begin()) && + !anm2.content.spritesheets.at(*selection.begin()).regions.empty(); + if (ImGui::MenuItem(localize.get(BASIC_RELOAD), nullptr, false, !selection.empty())) reload(); if (ImGui::MenuItem(localize.get(BASIC_REPLACE), nullptr, false, selection.size() == 1)) replace_open(); + if (ImGui::MenuItem(localize.get(BASIC_MERGE), settings.shortcutMerge.c_str(), false, selection.size() > 1)) + merge_open(); + if (ImGui::MenuItem(localize.get(BASIC_PACK), nullptr, false, isPackable)) pack_open(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PACK_SPRITESHEET)); if (ImGui::MenuItem(localize.get(BASIC_SAVE), nullptr, false, !selection.empty())) save(); ImGui::Separator(); @@ -217,9 +306,17 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); selection.start(anm2.content.spritesheets.size()); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) + { + selection.clear(); + for (auto& id : anm2.content.spritesheets | std::views::keys) + selection.insert(id); + } + if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear(); for (auto& [id, spritesheet] : anm2.content.spritesheets) { + auto isNewSpritesheet = newSpritesheetId == id; ImGui::PushID(id); if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders)) @@ -243,11 +340,6 @@ namespace anm2ed::imgui } if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) open_directory(spritesheet); - if (newSpritesheetId == id) - { - ImGui::SetScrollHereY(0.5f); - newSpritesheetId = -1; - } auto viewport = ImGui::GetMainViewport(); auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f; @@ -325,6 +417,13 @@ namespace anm2ed::imgui } ImGui::EndChild(); + + if (isNewSpritesheet) + { + ImGui::SetScrollHereY(0.5f); + newSpritesheetId = -1; + } + ImGui::PopID(); } @@ -385,7 +484,64 @@ namespace anm2ed::imgui if (imgui::shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused(); if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); + if (imgui::shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED) && selection.size() > 1) merge_open(); } ImGui::End(); + + mergePopup.trigger(); + if (ImGui::BeginPopupModal(mergePopup.label(), &mergePopup.isOpen, ImGuiWindowFlags_NoResize)) + { + settings.mergeSpritesheetsRegionOrigin = + glm::clamp(settings.mergeSpritesheetsRegionOrigin, (int)origin::TOP_LEFT, (int)origin::ORIGIN_CENTER); + + auto close = [&]() + { + mergeSelection.clear(); + mergePopup.close(); + }; + + auto optionsSize = child_size_get(5); + if (ImGui::BeginChild("##Merge Spritesheets Options", optionsSize, ImGuiChildFlags_Borders)) + { + ImGui::SeparatorText(localize.get(LABEL_REGION_PROPERTIES_ORIGIN)); + ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_BOTTOM), &settings.mergeSpritesheetsOrigin, + anm2::APPEND_BOTTOM); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_BOTTOM_LEFT)); + ImGui::SameLine(); + ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_RIGHT), &settings.mergeSpritesheetsOrigin, + anm2::APPEND_RIGHT); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_TOP_RIGHT)); + + ImGui::SeparatorText(localize.get(LABEL_OPTIONS)); + ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_SPRITESHEET_REGIONS), &settings.mergeSpritesheetsIsMakeRegions); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_SPRITESHEET_REGIONS)); + + const char* regionOriginOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT), + localize.get(LABEL_REGION_ORIGIN_CENTER)}; + ImGui::BeginDisabled(!settings.mergeSpritesheetsIsMakeRegions); + ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), &settings.mergeSpritesheetsRegionOrigin, + regionOriginOptions, IM_ARRAYSIZE(regionOriginOptions)); + ImGui::EndDisabled(); + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + shortcut(manager.chords[SHORTCUT_CONFIRM]); + ImGui::BeginDisabled(mergeSelection.size() <= 1); + if (ImGui::Button(localize.get(BASIC_MERGE), widgetSize)) + { + merge(); + close(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close(); + + ImGui::EndPopup(); + } + mergePopup.end(); + } } diff --git a/src/imgui/window/spritesheets.h b/src/imgui/window/spritesheets.h index c35e88d..1e3a9ad 100644 --- a/src/imgui/window/spritesheets.h +++ b/src/imgui/window/spritesheets.h @@ -11,6 +11,8 @@ namespace anm2ed::imgui class Spritesheets { int newSpritesheetId{-1}; + PopupHelper mergePopup{PopupHelper(LABEL_SPRITESHEETS_MERGE_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)}; + std::set mergeSelection{}; public: void update(Manager&, Settings&, Resources&, Dialog&, Clipboard& clipboard); diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index 0859436..2c206c7 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -395,17 +395,19 @@ namespace anm2ed::imgui { if (reference.itemType == anm2::LAYER && reference.itemID != -1) { - auto& layer = anm2.content.layers.at(reference.itemID); - auto spritesheet = anm2.spritesheet_get(layer.spritesheetID); - if (spritesheet) + anm2::Spritesheet* spritesheet = nullptr; + if (anm2.content.layers.contains(reference.itemID)) { - for (auto i : indices) - { - if (!vector::in_bounds(item->frames, i)) continue; - auto& frame = item->frames[i]; - if (frame.regionID != -1 && !spritesheet->regions.contains(frame.regionID)) - frame.regionID = -1; - } + auto& layer = anm2.content.layers.at(reference.itemID); + spritesheet = anm2.spritesheet_get(layer.spritesheetID); + } + + for (auto i : indices) + { + if (!vector::in_bounds(item->frames, i)) continue; + auto& frame = item->frames[i]; + if (frame.regionID == -1) continue; + if (!spritesheet || !spritesheet->regions.contains(frame.regionID)) frame.regionID = -1; } } @@ -1774,6 +1776,7 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); ImGui::BeginDisabled(source == source::EXISTING && addItemID == -1); + shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) { anm2::Reference addReference{}; @@ -1797,6 +1800,7 @@ namespace anm2ed::imgui ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) item_properties_close(); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CANCEL_ADD_ITEM)); @@ -1825,6 +1829,7 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); + shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(LABEL_BAKE), widgetSize)) { frames_bake(); @@ -1834,6 +1839,7 @@ namespace anm2ed::imgui ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) bakePopup.close(); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CANCEL_BAKE_FRAMES)); diff --git a/src/imgui/wizard/configure.cpp b/src/imgui/wizard/configure.cpp index 305fbcf..5c075d2 100644 --- a/src/imgui/wizard/configure.cpp +++ b/src/imgui/wizard/configure.cpp @@ -56,6 +56,16 @@ namespace anm2ed::imgui::wizard input_int_range(localize.get(LABEL_STACK_SIZE), temporary.fileSnapshotStackSize, 0, 100); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_STACK_SIZE)); + ImGui::SeparatorText(localize.get(LABEL_COMPATIBILITY)); + ImGui::RadioButton(localize.get(LABEL_ISAAC), &temporary.fileCompatibility, anm2::ISAAC); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ISAAC)); + ImGui::SameLine(); + ImGui::RadioButton(localize.get(LABEL_ANM2ED), &temporary.fileCompatibility, anm2::ANM2ED); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED)); + ImGui::SameLine(); + ImGui::RadioButton(localize.get(LABEL_ANM2ED_LIMITED), &temporary.fileCompatibility, anm2::ANM2ED_LIMITED); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED_LIMITED)); + ImGui::SeparatorText(localize.get(LABEL_OPTIONS)); ImGui::Checkbox(localize.get(LABEL_OVERWRITE_WARNING), &temporary.fileIsWarnOverwrite); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERWRITE_WARNING)); @@ -164,6 +174,7 @@ namespace anm2ed::imgui::wizard auto widgetSize = widget_size_with_row_get(3); + shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(BASIC_SAVE), widgetSize)) { settings = temporary; @@ -190,6 +201,7 @@ namespace anm2ed::imgui::wizard ImGui::SameLine(); + shortcut(manager.chords[SHORTCUT_CLOSE]); if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) isSet = true; ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CLOSE_SETTINGS)); } diff --git a/src/imgui/wizard/render_animation.cpp b/src/imgui/wizard/render_animation.cpp index d22a3aa..86e78f6 100644 --- a/src/imgui/wizard/render_animation.cpp +++ b/src/imgui/wizard/render_animation.cpp @@ -3,6 +3,7 @@ #include #include +#include "imgui_.h" #include "log.h" #include "path_.h" #include "process_.h" @@ -192,6 +193,7 @@ namespace anm2ed::imgui::wizard ImGui::Separator(); + imgui::shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(LABEL_RENDER), widgetSize)) { path.replace_extension(render::EXTENSIONS[type]); @@ -304,6 +306,7 @@ namespace anm2ed::imgui::wizard ImGui::SameLine(); + imgui::shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) isEnd = true; } } diff --git a/src/manager.cpp b/src/manager.cpp index 4e0b06f..b631d48 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -99,7 +99,7 @@ namespace anm2ed void Manager::new_(const std::filesystem::path& path) { open(path, true); } - void Manager::save(int index, const std::filesystem::path& path) + void Manager::save(int index, const std::filesystem::path& path, anm2::Compatibility compatibility) { if (auto document = get(index); document) { @@ -107,18 +107,21 @@ namespace anm2ed ensure_parent_directory_exists(path); document->path = !path.empty() ? path : document->path; document->path.replace_extension(".anm2"); - document->save(document->path, &errorString); + document->save(document->path, &errorString, compatibility); recent_file_add(document->path); } } - void Manager::save(const std::filesystem::path& path) { save(selected, path); } + void Manager::save(const std::filesystem::path& path, anm2::Compatibility compatibility) + { + save(selected, path, compatibility); + } - void Manager::autosave(Document& document) + void Manager::autosave(Document& document, anm2::Compatibility compatibility) { std::string errorString{}; auto autosavePath = document.autosave_path_get(); - if (!document.autosave(&errorString)) return; + if (!document.autosave(&errorString, compatibility)) return; autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), autosavePath), autosaveFiles.end()); autosaveFiles.insert(autosaveFiles.begin(), autosavePath); diff --git a/src/manager.h b/src/manager.h index 1744928..2dad576 100644 --- a/src/manager.h +++ b/src/manager.h @@ -66,9 +66,9 @@ namespace anm2ed Document* get(int = -1); Document* open(const std::filesystem::path&, bool = false, bool = true); void new_(const std::filesystem::path&); - void save(int, const std::filesystem::path& = {}); - void save(const std::filesystem::path& = {}); - void autosave(Document&); + void save(int, const std::filesystem::path& = {}, anm2::Compatibility = anm2::ANM2ED); + void save(const std::filesystem::path& = {}, anm2::Compatibility = anm2::ANM2ED); + void autosave(Document&, anm2::Compatibility = anm2::ANM2ED); void set(int); void close(int); void layer_properties_open(int = -1); diff --git a/src/resource/strings.h b/src/resource/strings.h index 5f8435d..0532659 100644 --- a/src/resource/strings.h +++ b/src/resource/strings.h @@ -58,6 +58,7 @@ namespace anm2ed X(BASIC_INDEX, "Index", "Indice", "Индекс", "下标", "인덱스") \ X(BASIC_INTERPOLATED, "Interpolated", "Interpolado", "Интерполировано", "线性插值", "매끄럽게 연결") \ X(BASIC_LAYER_ANIMATION, "Layer", "Capa", "Слой", "动画层", "레이어") \ + X(BASIC_MERGE, "Merge", "Combinar", "Соединить", "合并", "병합") \ X(BASIC_MODE, "Mode", "Modo", "Режим", "模式", "모드") \ X(BASIC_NAME, "Name", "Nombre", "Имя", "名字", "이름") \ X(BASIC_NEW, "New", "Nuevo", "Новый", "新建", "새 파일") \ @@ -67,6 +68,7 @@ namespace anm2ed X(BASIC_OFFSET, "Offset", "Offset", "Смещение", "偏移", "오프셋") \ X(BASIC_OPEN, "Open", "Abrir", "Открыть", "打开", "열기") \ X(BASIC_OPEN_DIRECTORY, "Open Directory", "Abrir Directorio", "Открыть директорию", "打开目录", "디렉터리 열기") \ + X(BASIC_PACK, "Pack", "Empaquetar", "Упаковать", "打包", "패킹") \ X(BASIC_PASTE, "Paste", "Pegar", "Вставить", "粘贴", "붙여넣기") \ X(BASIC_PIVOT, "Pivot", "Pivote", "Точка вращения", "枢轴", "중심점") \ X(BASIC_POSITION, "Position", "Posicion", "Позиция", "位置", "위치") \ @@ -83,6 +85,7 @@ namespace anm2ed X(BASIC_SCALE, "Scale", "Escalar", "Масштаб", "缩放", "크기") \ X(BASIC_SIZE, "Size", "Tamaño", "Размер", "大小", "비율") \ X(BASIC_SOUND, "Sound", "Sonido", "Звук", "声音", "사운드") \ + X(BASIC_TRIM, "Trim", "Recortar contenido", "Обрезать по содержимому", "修剪", "내용으로 자르기") \ X(BASIC_TIME, "Time", "Tiempo", "Время", "时间", "시간") \ X(BASIC_TINT, "Tint", "Matiz", "Оттенок", "色调", "색조") \ X(BASIC_TRIGGERS, "Triggers", "Triggers", "Триггер", "事件触发器", "트리거") \ @@ -116,6 +119,7 @@ namespace anm2ed X(EDIT_FRAME_COLOR_OFFSET, "Frame Color Offset", "Offset de color de Frame", "Смещение цвета кадра", "帧颜色偏移", "프레임 색상 오프셋") \ X(EDIT_FRAME_CROP, "Frame Crop", "Recorte de Frame", "Обрезка кадра", "帧裁剪", "프레임 자르기") \ X(EDIT_FRAME_DURATION, "Frame Duration", "Duracion de Frame", "Продолжительность кадра", "帧时长", "프레임 유지 시간") \ + X(EDIT_FRAME_REGION, "Frame Region", "Region de Frame", "Регион кадра", "帧区域", "프레임 영역") \ X(EDIT_FRAME_FLIP_X, "Frame Flip X", "Invertir X de Frame", "Отразить кадр по X", "X轴翻转", "프레임 수평 뒤집기") \ X(EDIT_FRAME_FLIP_Y, "Frame Flip Y", "Invertir Y de Frame", "Отразить кадр по Y", "Y轴翻转", "프레임 수직 뒤집기") \ X(EDIT_FRAME_INTERPOLATION, "Frame Interpolation", "Interpolacion de Frame", "Интерполяция кадра", "帧线性插值", "매끄럽게 프레임 연결") \ @@ -131,8 +135,11 @@ namespace anm2ed X(EDIT_LOOP, "Loop", "Loop", "Цикл", "循环", "반복") \ X(EDIT_MERGE_ANIMATIONS, "Merge Animations", "Combinar Animaciones", "Соединить анимации", "合并多个动画", "애니메이션 병합") \ X(EDIT_MERGE_ANM2, "Merge Anm2", "Combinar Anm2", "Соединить Anm2", "合并多个Anm2", "Anm2 병합") \ + X(EDIT_MERGE_SPRITESHEETS, "Merge Spritesheets", "Combinar Spritesheets", "Объединить спрайт-листы", "合并图集", "스프라이트 시트 병합") \ + X(EDIT_PACK_SPRITESHEET, "Pack Spritesheet", "Empaquetar spritesheet", "Упаковать спрайт-лист", "打包图集", "스프라이트 시트 패킹") \ X(EDIT_MOVE_ANIMATIONS, "Move Animation(s)", "Mover Animacion(es)", "Переместить анимации", "移动动画", "애니메이션 이동") \ X(EDIT_MOVE_FRAMES, "Move Frame(s)", "Mover Frame(s)", "Перемесить кадры", "移动多个/单个帧", "프레임 이동") \ + X(EDIT_MOVE_REGIONS, "Move Regions", "Mover regiones", "Переместить регионы", "移动区域", "영역 이동") \ X(EDIT_MOVE_LAYER_ANIMATION, "Move Layer Animation", "Mover Animacion de Capa", "Переместить анимацию слоя", "移动动画层", "레이어 애니메이션 이동") \ X(EDIT_PASTE_ANIMATIONS, "Paste Animation(s)", "Pegar Animacion(es)", "Вставить анимации", "粘贴动画", "애니메이션 붙여넣기") \ X(EDIT_PASTE_EVENTS, "Paste Event(s)", "Pegar Eventos", "Вставить события", "粘贴事件", "이벤트 붙여넣기") \ @@ -157,7 +164,9 @@ namespace anm2ed X(EDIT_REPLACE_SOUND, "Replace Sound", "Reemplazar Sonido", "Заменить звук", "替换声音", "사운드 교체") \ X(EDIT_SET_LAYER_PROPERTIES, "Set Layer Properties", "Establecer Propiedades de Capa", "Установить свойства слоя", "更改动画层属性", "레이어 속성 설정") \ X(EDIT_SET_REGION_PROPERTIES, "Set Region Properties", "Establecer propiedades de región", "Установить свойства региона", "更改区域属性", "영역 속성 설정") \ + X(EDIT_TRIM_REGIONS, "Trim Regions", "Recortar regiones", "Обрезать регионы", "修剪区域", "영역 트리밍") \ X(EDIT_SET_NULL_PROPERTIES, "Set Null Properties", "Establecer Propiedades Null", "Установить свойства нуля", "更改Null属性", "Null 속성 설정") \ + X(EDIT_SCAN_AND_SET_REGIONS, "Scan and Set Regions", "Escanear y establecer regiones", "Сканировать и установить регионы", "扫描并设置区域", "영역 스캔 및 설정") \ X(EDIT_REGION_CROP, "Region Crop", "Recorte de región", "Обрезка региона", "区域裁剪", "영역 자르기") \ X(EDIT_REGION_MOVE, "Region Pivot", "Pivote de región", "Пивот региона", "区域枢轴", "영역 피벗") \ X(EDIT_SPLIT_FRAME, "Split Frame", "Dividir Frame", "Разделить кадр", "拆分帧", "프레임 분할") \ @@ -174,6 +183,7 @@ namespace anm2ed X(FORMAT_DURATION, "Duration: {0}", "Duracion: {0}", "Продолжительность: {0}", "时长: {0}", "유지 시간: {0}") \ X(FORMAT_EVENT_LABEL, "Event: {0}", "Evento: {0}", "Событие: {0}", "事件: {0}", "이벤트: {0}") \ X(FORMAT_ID, "ID: {0}", "ID: {0}", "ID: {0}", "ID: {0}", "ID: {0}") \ + X(FORMAT_ORIGIN, "Origin: {0}", "Origen: {0}", "Точка отсчета: {0}", "原点: {0}", "원점: {0}") \ X(FORMAT_SPRITESHEET_ID, "Spritesheet ID: {0}", "ID de Spritesheet: {0}", "", "图集 ID: {0}", "스프라이트 시트 ID: {0}") \ X(FORMAT_INDEX, "Index: {0}", "Indice: {0}", "Индекс: {0}", "下标: {0}", "인덱스: {0}") \ X(FORMAT_INTERPOLATED, "Interpolated: {0}", "Interpolado: {0}", "Интерполировано: {0}", "线性插值: {0}", "매끄럽게 연결: {0}") \ @@ -191,6 +201,7 @@ namespace anm2ed X(FORMAT_SOUND_LABEL, "Sound: {0}", "Sonido: {0}", "Звук: {0}", "声音: {0}", "사운드: {0}") \ X(FORMAT_SPRITESHEET, "#{0} {1}", "#{0} {1}", "#{0} {1}", "#{0} {1}", "#{0} {1}") \ X(FORMAT_SOUND, "#{0} {1}", "#{0} {1}", "#{0} {1}", "#{0} {1}", "#{0} {1}") \ + X(FORMAT_REGION, "#{0} {1}", "#{0} {1}", "#{0} {1}", "#{0} {1}", "#{0} {1}") \ X(FORMAT_TRANSFORM, "Transform: {0}", "Transformar: {0}", "Трансформация: {0}", "变换: {0}", "변환: {0}") \ X(FORMAT_RECT, "Rect: {0}", "Rect: {0}", "Прямоугольник: {0}", "矩形: {0}", "사각형: {0}") \ X(FORMAT_FRAMES_COUNT, "Frames: {0}", "Frames: {0}", "Кадры: {0}", "帧: {0}", "프레임: {0}") \ @@ -203,13 +214,15 @@ namespace anm2ed X(LABEL_ALT_ICONS, "Alt Icons", "Iconos Alternos", "Альт-иконки", "替代图标", "대체 아이콘") \ X(LABEL_ANIMATIONS_CHILD, "Animations", "Animaciones", "", "动画", "애니메이션") \ X(LABEL_ANIMATIONS_MERGE_POPUP, "Merge Animations", "Combinar Animaciones", "Соединить анимации", "合并多个动画", "애니메이션 병합") \ + X(LABEL_SPRITESHEETS_PACK_POPUP, "Pack Spritesheet", "Empaquetar spritesheet", "Упаковать спрайт-лист", "打包图集", "스프라이트 시트 패킹") \ + X(LABEL_SPRITESHEETS_MERGE_POPUP, "Merge Spritesheets", "Combinar Spritesheets", "Объединить спрайт-листы", "合并图集", "스프라이트 시트 병합") \ X(LABEL_ANIMATIONS_WINDOW, "Animations###Animations", "Animaciones###Animations", "Анимации###Animations", "动画###Animations", "애니메이션###Animations") \ X(LABEL_REGIONS_WINDOW, "Regions###Regions", "Regiones###Regions", "Регионы###Regions", "区域###Regions", "영역###Regions") \ X(LABEL_ANIMATION_LENGTH, "Animation Length", "Duracion de Animacion", "Длина анимации", "动画时长", "애니메이션 길이") \ X(LABEL_ANIMATION_PREVIEW_WINDOW, "Animation Preview###Animation Preview", "Vista Previa de Animacion###Animation Preview", "Предпросмотр анимации###Animation Preview", "动画预放###Animation Preview", "애니메이션 프리뷰###Animation Preview") \ X(LABEL_APPEND_FRAMES, "Append Frames", "Anteponer Frames", "Добавить кадры к концу", "在后面添加帧", "뒷프레임에 추가") \ X(LABEL_APPLICATION_NAME, "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed") \ - X(LABEL_APPLICATION_VERSION, "Version 2.2", "Version 2.2", "Версия 2.2", "2.2版本", "버전 2.2") \ + X(LABEL_APPLICATION_VERSION, "Version 2.3", "Version 2.3", "Версия 2.3", "2.3版本", "버전 2.3") \ X(LABEL_AUTHOR, "Author", "Autor", "Автор", "制作者", "작성자") \ X(LABEL_AUTOSAVE, "Autosave", "Autoguardado", "Автосохранение", "自动保存", "자동저장") \ X(LABEL_AXES, "Axes", "Ejes", "Оси", "坐标轴", "가로/세로 축") \ @@ -222,6 +235,7 @@ namespace anm2ed X(LABEL_CLAMP, "Clamp", "Clamp", "Ограничить", "限制数值范围", "작업 영역 제한") \ X(LABEL_CLEAR_LIST, "Clear List", "Limpiar Lista", "Стереть список", "清除列表", "기록 삭제") \ X(LABEL_CLOSE, "Close", "Cerrar", "Закрыть", "关闭", "닫기") \ + X(LABEL_COMPATIBILITY, "Compatibility", "Compatibilidad", "Совместимость", "兼容性", "호환성") \ X(LABEL_CUSTOM_RANGE, "Custom Range", "Rango Personalizado", "Пользовательский диапазон", "自定义范围", "길이 맞춤설정") \ X(LABEL_DELETE, "Delete", "Borrar", "Удалить", "删除", "삭제") \ X(LABEL_DELETE_ANIMATIONS_AFTER, "Delete Animations After", "Borrar Animaciones Despues", "Удалить анимации после", "删除之后的动画", "기존 애니메이션 삭제") \ @@ -265,15 +279,25 @@ namespace anm2ed X(LABEL_LAYERS_CHILD, "Layers List", "Lista de Capas", "", "动画层列表", "레이어 목록") \ X(LABEL_LAYERS_WINDOW, "Layers###Layers", "Capas###Layers", "Слои###Layers", "动画层###Layers", "레이어###Layers") \ X(LABEL_THIS_ANIMATION, "This Animation", "Esta Animacion", "Эта анимация", "此动画", "이 애니메이션") \ + X(LABEL_ISAAC, "Isaac", "Isaac", "Isaac", "Isaac", "Isaac") \ + X(LABEL_ANM2ED, "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed") \ + X(LABEL_ANM2ED_LIMITED, "Anm2Ed Limited", "Anm2Ed Limitado", "Anm2Ed Ограниченный", "Anm2Ed 限制版", "Anm2Ed 제한") \ X(LABEL_DESTINATION, "Destination", "Destino", "Назначение", "目标", "대상") \ X(LABEL_LOCALIZATION, "Localization", "Localizacion", "Локализация", "本地化", "현지화") \ X(LABEL_LOOP, "Loop", "Loop", "Цикл", "循环", "반복") \ X(LABEL_MANAGER_ANM2_DRAG_DROP, "Anm2 Drag Drop", "Arrastrar y Soltar Anm2", "Anm2 Drag Drop", "Anm2 拖放", "Anm2 드래그 앤 드롭") \ X(LABEL_REGION_PROPERTIES, "Region Properties", "Propiedades de región", "Свойства региона", "区域属性", "영역 속성") \ + X(LABEL_REGION_PROPERTIES_ORIGIN, "Origin", "Origen", "Точка отсчета", "原点", "원점") \ + X(LABEL_REGION_ORIGIN_TOP_LEFT, "Top Left", "Superior izquierda", "Верхний левый", "左上", "왼쪽 위") \ + X(LABEL_REGION_ORIGIN_CENTER, "Center", "Centro", "Центр", "中心", "중앙") \ + X(LABEL_REGION_ORIGIN_CUSTOM, "Custom", "Personalizado", "Пользовательский", "自定义", "사용자 지정") \ X(LABEL_MANAGER_LAYER_PROPERTIES, "Layer Properties", "Propiedades de Capa", "Свойства слоя", "动画层属性", "레이어 속성") \ X(LABEL_MANAGER_NULL_PROPERTIES, "Null Properties", "Propiedades Null", "Свойства нуля", "Null属性", "Null 속성") \ X(LABEL_MANAGER_RENDERING_PROGRESS, "Rendering...", "Renderizando...", "Рендеринг...", "渲染中...", "렌더링 중...") \ X(LABEL_MERGE, "Merge", "Combinar", "Соединить", "合并", "병합") \ + X(LABEL_MERGE_SPRITESHEETS_APPEND_RIGHT, "Top Right", "Arriba a la derecha", "Сверху справа", "右上", "우측 상단") \ + X(LABEL_MERGE_SPRITESHEETS_APPEND_BOTTOM, "Bottom Left", "Abajo a la izquierda", "Снизу слева", "左下", "좌측 하단") \ + X(LABEL_MERGE_MAKE_SPRITESHEET_REGIONS, "Make Spritesheet Regions", "Crear regiones de spritesheet", "Создать регионы спрайт-листа", "生成图集区域", "스프라이트 시트 영역 만들기") \ X(LABEL_MOVE_TOOL_SNAP, "Move Tool: Snap to Mouse", "Herramienta Mover: Ajustar al Mouse", "Инструмент перемещения: Привязка к мыши", "移动工具: 吸附到鼠标指针", "이동 도구: 마우스에 맞추기") \ X(LABEL_MULTIPLY, "Multiply", "Multiplicar", "Умножить", "乘", "곱하기") \ X(LABEL_NEW, "New", "Nuevo", "Новый", "新", "새로") \ @@ -317,6 +341,7 @@ namespace anm2ed X(LABEL_SPRITESHEET, "Spritesheet", "Spritesheet", "Спрайт-лист", "图集", "스프라이트 시트") \ X(LABEL_SPRITESHEETS_WINDOW, "Spritesheets###Spritesheets", "Spritesheets###Spritesheets", "Спрайт-листы###Spritesheets", "图集###Spritesheets", "스프라이트 시트###Spritesheets") \ X(LABEL_SPRITESHEET_EDITOR_WINDOW, "Spritesheet Editor###Spritesheet Editor", "Editor de Spritesheet###Spritesheet Editor", "Редактор спрайт-листов###Spritesheet Editor", "图集编辑器###Spritesheet Editor", "스프라이트 편집기###Spritesheet Editor") \ + X(LABEL_SCAN_AND_SET_REGIONS, "Scan and Set Regions", "Escanear y establecer regiones", "Сканировать и установить регионы", "扫描并设置区域", "영역 스캔 및 설정") \ X(LABEL_STACK_SIZE, "Stack Size", "Tamaño de Stack", "Размер стека", "栈内存大小", "스택 크기") \ X(LABEL_START, "Start", "Empezar", "Старт", "开始", "시작") \ X(LABEL_SUBTRACT, "Subtract", "Substraer", "Вычетание", "减去", "빼기") \ @@ -359,6 +384,8 @@ namespace anm2ed X(SHORTCUT_STRING_COPY, "Copy", "Copiar", "Копировать", "复制", "복사") \ X(SHORTCUT_STRING_CROP, "Crop", "Recortar", "Обрезать", "裁剪", "자르기") \ X(SHORTCUT_STRING_CUT, "Cut", "Cortar", "Вырезать", "剪切", "잘라내기") \ + X(SHORTCUT_STRING_CONFIRM, "Confirm", "Confirmar", "Подтвердить", "确定", "확인") \ + X(SHORTCUT_STRING_CANCEL, "Cancel", "Cancelar", "Отмена", "取消", "취소") \ X(SHORTCUT_STRING_DEFAULT, "Default", "Predeterminado", "По умолчанию", "默认", "기본값") \ X(SHORTCUT_STRING_DRAW, "Draw", "Dibujar", "Рисовать", "绘画", "그리기") \ X(SHORTCUT_STRING_DUPLICATE, "Duplicate", "Duplicar", "Дублировать", "拷贝", "복제") \ @@ -397,6 +424,8 @@ namespace anm2ed X(SNAPSHOT_RENAME_ANIMATION, "Rename Animation", "Renombrar Animacion", "Переименовать анимацию", "重命名动画", "애니메이션 이름 바꾸기") \ X(TEXT_SELECT_FRAME, "Select a frame first!", "¡Selecciona primero un frame!", "Сначала выберите кадр!", "请先选择帧!", "먼저 프레임을 선택하세요!") \ X(TEXT_SELECT_FRAME_OR_REGION, "Select a frame or region first!", "¡Selecciona primero un frame o región!", "Сначала выберите кадр или регион!", "请先选择帧或区域!", "먼저 프레임 또는 영역을 선택하세요!") \ + X(TEXT_MERGE_SPRITESHEETS_DESCRIPTION, "Merge selected spritesheets into the first selected spritesheet.", "Combina los spritesheets seleccionados en el primer spritesheet seleccionado.", "Объединить выбранные спрайт-листы в первый выбранный спрайт-лист.", "将所选图集合并到第一个选中的图集中。", "선택된 스프라이트 시트를 첫 번째 선택된 스프라이트 시트로 병합합니다.") \ + X(TEXT_PACK_SPRITESHEET_DESCRIPTION, "Pack this spritesheet using its region rectangles and rebuild the texture from packed regions.", "Empaqueta este spritesheet usando sus rectángulos de región y reconstruye la textura con las regiones empaquetadas.", "Упаковать этот спрайт-лист, используя прямоугольники его регионов, и пересобрать текстуру из упакованных регионов.", "使用该图集的区域矩形进行打包,并用打包后的区域重建纹理。", "이 스프라이트 시트의 영역 사각형을 기준으로 패킹하고, 패킹된 영역으로 텍스처를 다시 만듭니다.") \ X(TEXT_SELECT_SPRITESHEET, "Select a spritesheet first!", "¡Selecciona primero un spritesheet!", "Сначала выберите спрайт-лист!", "请先选择图集!", "먼저 스프라이트 시트를 선택하세요!") \ X(TEXT_TOOL_ANIMATION_PREVIEW, "This tool can only be used in Animation Preview!", "¡Esta herramienta solo se puede usar en Vista previa de animación!", "Этот инструмент можно использовать только в \"Предпросмотре анимации\"!", "该工具只能在“动画预放”中使用!", "이 도구는 애니메이션 프리뷰에서만 사용할 수 있습니다!") \ X(TEXT_TOOL_SPRITESHEET_EDITOR, "This tool can only be used in Spritesheet Editor!", "¡Esta herramienta solo se puede usar en el Editor de spritesheets!", "Этот инструмент можно использовать только в \"Редакторе спрайт-листов\"!", "该工具只能在“图集编辑器”中使用!", "이 도구는 스프라이트 시트 편집기에서만 사용할 수 있습니다!") \ @@ -434,7 +463,12 @@ namespace anm2ed X(ERROR_FILE_NOT_FOUND, "File not found!", "¡Archivo no encontrado!", "Файл не найден!", "找不到文件!", "파일을 찾을 수 없습니다!") \ X(ERROR_FILE_PERMISSIONS, "File does not have write permissions!", "¡El archivo no tiene permisos de escritura!", "У файла нет прав на запись!", "文件没有写入权限!", "파일에 쓰기 권한이 없습니다!") \ X(TOAST_RELOAD_SPRITESHEET, "Reloaded spritesheet #{0}: {1}", "Se ha recargado spritesheet #{0}: {1}", "Спрайт-лист #{0} перезагружен: {1}", "重新加载了图集 #{0}: {1}", "{0}번 스프라이트 시트 다시 불러옴: {1}") \ + X(TOAST_MERGE_SPRITESHEETS, "Merged selected spritesheets.", "Spritesheets seleccionados combinados.", "Выбранные спрайт-листы объединены.", "已合并所选图集。", "선택된 스프라이트 시트를 병합했습니다.") \ + X(TOAST_MERGE_SPRITESHEETS_FAILED, "Failed to merge selected spritesheets.", "No se pudieron combinar los spritesheets seleccionados.", "Не удалось объединить выбранные спрайт-листы.", "合并所选图集失败。", "선택된 스프라이트 시트 병합에 실패했습니다.") \ + X(TOAST_PACK_SPRITESHEET, "Packed spritesheet.", "Spritesheet empaquetado.", "Спрайт-лист упакован.", "图集已打包。", "스프라이트 시트를 패킹했습니다.") \ + X(TOAST_PACK_SPRITESHEET_FAILED, "Failed to pack spritesheet.", "No se pudo empaquetar el spritesheet.", "Не удалось упаковать спрайт-лист.", "图集打包失败。", "스프라이트 시트 패킹에 실패했습니다.") \ X(TOAST_RELOAD_SOUND, "Reloaded sound #{0}: {1}", "Se ha recargado sonido #{0}: {1}", "Звук #{0} перезагружен: {1}", "重新加载了声音 #{0}: {1}", "{0}번 사운드 다시 불러옴: {1}") \ + X(TOAST_SCAN_AND_SET_REGIONS, "Matched regionless frames to candidate regions.", "Se emparejaron frames sin region con regiones candidatas.", "Кадры без региона сопоставлены с подходящими регионами.", "已将无区域帧匹配到候选区域。", "영역이 없는 프레임을 후보 영역에 매칭했습니다.") \ X(TOAST_REMOVE_SPRITESHEET, "Removed spritesheet #{0}: {1}", "Se ha removido spritesheet #{0}: {1}", "Спрайт-лист #{0} удален: {1}", "去除了图集 #{0}: {1}", "{0}번 스프라이트 시트 제거됨: {1}") \ X(TOAST_REPLACE_SPRITESHEET, "Replaced spritesheet #{0}: {1}", "Se ha reemplazado spritesheet #{0}: {1}", "Спрайт-лист #{0} заменен: {1}", "替换了图集 #{0}: {1}", "{0}번 스프라이트 시트 교체됨: {1}") \ X(TOAST_REPLACE_SOUND, "Replaced sound #{0}: {1}", "Se ha reemplazado sonido #{0}: {1}", "Звук #{0} заменен: {1}", "已替换声音 #{0}: {1}", "{0}번 사운드 교체됨: {1}") \ @@ -459,7 +493,7 @@ namespace anm2ed X(TOOLTIP_ADD_LAYER, "Add a layer.", "Añadir una capa.", "Добавить слой.", "添加一个动画层.", "레이어를 추가합니다.") \ X(TOOLTIP_ADD_NULL, "Add a null.", "Añadir Null.", "Добавить нуль.", "添加一个Null.", "Null을 추가합니다.") \ X(TOOLTIP_ADD_SPRITESHEET, "Add a new spritesheet.", "Añadir nueva spritesheet.", "Добавить новый спрайт-лист.", "添加一个新图集.", "새 스프라이트 시트를 추가합니다.") \ - X(TOOLTIP_ADD_VALUES, "Add the specified values onto each frame.\n(Boolean values will simply be set.)", "Añadir los valores especifiados a cada Frame.\n(los valores booleanos seran ajustados.)", "Добавить указанные значения к каждому кадру.\n(Булевы значения будут просто установлены.)", "将指定的数值添加到每一帧上. (任何布尔值[真假值]将直接被设置.)", "각 프레임의 속성에 지정한 값을 더합니다.\n(참/거짓 값은 그대로 설정됩니다.)") \ + X(TOOLTIP_ADD_VALUES, "Add the specified values onto each frame.\n(Boolean/mapped values will simply be set.)", "Añadir los valores especifiados a cada Frame.\n(Los valores booleanos/mapeados simplemente se estableceran.)", "Добавить указанные значения к каждому кадру.\n(Булевы/сопоставленные значения будут просто установлены.)", "将指定的数值添加到每一帧上. (布尔/映射值将直接被设置.)", "각 프레임의 속성에 지정한 값을 더합니다.\n(불리언/매핑 값은 그대로 설정됩니다.)") \ X(TOOLTIP_ADJUST, "Set the value of each specified value onto the frame's equivalent.", "Ajustar el valor de cada valor especificado a el equivalente del Frame.", "Установить значение каждого указанного значения к эквиваленту кадра.", "将每个指定的数值设置到对应帧的相等的属性上.", "지정된 각 값을 프레임의 대응 값으로 설정합니다.") \ X(TOOLTIP_ALL_ITEMS_VISIBLE, "All items are visible. Press to only show layers.", "Todos los items son visibles. Presiona solo para mostrar capas.", "Все предметы видимы. Нажмите, чтобы только показать слои.", "所有物品均可见. 点击即可仅显示动画层.", "모든 항목이 표시됩니다. 레이어만 표시하려면 누르세요.") \ X(TOOLTIP_ALT_ICONS, "Toggle a different appearance of the target icons.", "Alterna una apariencia diferente de los iconos de objetivo", "Переключить альтернативный вид иконок-перекрестий.", "切换指定图标为另一个样式.", "대상 아이콘을 다른 외형으로 전환합니다.") \ @@ -476,14 +510,18 @@ namespace anm2ed X(TOOLTIP_CANCEL_BAKE_FRAMES, "Cancel baking frames.", "Cancelar hacer bake de Frames.", "Отменить запечку кадров.", "取消提前渲染.", "프레임 베이킹을 취소합니다.") \ X(TOOLTIP_CENTER_VIEW, "Centers the view.", "Centra la vista.", "Центрирует вид.", "居中视角.", "미리보기 화면을 가운데에 맞춥니다.") \ X(TOOLTIP_CLOSE_SETTINGS, "Close without updating settings.", "Cerrar sin actualizar las configuraciones.", "Закрыть без обновления настройки.", "关闭但不保存设置.", "설정을 갱신하지 않고 닫습니다.") \ + X(TOOLTIP_COMPATIBILITY_ISAAC, "Sets the output file format to that of The Binding of Isaac: Rebirth's.\nThis removes the following:\n- Sounds\n- Regions\nNOTE: This will not serialize this data and it won't be able to be recovered.", "Establece el formato del archivo de salida al de The Binding of Isaac: Rebirth.\nEsto elimina lo siguiente:\n- Sonidos\n- Regiones\nNOTA: Estos datos no se serializaran y no podran recuperarse.", "Устанавливает формат выходного файла как у The Binding of Isaac: Rebirth.\nЭто удаляет следующее:\n- Звуки\n- Регионы\nПРИМЕЧАНИЕ: Эти данные не будут сериализованы, и их нельзя будет восстановить.", "将输出文件格式设置为 The Binding of Isaac: Rebirth 的格式。\n这会移除以下内容:\n- 声音\n- 区域\n注意:这些数据不会被序列化,且无法恢复。", "출력 파일 형식을 The Binding of Isaac: Rebirth의 형식으로 설정합니다.\n다음 항목이 제거됩니다:\n- 사운드\n- 영역\n참고: 이 데이터는 직렬화되지 않으며 복구할 수 없습니다.") \ + X(TOOLTIP_COMPATIBILITY_ANM2ED, "Sets the output file format to that of this editor.\nAll features will be serialized, including Sounds and Regions.", "Establece el formato del archivo de salida al de este editor.\nTodas las funciones se serializaran, incluyendo Sonidos y Regiones.", "Устанавливает формат выходного файла как у этого редактора.\nВсе возможности будут сериализованы, включая звуки и регионы.", "将输出文件格式设置为本编辑器的格式。\n所有功能都会被序列化,包括声音和区域。", "출력 파일 형식을 이 편집기의 형식으로 설정합니다.\n사운드와 영역을 포함한 모든 기능이 직렬화됩니다.") \ + X(TOOLTIP_COMPATIBILITY_ANM2ED_LIMITED, "Sets the output file format to that of this editor.\nThis will additionally remove redundant Region-specific information in frames.", "Establece el formato del archivo de salida al de este editor.\nEsto ademas eliminara informacion redundante especifica de Region en los frames.", "Устанавливает формат выходного файла как у этого редактора.\nДополнительно это удалит избыточную информацию, связанную с регионами, в кадрах.", "将输出文件格式设置为本编辑器的格式。\n此外,这还会移除帧中冗余的区域专用信息。", "출력 파일 형식을 이 편집기의 형식으로 설정합니다.\n추가로 프레임 내 중복된 영역 관련 정보를 제거합니다.") \ X(TOOLTIP_COLOR_OFFSET, "Change the color added onto the frame.", "Cambia el color añadido al Frame.", "Изменить цвет, который добавлен на кадр.", "更改覆盖在帧上的颜色.", "프레임에 더해지는 색을 변경합니다.") \ X(TOOLTIP_REGION, "Set the spritesheet region the frame will use.", "Establece la región del spritesheet que usará el frame.", "Установить регион спрайт-листа, который будет использовать кадр.", "设置帧将使用的图集区域.", "프레임이 사용할 스프라이트 시트 영역을 설정합니다.") \ + X(TOOLTIP_REGION_PROPERTIES_ORIGIN, "Use a preset origin for the region.", "Usa un origen predefinido para la región.", "Использовать предустановленную точку отсчета для региона.", "为区域使用预设原点。", "영역에 사전 설정된 원점을 사용합니다.") \ X(TOOLTIP_COLUMNS, "Set how many columns the spritesheet will have.", "Ajusta cuantas columnas va a tener el spritesheet.", "Установить сколько колонн будет иметь спрайт-лист.", "设置图集有多少列.", "스프라이트 시트의 열 수를 설정합니다.") \ X(TOOLTIP_CROP, "Change the crop position the frame uses.", "Cambiar la poscicion de recortado que usa el Frame.", "Изменить позицию обрезки, которую использует кадр.", "更改当前帧的裁剪位置.", "프레임에 대응되는 스프라이트 시트를 어느 지점부터 사용할지 변경합니다.") \ X(TOOLTIP_CUSTOM_RANGE, "Toggle using a custom range for the animation.", "Alterna usando un rango personalizado para la animacion.", "Переключить использование пользовательского диапазона для анимации.", "切换是否让动画使用自定义区间.", "애니메이션에 사용자 지정 길이를 사용할지 정합니다.") \ X(TOOLTIP_DELETE_ANIMATIONS_AFTER, "Delete animations after merging them.", "Borrar animaciones despues de combinarlas.", "Удалить анимации после их соединения.", "合并动画后,删除其他动画。", "병합 후 기존 애니메이션을 삭제합니다.") \ X(TOOLTIP_DELETE_FRAMES, "Delete the selected frames.", "Borrar los Frames seleccionados.", "Удалить выбранные кадры.", "删除所选帧.", "선택된 프레임을 삭제합니다.") \ - X(TOOLTIP_DIVIDE_VALUES, "Divide the specified values for each frame.\n(Boolean values will simply be set.)", "Dividir los valores especificos para cada Frame.\n(Los valores booleanos seran ajustados. )", "Разделить указанные значения для каждого кадра.\n(Булевы значения будут просто установлены.)", "将每一帧的指定值进行除法操作. (布尔值将直接被设置.)", "각 프레임의 속성을 지정된 값으로 나눕니다.\n(참/거짓 값은 그대로 설정됩니다.)") \ + X(TOOLTIP_DIVIDE_VALUES, "Divide the specified values for each frame.\n(Boolean/mapped values will simply be set.)", "Dividir los valores especificos para cada Frame.\n(Los valores booleanos/mapeados simplemente se estableceran.)", "Разделить указанные значения для каждого кадра.\n(Булевы/сопоставленные значения будут просто установлены.)", "将每一帧的指定值进行除法操作. (布尔/映射值将直接被设置.)", "각 프레임의 속성을 지정된 값으로 나눕니다.\n(불리언/매핑 값은 그대로 설정됩니다.)") \ X(TOOLTIP_DUPLICATE_ANIMATION, "Duplicate the selected animation(s).", "Duplica la(s) animacion(es) seleccionada.", "Дублировать выбранные анимации.", "拷贝所选动画.", "선택된 애니메이션을 복제합니다.") \ X(TOOLTIP_DURATION, "Change how long the frame lasts.", "Cambia la duracion de el último Frame.", "Изменить сколько длится кадр.", "更改此帧的时长.", "프레임의 지속 시간을 변경합니다.") \ X(TOOLTIP_EDITOR_ZOOM, "Change the zoom of the editor.", "Cambia el zoom del editor.", "Изменить масштаб редактора.", "更改编辑器的视角缩放.", "편집기의 줌을 변경합니다.") \ @@ -514,7 +552,7 @@ namespace anm2ed X(TOOLTIP_ITEM_THIS_ANIMATION, "The item will be placed into only this animation.", "El item sera aplicado solo a esta animacion.", "Предмет будет добавлен только в эту анимацию.", "该物体将仅放入此动画中。", "항목이 이 애니메이션에만 배치됩니다.") \ X(TOOLTIP_LOOP_ANIMATION, "Toggle the animation looping.", "Alterna el looping de la animacion.", "Переключить цикличное возпроизведение анимации.", "切换动画是否循环.", "애니메이션을 반복할지 정합니다.") \ X(TOOLTIP_MOVE_TOOL_SNAP, "In Animation Preview, the Move tool will snap the frame's position right to the cursor, instead of being moved at a distance.", "En la vista previa de la animacion, la herramienta Mover ajustara la posicion del Frame al puntero, en vez de moverse a distancia.", "В предпросмотре анимации, инструмент передвижения привяжет позицию кадра прямо к курсору, а не перемещает его на расстоянии.", "在动画预览时, 移动工具会使帧的位置直接吸附与光标上,而不是按距离移动.", "애니메이션 미리보기에서 이동 도구로 프레임을 이동시킬 때 프레임을 마우스 커서 바로 옆에 정렬되게 합니다.") \ - X(TOOLTIP_MULTIPLY_VALUES, "Multiply the specified values for each frame.\n(Boolean values will simply be set.)", "Multiplica los valores especificados para cada Frame.\n(Los valores booleanos seran ajustados).", "Умножить указанные значения для каждого кадра.\n(Булевы значения будут просто установлены.)", "将每一帧的指定值进行相乘操作. (布尔值将直接被设置.)", "각 프레임의 속성에 지정한 값을 곱합니다.\n(참/거짓 값은 그대로 설정됩니다.)") \ + X(TOOLTIP_MULTIPLY_VALUES, "Multiply the specified values for each frame.\n(Boolean/mapped values will simply be set.)", "Multiplica los valores especificados para cada Frame.\n(Los valores booleanos/mapeados simplemente se estableceran.)", "Умножить указанные значения для каждого кадра.\n(Булевы/сопоставленные значения будут просто установлены.)", "将每一帧的指定值进行相乘操作. (布尔/映射值将直接被设置.)", "각 프레임의 속성에 지정한 값을 곱합니다.\n(불리언/매핑 값은 그대로 설정됩니다.)") \ X(TOOLTIP_NEW_ITEM, "Create a new item.", "Crea un nuevo item.", "Создать новый предмет.", "创造一个新物品.", "새 항목을 만듭니다.") \ X(TOOLTIP_NO_UNUSED_ITEMS, "There are no unused items to use.", "No hay items sin utilizar para usar.", "Нет неиспользуемых предметов, которые использовать.", "没有可用的未使用物品.", "사용하지 않는 항목이 없습니다.") \ X(TOOLTIP_NULL_NAME, "Set the null's name.", "Ajusta el nombre del Null.", "Назвать этот нуль.", "更改Null的名字.", "Null의 이름을 설정합니다.") \ @@ -529,6 +567,10 @@ namespace anm2ed X(TOOLTIP_ONIONSKIN_TIME, "The onionskinned frames will be based on frame time.", "Los Frames de papel de cebolla seran basados al tiempo del Frame.", "Кадры оньонскина будут основаны на времени кадров.", "洋葱皮预览的帧会基于帧时间.", "프레임 비교를 프레임 시간을 기준으로 합니다.") \ X(TOOLTIP_ONLY_LAYERS_VISIBLE, "Only layers are visible. Press to show all items.", "Solo las capas estan visibles. Presiona para mostrar todos los items.", "Только слои видимы. Нажмите, чтобы показать все предметы.", "当前仅有动画层可见. 点击以显示所有物品.", "레이어만 표시합니다. 모두 보려면 누르세요.") \ X(TOOLTIP_OPEN_MERGE_POPUP, "Open merge popup.\nUse the shortcut to merge quickly.", "Abre el popup de combinacion.\n Usa este atajo para combinar mas rapido.", "Открыть всплывающее окно соединения.\nИспользуйте горячую клавишу, чтобы быстро выполнить слияние.", "打开合并弹窗。\n使用快捷键可快速合并。", "병합 팝업을 엽니다.\n단축키로 빠르게 병합하세요.") \ + X(TOOLTIP_MERGE_SPRITESHEETS_BOTTOM_LEFT, "The merged spritesheets will be joined at the bottom left of the previous spritesheet.", "Los spritesheets fusionados se unirán en la parte inferior izquierda del spritesheet anterior.", "Объединяемые спрайт-листы будут стыковаться в нижнем левом углу предыдущего спрайт-листа.", "合并的图集将拼接在前一个图集的左下方。", "병합된 스프라이트 시트는 이전 스프라이트 시트의 좌하단에 이어 붙습니다.") \ + X(TOOLTIP_MERGE_SPRITESHEETS_TOP_RIGHT, "The merged spritesheets will be joined at the top right of the previous spritesheet.", "Los spritesheets fusionados se unirán en la parte superior derecha del spritesheet anterior.", "Объединяемые спрайт-листы будут стыковаться в верхнем правом углу предыдущего спрайт-листа.", "合并的图集将拼接在前一个图集的右上方。", "병합된 스프라이트 시트는 이전 스프라이트 시트의 우상단에 이어 붙습니다.") \ + X(TOOLTIP_MERGE_MAKE_SPRITESHEET_REGIONS, "The respective spritesheets will be added as regions.", "Los spritesheets correspondientes se agregarán como regiones.", "Соответствующие спрайт-листы будут добавлены как регионы.", "对应图集将作为区域添加。", "해당 스프라이트 시트가 영역으로 추가됩니다.") \ + X(TOOLTIP_PACK_SPRITESHEET, "Pack the spritesheet by its regions and rebuild the texture.", "Empaqueta el spritesheet por sus regiones y reconstruye la textura.", "Упаковать спрайт-лист по его регионам и пересобрать текстуру.", "按区域打包图集并重建纹理。", "영역 기준으로 스프라이트 시트를 패킹하고 텍스처를 다시 만듭니다.") \ X(TOOLTIP_OUTPUT_PATH, "Set the output path or directory for the animation.", "Ajusta la ruta de salida o el directiorio de la animacion.", "Установить путь или директорию вывода для анимации.", "更改动画的输出路径/目录.", "애니메이션의 출력 경로 또는 디렉터리를 설정합니다.") \ X(TOOLTIP_OVERLAY, "Set an animation to be drawn over the current animation.", "Ajusta una animacion para ser dibujada sobre la animacion actual.", "Установить анимацию, которая будет выведена над текущей анимацией.", "设置一个当前动画的覆盖动画.", "현재 애니메이션 위에 그려질 애니메이션을 설정합니다.") \ X(TOOLTIP_OVERLAY_ALPHA, "Set the alpha of the overlayed animation.", "Ajusta el alpha de la animacion en Overlay", "Установить прозрачность наложенной анимации.", "更改覆盖动画的透明度.", "오버레이된 애니메이션의 불투명도를 설정합니다.") \ @@ -547,6 +589,7 @@ namespace anm2ed X(TOOLTIP_REMOVE_ANIMATION, "Remove the selected animation(s).", "Remueve la(s) animacion(es) seleccionada(s).", "Удалить выбранные анимации.", "去除所选动画.", "선택한 애니메이션을 제거합니다.") \ X(TOOLTIP_REMOVE_ITEMS, "Remove the selected item(s).", "Remueve el/los item(s) seleccionado(s).", "Удалить выбранные предметы.", "去除所选物品.", "선택한 항목을 제거합니다.") \ X(TOOLTIP_REMOVE_UNUSED_REGIONS, "Remove unused regions (i.e., ones not used by any frame in any animation.)", "Remueve regiones no utilizadas (es decir, aquellas no usadas por ningun frame en ninguna animacion.)", "Удалить неиспользуемые регионы (т. е. те, которые не используются ни одним кадром ни в одной анимации.)", "移除未使用的区域(即未被任何动画中的任何帧使用的区域。)", "사용되지 않는 영역(어떤 애니메이션의 어떤 프레임에서도 사용되지 않는 것)를 제거합니다.") \ + X(TOOLTIP_TRIM_REGIONS, "Trim region to non-transparent content.", "Recorta la región al contenido no transparente.", "Обрезать регион до непрозрачного содержимого.", "将区域裁剪到非透明内容。", "영역을 불투명 콘텐츠에 맞게 자릅니다.") \ X(TOOLTIP_REMOVE_UNUSED_EVENTS, "Remove unused events (i.e., ones not used by any trigger in any animation.)", "Remueve eventos no utilizados (i. e., aquellos no usados por algun trigger en ninguna animacion.)", "Удалить неиспользуемые события (т. е. события, которые не использует ни один триггер в ни одной анимации.)", "去除未使用的事件 (未被任何动画触发的事件.)", "사용되지 않는 이벤트(어떤 애니메이션의 트리거에서도 사용되지 않는 것)를 제거합니다.") \ X(TOOLTIP_REMOVE_UNUSED_LAYERS, "Remove unused layers (i.e., ones not used in any animation.)", "Remueve capas no utilizadas (i. e., aquellos no usados en ninguna animacion.)", "Удалить неиспользуемые слои (т. е. слои, которые не используются ни одной анимацией.)", "去除未使用的动画层 (未被任何动画使用的那些)", "사용되지 않는 레이어(어떤 애니메이션에서도 사용되지 않는 것)를 제거합니다.") \ X(TOOLTIP_REMOVE_UNUSED_NULLS, "Remove unused nulls (i.e., ones not used in any animation.)", "Remueve nulls no utilizados (i. e., aquellos no usados en ninguna animacion.)", "Удалить неиспользуемые нули (т. е. нули, которые не используются ни одной анимацией.)", "去除未使用的Null (未被任何动画使用的那些.)", "사용되지 않는 Null(어떤 애니메이션에서도 사용되지 않는 것)을 제거합니다.") \ @@ -578,7 +621,7 @@ namespace anm2ed X(TOOLTIP_SPRITESHEET_INVALID, "This spritesheet isn't valid!\nLoad an existing, valid texture.", "¡Este spritesheet no es valido!\nCarga una textura que exista y sea valida.", "Этот спрайт-лист невалиден!\nЗагрузите существующую, валидную текстуру.", "此图集无效!\n请加载一个已存在并有效的纹理/图集.", "이 스프라이트 시트는 유효하지 않습니다!\n유효한 텍스처를 불러오세요.") \ X(TOOLTIP_STACK_SIZE, "Set the maximum snapshot stack size of a document (i.e., how many undo/redos are preserved at a time).", "Ajusta el tamaño maximo del stack de snapshot de un documento (i. e., cuantos deshacer/rehacer se preservan a lo largo del tiempo.", "Установить максимальный размер стека снимков документа (т. е. количество отмен/повторов, сохраняемых одновременно).", "设置文件的快照栈的最大存储空间. (也就是最大可以存储多少撤销与重做)", "파일의 최대 스냅숏 스택 크기(즉 한 번에 보존되는 실행 취소/다시 실행 수)를 설정합니다.") \ X(TOOLTIP_START, "Set the starting time of the animation.", "Ajusta el tiempo de inicio de la animacion.", "Установить начальное время анимации.", "设置动画的起始时间.", "애니메이션의 시작 시간을 설정합니다.") \ - X(TOOLTIP_SUBTRACT_VALUES, "Subtract the specified values from each frame.\n(Boolean values will simply be set.)", "Subtrae los valores especificos de cada Frame.\n(Los valores booleanos seran ajustados.)", "Вычтить указанные значения из каждого кадра.\n(Булевы значения будут просто указаны.)", "将每一帧的指定值进行相减操作. (布尔值将直接被设置.)", "각 프레임의 속성에 지정한 값을 뺍니다.\n(참/거짓 값은 그냥 설정됩니다.)") \ + X(TOOLTIP_SUBTRACT_VALUES, "Subtract the specified values from each frame.\n(Boolean/mapped values will simply be set.)", "Subtrae los valores especificos de cada Frame.\n(Los valores booleanos/mapeados simplemente se estableceran.)", "Вычтить указанные значения из каждого кадра.\n(Булевы/сопоставленные значения будут просто установлены.)", "将每一帧的指定值进行相减操作. (布尔/映射值将直接被设置.)", "각 프레임의 속성에 지정한 값을 뺍니다.\n(불리언/매핑 값은 그대로 설정됩니다.)") \ X(TOOLTIP_TIMELINE_SHORTCUTS, "- Press {0} to decrement time.\n- Press {1} to increment time.\n- Press {2} to shorten the selected frame, by one frame.\n- Press {3} to extend the selected frame, by one frame.\n- Press {4} to go to the previous frame.\n- Press {5} to go to the next frame.\n- Click and hold on a frame while holding CTRL to change its duration.\n- Click and hold on a trigger to change its At Frame.\n- Hold Alt while clicking a non-trigger frame to toggle interpolation.", "- Presiona{0} para reducir el tiempo.\n- Presiona {1} para incrementar el tiempo.\n- Presiona {2} para acortar el Frame selecionado, por uno.\n- Presiona {3} para extender el Frame seleccionado, por uno.\n- Presiona {4} para ir al Frame anterior.\n- Presiona {5} para ir al siguiente Frame.\n- Haz click y mantiene en un Frame mientras apretas CTRL para cambiar su duracion.\n- Haz click y mantiene en un trigger para cambiar su \"En Frame\".\n- Manten Alt mientras haces click en un frame sin trigger para alternar la interpolacion.", "- Нажмите {0}, чтобы уменьшить время.\n- Нажмите {1}, чтобы увеличить время.\n- Нажмите {2}, чтобы укоротить выбранный кадр одной мерной единицей.\n- Нажмите {3}, чтобы продлить выбранный кадр на одну мерную единицу.\n- Нажмите {4}, чтобы перейти к предыдущему кадру.\n- Нажмите {5}, чтобы перейти к следующему кадру.\n- Удерживайте нажатой кнопку мыши по кадру, удерживая CTRL, чтобы изменить его длительность.\n- Нажмите и удерживайте кнопку мыши по триггеру, чтобы изменить параметр «На кадре».\n- Удерживайте Alt и нажмите по кадру, который не является триггером, чтобы переключить интерполяцию.", "- 按下 {0} 减少时间.\n- 按下 {1} 增加时间.\n- 按下 {2} 将所选帧缩短一帧.\n- 按下 {3} 将所选帧延长一帧.\n- 按下 {4} 跳到上一帧.\n- 按下 {5} 跳到下一帧.\n- 在按住 CTRL 的同时点击并按住某一帧以更改其持续时间.\n- 点击并按住触发器即可更改其触发帧.\n- 按住 Alt 并点击任何无触发器的帧以切换线性插值的使用.", "- {0} 키: 플레이헤드를 뒤로 보냅니다.\n- {1} 키: 플레이헤드를 앞으로 보냅니다.\n- {2} 키: 선택한 프레임을 한 프레임 단축합니다.\n- {3} 키: 선택한 프레임을 한 프레임 연장합니다.\n- {4} 키: 이전 프레임을 선택합니다.\n- {5} 키: 다음 프레임을 선택합니다.\n- CTRL 키를 누른 채 프레임을 클릭하고 드래그하면 프레임의 유지 시간을 변경할 수 있습니다.\n- 트리거를 클릭하고 드래그하면 트리거의 시작 프레임을 변경할 수 있습니다.\n- Alt 키를 누른 채 트리거를 제외한 프레임을 클릭하면 매끄럽게 연결 설정을 켤 수 있습니다.") \ X(TOOLTIP_TINT, "Change the tint of the frame.", "Cambia el matiz del Frame", "Изменить оттенок кадра.", "更改此帧的色调.", "프레임의 색조를 변경합니다.") \ X(TOOLTIP_TOOL_COLOR, "Selects the color to be used for drawing.\n(Spritesheet Editor only.)", "Selecciona el color que se usara para dibujar.\n(Solo en el Editor de Spritesheet.)", "Выбирает цвет, который будет использоваться для рисования.\n(Только в редакторе спрайт-листов.)", "选择用于绘画的颜色.\n(仅应用于图集编辑器.)", "그리기용으로 사용할 색을 선택합니다.\n(스프라이트 시트 편집기 전용)") \ @@ -605,6 +648,10 @@ namespace anm2ed X(TOOLTIP_UNUSED_ITEMS_HIDDEN, "Unused layers/nulls are hidden. Press to show them.", "Las capas/nulls no utilizados estan ocultos. Presiona para hacerlos visibles", "Неиспользуемые слои/нули скрыты. Нажмите, чтобы их показать.", "正在隐藏未使用的动画层/Null. 点击以显示它们.", "사용되지 않는 레이어/Null이 숨겨져 있습니다. 표시하려면 누르세요.") \ X(TOOLTIP_UNUSED_ITEMS_SHOWN, "Unused layers/nulls are shown. Press to hide them.", "Las capas/nulls no utilizados estan visibles. Presiona para ocultarlos", "Неиспользуемые слои/нули видимы. Нажмите, чтобы их скрыть.", "正在显示未使用的动画层/Null. 点击以隐藏它们.", "사용되지 않는 레이어/Null이 표시되어 있습니다. 숨기려면 누르세요.") \ X(TOOLTIP_USE_DEFAULT_SETTINGS, "Reset the settings to their defaults.", "Reinicia las configuraciones a sus predeterminados.", "Сбросить настройки на настройки по умолчанию.", "重设所有设置为默认.", "설정을 기본값으로 재설정합니다.") \ + X(TOOLTIP_WIZARD_GENERATE_ANIMATION_FROM_GRID, "Generate frames in the current selected animation item from grid values.", "Genera frames en el item de animacion seleccionado actualmente a partir de valores de la cuadricula.", "Создает кадры в текущем выбранном элементе анимации на основе значений сетки.", "根据网格值在当前选中的动画项目中生成帧。", "현재 선택된 애니메이션 항목에서 그리드 값으로 프레임을 생성합니다.") \ + X(TOOLTIP_WIZARD_CHANGE_ALL_FRAME_PROPERTIES, "Change certain properties of selected frames.\n(Note: this will also appear in Frame Properties when multiselecting frames.)", "Cambia ciertas propiedades de los frames seleccionados.\n(Nota: esto tambien aparecera en Propiedades de Frame al seleccionar multiples frames.)", "Изменить некоторые свойства выбранных кадров.\n(Примечание: это также появится в \"Свойствах кадра\" при выборе нескольких кадров.)", "更改所选帧的某些属性。\n(注意:在多选帧时,这也会显示在“帧属性”中。)", "선택한 프레임의 일부 속성을 변경합니다.\n(참고: 프레임을 여러 개 선택하면 프레임 속성에도 표시됩니다.)") \ + X(TOOLTIP_WIZARD_RENDER_ANIMATION, "Render the animation into a media format.\n(Note: this requires FFmpeg!)", "Renderiza la animacion a un formato multimedia.\n(Nota: esto requiere FFmpeg!)", "Рендерит анимацию в медиаформат.\n(Примечание: требуется FFmpeg!)", "将动画渲染为媒体格式。\n(注意:需要 FFmpeg!)", "애니메이션을 미디어 형식으로 렌더링합니다.\n(참고: FFmpeg가 필요합니다!)") \ + X(TOOLTIP_WIZARD_SCAN_AND_SET_REGIONS, "Match all regionless frames in all animations in the document to a region, if a matching region exists and matches the frame's relevant values.", "Empareja todos los frames sin region de todas las animaciones del documento con una region, si existe una region coincidente y coincide con los valores relevantes del frame.", "Сопоставляет все кадры без региона во всех анимациях документа с регионом, если существует подходящий регион и его релевантные значения совпадают со значениями кадра.", "将文档中所有动画里无区域的帧与区域进行匹配;若存在匹配区域且其相关值与帧一致,则进行设置。", "문서의 모든 애니메이션에서 영역이 없는 프레임을 영역과 매칭합니다. 일치하는 영역이 있고 프레임의 관련 값이 일치하면 설정합니다.") \ X(TOOLTIP_USE_EXISTING_ITEM, "Reuse an unused item instead of creating a new one.", "Reusa un item no utilizado en vez de crear uno nuevo.", "Использовать неиспользуемый предмет, а не создавать новый.", "重用未使用的物品,而不是创建新物品。", "새로 만들지 않고 사용되지 않는 항목을 재사용합니다.") \ X(TOOLTIP_VSYNC, "Toggle vertical sync; synchronizes program update rate with monitor refresh rate.", "Alterna la Sincronizacion Vertical; Sincroniza la tasa de actualizacion del programa con la tasa de refresco del monitor.", "Переключить вертикальную синхронизацию; синхронизирует частоту обновления программы с частотой обновления монитора.", "切换垂直同步; 同步程序的更新频率与屏幕的刷新频率.", "수직 동기화를 켜거나 끕니다. 프로그램의 업데이트 속도를 모니터의 새로고침 속도와 동기화합니다.") \ X(TOOLTIP_ZOOM_STEP, "When zooming in/out with mouse or shortcut, this value will be used.", "Cuando se haga zoom in/out con el mouse o atajo, este valor sera usado.", "При масштабировании мышью или горячей клавишей будет использоваться это значение.", "当通过鼠标或快捷键放大/缩小视图时, 此数值会被使用.", "마우스나 단축키로 확대/축소할 때 이 값이 사용됩니다.") diff --git a/src/resource/texture.cpp b/src/resource/texture.cpp index ac4a439..3ce0aa2 100644 --- a/src/resource/texture.cpp +++ b/src/resource/texture.cpp @@ -1,6 +1,7 @@ #include "texture.h" #include +#include #include #include #include @@ -146,6 +147,35 @@ namespace anm2ed::resource return false; } + Texture Texture::merge_append(const Texture& base, const Texture& append, bool isAppendRight) + { + if (base.size.x <= 0 || base.size.y <= 0) return append; + if (append.size.x <= 0 || append.size.y <= 0) return base; + if (base.pixels.empty()) return append; + if (append.pixels.empty()) return base; + + auto resultSize = + isAppendRight ? ivec2(base.size.x + append.size.x, std::max(base.size.y, append.size.y)) + : ivec2(std::max(base.size.x, append.size.x), base.size.y + append.size.y); + auto resultPixelsSize = (size_t)resultSize.x * (size_t)resultSize.y * CHANNELS; + std::vector resultPixels(resultPixelsSize, 0); + + auto blit = [&](const Texture& texture, ivec2 offset) + { + for (int y = 0; y < texture.size.y; y++) + { + auto src = (size_t)y * (size_t)texture.size.x * CHANNELS; + auto dst = ((size_t)(offset.y + y) * (size_t)resultSize.x + (size_t)offset.x) * CHANNELS; + std::memcpy(resultPixels.data() + dst, texture.pixels.data() + src, (size_t)texture.size.x * CHANNELS); + } + }; + + blit(base, {0, 0}); + blit(append, isAppendRight ? ivec2(base.size.x, 0) : ivec2(0, base.size.y)); + + return Texture(resultPixels.data(), resultSize); + } + vec4 Texture::pixel_read(vec2 position) const { if (pixels.size() < CHANNELS || size.x <= 0 || size.y <= 0) return vec4(0.0f); diff --git a/src/resource/texture.h b/src/resource/texture.h index 842c8a8..30f41db 100644 --- a/src/resource/texture.h +++ b/src/resource/texture.h @@ -39,6 +39,7 @@ namespace anm2ed::resource Texture(const std::filesystem::path&); bool write_png(const std::filesystem::path&); static bool write_pixels_png(const std::filesystem::path&, glm::ivec2, const uint8_t*); + static Texture merge_append(const Texture&, const Texture&, bool); void pixel_set(glm::ivec2, glm::vec4); void pixel_line(glm::ivec2, glm::ivec2, glm::vec4); }; diff --git a/src/settings.h b/src/settings.h index 15f6df2..4e8efdc 100644 --- a/src/settings.h +++ b/src/settings.h @@ -6,6 +6,7 @@ #include #include "anm2/anm2_type.h" +#include "origin.h" #include "render.h" #include "strings.h" #include "types.h" @@ -60,6 +61,7 @@ namespace anm2ed X(FILE_IS_AUTOSAVE, fileIsAutosave, STRING_UNDEFINED, BOOL, true) \ X(FILE_IS_WARN_OVERWRITE, fileIsWarnOverwrite, STRING_UNDEFINED, BOOL, true) \ X(FILE_SNAPSHOT_STACK_SIZE, fileSnapshotStackSize, STRING_UNDEFINED, INT, 50) \ + X(FILE_COMPATIBILITY, fileCompatibility, STRING_UNDEFINED, INT, anm2::ANM2ED) \ \ X(KEYBOARD_REPEAT_DELAY, keyboardRepeatDelay, STRING_UNDEFINED, FLOAT, 0.300f) \ X(KEYBOARD_REPEAT_RATE, keyboardRepeatRate, STRING_UNDEFINED, FLOAT, 0.050f) \ @@ -150,6 +152,9 @@ namespace anm2ed \ X(MERGE_TYPE, mergeType, STRING_UNDEFINED, INT, 0) \ X(MERGE_IS_DELETE_ANIMATIONS_AFTER, mergeIsDeleteAnimationsAfter, STRING_UNDEFINED, BOOL, false) \ + X(MERGE_SPRITESHEETS_ORIGIN, mergeSpritesheetsOrigin, STRING_UNDEFINED, INT, anm2::APPEND_RIGHT) \ + X(MERGE_SPRITESHEETS_IS_MAKE_REGIONS, mergeSpritesheetsIsMakeRegions, STRING_UNDEFINED, BOOL, true) \ + X(MERGE_SPRITESHEETS_REGION_ORIGIN, mergeSpritesheetsRegionOrigin, STRING_UNDEFINED, INT, origin::TOP_LEFT) \ \ X(BAKE_INTERVAL, bakeInterval, STRING_UNDEFINED, INT, 1) \ X(BAKE_IS_ROUND_SCALE, bakeIsRoundScale, STRING_UNDEFINED, BOOL, true) \ @@ -202,6 +207,8 @@ namespace anm2ed X(SHORTCUT_RENAME, shortcutRename, SHORTCUT_STRING_RENAME, STRING, "F2") \ X(SHORTCUT_DEFAULT, shortcutDefault, SHORTCUT_STRING_DEFAULT, STRING, "Home") \ X(SHORTCUT_MERGE, shortcutMerge, SHORTCUT_STRING_MERGE, STRING, "Ctrl+E") \ + X(SHORTCUT_CONFIRM, shortcutConfirm, SHORTCUT_STRING_CONFIRM, STRING, "Enter") \ + X(SHORTCUT_CANCEL, shortcutCancel, SHORTCUT_STRING_CANCEL, STRING, "Escape") \ /* Tools */ \ X(SHORTCUT_PAN, shortcutPan, SHORTCUT_STRING_PAN, STRING, "P") \ X(SHORTCUT_MOVE, shortcutMove, SHORTCUT_STRING_MOVE, STRING, "V") \ @@ -238,7 +245,7 @@ namespace anm2ed /* Symbol / Name / String / Type / Default */ \ X(WINDOW_ANIMATIONS, windowIsAnimations, LABEL_ANIMATIONS_WINDOW, BOOL, true) \ X(WINDOW_ANIMATION_PREVIEW, windowIsAnimationPreview, LABEL_ANIMATION_PREVIEW_WINDOW, BOOL, true) \ - X(WINDOW_REGIONS, windowIsRegions, LABEL_REGIONS_WINDOW, BOOL, true) \ + X(WINDOW_REGIONS, windowIsRegions, LABEL_REGIONS_WINDOW, BOOL, true) \ X(WINDOW_EVENTS, windowIsEvents, LABEL_EVENTS_WINDOW, BOOL, true) \ X(WINDOW_FRAME_PROPERTIES, windowIsFrameProperties, LABEL_FRAME_PROPERTIES_WINDOW, BOOL, true) \ X(WINDOW_LAYERS, windowIsLayers, LABEL_LAYERS_WINDOW, BOOL, true) \ diff --git a/src/util/origin.h b/src/util/origin.h new file mode 100644 index 0000000..fa3481a --- /dev/null +++ b/src/util/origin.h @@ -0,0 +1,11 @@ +#pragma once + +namespace anm2ed::origin +{ + enum Type + { + TOP_LEFT, + ORIGIN_CENTER, + CUSTOM + }; +} diff --git a/workshop/metadata.xml b/workshop/metadata.xml index fadfc93..5e4e651 100644 --- a/workshop/metadata.xml +++ b/workshop/metadata.xml @@ -51,6 +51,6 @@ Alternatively, if you have subscribed to the mod, you can find the latest releas [h3]Happy animating![/h3] [img]https://files.catbox.moe/4auc1c.gif[/img] - 2.13 + 2.14 Public