#include "document.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "log.hpp" #include "manager.hpp" #include "path.hpp" #include "strings.hpp" #include "toast.hpp" #include "working_directory.hpp" using namespace anm2ed::imgui; using namespace anm2ed::types; using namespace anm2ed::util; using namespace glm; namespace anm2ed::document { Options save_options_get(Compatibility compatibility, bool isBakeSpecialInterpolatedFrames, bool isRoundScale, bool isRoundRotation) { return {.compatibility = compatibility, .isBakeSpecialInterpolatedFrames = isBakeSpecialInterpolatedFrames, .isRoundScale = isRoundScale, .isRoundRotation = isRoundRotation}; } ItemType item_type_get(int type) { return static_cast(type); } int animation_count_get(const Anm2& data) { int count{}; if (auto animations = data.element_get(ElementType::ANIMATIONS)) for (auto& animation : animations->children) if (animation.type == ElementType::ANIMATION) ++count; return count; } int frame_count_get(const Element& item) { auto frameType = item.type == ElementType::TRIGGERS ? ElementType::TRIGGER : ElementType::FRAME; int count{}; for (auto& frame : item.children) if (frame.type == frameType) ++count; return count; } std::vector animation_labels_get(const Anm2& data) { std::vector labels{"None"}; if (auto animations = data.element_get(ElementType::ANIMATIONS)) for (auto& animation : animations->children) if (animation.type == ElementType::ANIMATION) labels.emplace_back(animation.name); return labels; } std::vector element_name_labels_get(const Element* container, ElementType type, bool isNone) { std::vector labels{}; if (isNone) labels.emplace_back(localize.get(BASIC_NONE)); if (!container) return labels; for (auto& element : container->children) if (element.type == type) labels.emplace_back(element.name); return labels; } std::vector element_ids_get(const Element* container, ElementType type, bool isNone) { std::vector ids{}; if (isNone) ids.emplace_back(-1); if (!container) return ids; for (auto& element : container->children) if (element.type == type) ids.emplace_back(element.id); return ids; } std::vector spritesheet_labels_get(const Anm2& data) { std::vector labels{}; if (auto spritesheets = data.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) if (spritesheet.type == ElementType::SPRITESHEET) { auto pathString = path::to_utf8(spritesheet.path); labels.emplace_back( std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(spritesheet.id, pathString))); } return labels; } std::vector spritesheet_ids_get(const Anm2& data) { std::vector ids{}; if (auto spritesheets = data.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) if (spritesheet.type == ElementType::SPRITESHEET) ids.emplace_back(spritesheet.id); return ids; } std::vector sound_labels_get(const Anm2& data) { std::vector labels{localize.get(BASIC_NONE)}; if (auto sounds = data.element_get(ElementType::SOUNDS)) for (auto& sound : sounds->children) if (sound.type == ElementType::SOUND_ELEMENT) { auto pathString = path::to_utf8(sound.path); labels.emplace_back(std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(sound.id, pathString))); } return labels; } std::vector sound_ids_get(const Anm2& data) { std::vector ids{-1}; if (auto sounds = data.element_get(ElementType::SOUNDS)) for (auto& sound : sounds->children) if (sound.type == ElementType::SOUND_ELEMENT) ids.emplace_back(sound.id); return ids; } uint64_t spritesheet_hash_get(const Element& spritesheet, const resource::Texture* texture) { auto hash_combine = [](std::size_t& seed, std::size_t value) { seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); }; std::size_t seed{}; hash_combine(seed, std::hash{}(texture ? texture->size.x : 0)); hash_combine(seed, std::hash{}(texture ? texture->size.y : 0)); hash_combine(seed, std::hash{}(texture ? texture->channels : 0)); hash_combine(seed, std::hash{}(texture ? texture->filter : 0)); hash_combine(seed, std::hash{}(path::to_utf8(spritesheet.path))); if (texture && !texture->pixels.empty()) { std::string_view bytes(reinterpret_cast(texture->pixels.data()), texture->pixels.size()); hash_combine(seed, std::hash{}(bytes)); } else hash_combine(seed, 0); return static_cast(seed); } void restored_snapshot_sanitize(Document& document) { auto& reference = document.reference; auto& selection = document.frames.selection; auto& frameReferences = document.frames.references; auto& itemReferences = document.items.references; auto animationCount = animation_count_get(document.anm2); if (animationCount <= 0) { reference = {}; selection.clear(); frameReferences.clear(); itemReferences.clear(); document.frameTime = 0.0f; return; } if (reference.animationIndex < 0 || reference.animationIndex >= animationCount) reference.animationIndex = std::clamp(reference.animationIndex, 0, animationCount - 1); auto item = document.anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID); if (!item) { reference.itemType = (int)ItemType::ROOT; reference.itemID = -1; item = document.anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID); } if (!item) { reference.frameIndex = -1; selection.clear(); frameReferences.clear(); document.frameTime = 0.0f; return; } for (auto it = itemReferences.begin(); it != itemReferences.end();) { auto item = document.anm2.element_get(it->animationIndex, item_type_get(it->itemType), it->itemID); if (!item) it = itemReferences.erase(it); else ++it; } for (auto it = frameReferences.begin(); it != frameReferences.end();) { auto item = document.anm2.element_get(it->animationIndex, item_type_get(it->itemType), it->itemID); auto frameCount = item ? frame_count_get(*item) : 0; if (!item || it->frameIndex < 0 || it->frameIndex >= frameCount) it = frameReferences.erase(it); else ++it; } auto frameCount = frame_count_get(*item); for (auto it = selection.begin(); it != selection.end();) { if (*it < 0 || *it >= frameCount) it = selection.erase(it); else ++it; } if (frameCount <= 0) { reference.frameIndex = -1; frameReferences.clear(); document.frameTime = 0.0f; return; } if (reference.frameIndex < 0 || reference.frameIndex >= frameCount) reference.frameIndex = selection.empty() ? std::clamp(reference.frameIndex, 0, frameCount - 1) : *selection.begin(); if (frameReferences.empty()) for (auto frameIndex : selection) frameReferences.insert({reference.animationIndex, reference.itemType, reference.itemID, frameIndex}); selection.clear(); for (const auto& frameReference : frameReferences) if (frameReference.animationIndex == reference.animationIndex && frameReference.itemType == reference.itemType && frameReference.itemID == reference.itemID) selection.insert(frameReference.frameIndex); document.frameTime = frame_time_from_index_get(*item, reference.frameIndex); } } namespace anm2ed { Document::Document(const std::filesystem::path& path, bool isNew, std::string* errorString) { if (isNew) { anm2 = Anm2(); if (!save(path, errorString)) { isValid = false; this->path.clear(); return; } } else { anm2 = Anm2(path, errorString); if (!anm2.isValid) { isValid = false; this->path.clear(); return; } } this->path = path; isValid = anm2.isValid; clean(); change(Document::ALL); } Document::Document(Document&& other) noexcept : path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current), playback(current.playback), animation(current.animation), event(current.event), frames(current.frames), items(current.items), layer(current.layer), merge(current.merge), null(current.null), region(current.region), sound(current.sound), spritesheet(current.spritesheet), textures(current.textures), sounds(current.sounds), anm2(current.anm2), reference(current.reference), frameTime(current.frameTime), message(current.message), regionBySpritesheet(std::move(other.regionBySpritesheet)), changeAllFramePropertiesRegionId(other.changeAllFramePropertiesRegionId), previewZoom(other.previewZoom), previewPan(other.previewPan), editorPan(other.editorPan), editorZoom(other.editorZoom), overlayIndex(other.overlayIndex), hash(other.hash), saveHash(other.saveHash), autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isValid(other.isValid), isOpen(other.isOpen), isForceDirty(other.isForceDirty), spritesheetHashes(std::move(other.spritesheetHashes)), spritesheetSaveHashes(std::move(other.spritesheetSaveHashes)), texturePaths(std::move(other.texturePaths)), soundPaths(std::move(other.soundPaths)), isAnimationPreviewSet(other.isAnimationPreviewSet), isSpritesheetEditorSet(other.isSpritesheetEditorSet) { } Document& Document::operator=(Document&& other) noexcept { if (this != &other) this->~Document(); new (this) Document(std::move(other)); return *this; } bool Document::save(const std::filesystem::path& path, std::string* errorString, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) { auto absolutePath = !path.empty() ? path : this->path; auto absolutePathUtf8 = path::to_utf8(absolutePath); if (anm2.save(absolutePath, errorString, document::save_options_get(compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation))) { this->path = absolutePath; toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePathUtf8))); logger.info( std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), std::make_format_args(absolutePathUtf8))); clean(); return true; } else if (errorString) { toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED), std::make_format_args(absolutePathUtf8, *errorString))); logger.error(std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED, anm2ed::ENGLISH), std::make_format_args(absolutePathUtf8, *errorString))); } return false; } std::filesystem::path Document::autosave_path_get() { auto fileNameUtf8 = path::to_utf8(filename_get()); auto autosaveNameUtf8 = "." + fileNameUtf8 + ".autosave"; return directory_get() / path::from_utf8(autosaveNameUtf8); } std::filesystem::path Document::path_from_autosave_get(const std::filesystem::path& path) { auto fileName = path::to_utf8(path.filename()); if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin()); constexpr std::string_view autosaveExtension = ".autosave"; if (fileName.ends_with(autosaveExtension)) fileName.erase(fileName.size() - autosaveExtension.size()); auto restorePath = path.parent_path() / std::filesystem::path(std::u8string(fileName.begin(), fileName.end())); return restorePath; } bool Document::autosave(std::string* errorString, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation) { auto autosavePath = autosave_path_get(); auto autosavePathUtf8 = path::to_utf8(autosavePath); if (anm2.save(autosavePath, errorString, document::save_options_get(compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation))) { autosaveHash = hash; lastAutosaveTime = 0.0f; toasts.push(localize.get(TOAST_AUTOSAVING)); logger.info(localize.get(TOAST_AUTOSAVING, anm2ed::ENGLISH)); logger.info(std::format("Autosaved document to: {}", autosavePathUtf8)); return true; } else if (errorString) { toasts.push( std::vformat(localize.get(TOAST_AUTOSAVE_FAILED), std::make_format_args(autosavePathUtf8, *errorString))); logger.error(std::vformat(localize.get(TOAST_AUTOSAVE_FAILED, anm2ed::ENGLISH), std::make_format_args(autosavePathUtf8, *errorString))); } return false; } void Document::anm2_change(ChangeType type) { change(type); } void Document::texture_change(int id) { auto texture = texture_get(id); if (!texture || !anm2.element_get(ElementType::SPRITESHEET, id)) return; change(SPRITESHEETS); } bool Document::texture_reload(int id) { auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); if (!spritesheet) return false; util::WorkingDirectory workingDirectory(directory_get()); textures[id] = resource::Texture(spritesheet->path); texturePaths[id] = spritesheet->path; return true; } bool Document::sound_reload(int id) { auto sound = anm2.element_get(ElementType::SOUND_ELEMENT, id); if (!sound) return false; util::WorkingDirectory workingDirectory(directory_get()); sounds[id] = resource::Audio(sound->path); soundPaths[id] = sound->path; return true; } void Document::assets_sync(ChangeType type) { if (type == ALL || type == SPRITESHEETS || type == TEXTURES) { std::set validIds{}; util::WorkingDirectory workingDirectory(directory_get()); if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) { if (spritesheet.type != ElementType::SPRITESHEET) continue; validIds.insert(spritesheet.id); auto isReload = !textures.contains(spritesheet.id) || !texturePaths.contains(spritesheet.id) || texturePaths.at(spritesheet.id) != spritesheet.path; if (isReload) { textures[spritesheet.id] = resource::Texture(spritesheet.path); texturePaths[spritesheet.id] = spritesheet.path; } } for (auto it = textures.begin(); it != textures.end();) { if (!validIds.contains(it->first)) { texturePaths.erase(it->first); it = textures.erase(it); } else ++it; } } if (type == ALL || type == SOUNDS) { std::set validIds{}; util::WorkingDirectory workingDirectory(directory_get()); if (auto soundItems = anm2.element_get(ElementType::SOUNDS)) for (auto& sound : soundItems->children) { if (sound.type != ElementType::SOUND_ELEMENT) continue; validIds.insert(sound.id); auto isReload = !sounds.contains(sound.id) || !soundPaths.contains(sound.id) || soundPaths.at(sound.id) != sound.path; if (isReload) { sounds[sound.id] = resource::Audio(sound.path); soundPaths[sound.id] = sound.path; } } for (auto it = sounds.begin(); it != sounds.end();) { if (!validIds.contains(it->first)) { soundPaths.erase(it->first); it = sounds.erase(it); } else ++it; } } } resource::Texture* Document::texture_get(int id) { auto it = textures.find(id); return it == textures.end() ? nullptr : &it->second; } const resource::Texture* Document::texture_get(int id) const { auto it = textures.find(id); return it == textures.end() ? nullptr : &it->second; } resource::Audio* Document::sound_get(int id) { auto it = sounds.find(id); return it == sounds.end() ? nullptr : &it->second; } const resource::Audio* Document::sound_get(int id) const { auto it = sounds.find(id); return it == sounds.end() ? nullptr : &it->second; } bool Document::regions_trim(int spritesheetId, const std::set& ids) { auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, spritesheetId); auto texture = texture_get(spritesheetId); if (!spritesheet || !texture || !texture->is_valid() || texture->pixels.empty() || ids.empty()) return false; bool isChanged{}; for (auto id : ids) { auto region = element_child_id_get(*spritesheet, ElementType::REGION, id); if (!region) continue; 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 = ((std::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) continue; auto previousCrop = region->crop; region->crop = newCrop; region->size = newSize; if (region->origin == Origin::TOP_LEFT) region->pivot = {}; else if (region->origin == Origin::CENTER) region->pivot = {static_cast(region->size.x / 2.0f), static_cast(region->size.y / 2.0f)}; else region->pivot -= region->crop - previousCrop; isChanged = true; } return isChanged; } bool Document::spritesheet_pack(int id, int padding) { struct RectI { int x{}; int y{}; int w{}; int h{}; }; struct PackItem { int regionId{-1}; int srcX{}; int srcY{}; int width{}; int height{}; int packWidth{}; int packHeight{}; }; class MaxRectsPacker { int width{}; int height{}; std::vector freeRects{}; static bool intersects(const RectI& a, const RectI& b) { return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y); } static bool contains(const RectI& a, const RectI& b) { return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h; } void split_free_rects(const RectI& used) { std::vector next{}; next.reserve(freeRects.size() * 2); for (auto& free : freeRects) { if (!intersects(free, used)) { next.push_back(free); continue; } if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h}); if (used.x + used.w < free.x + free.w) next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h}); if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y}); if (used.y + used.h < free.y + free.h) next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)}); } freeRects = std::move(next); } void prune_free_rects() { for (int i = 0; i < (int)freeRects.size(); ++i) { if (freeRects[i].w <= 0 || freeRects[i].h <= 0) { freeRects.erase(freeRects.begin() + i--); continue; } for (int j = i + 1; j < (int)freeRects.size();) { if (contains(freeRects[i], freeRects[j])) freeRects.erase(freeRects.begin() + j); else if (contains(freeRects[j], freeRects[i])) { freeRects.erase(freeRects.begin() + i--); 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 isFound{}; 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}; isFound = true; } } if (!isFound) 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{}; std::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 += (std::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(); std::unordered_map bestRects{}; int bestWidth{}; int bestHeight{}; 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{}; 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; }; auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); auto texture = texture_get(id); if (!spritesheet || !texture || !texture->is_valid() || texture->pixels.empty()) return false; auto packingPadding = std::max(0, padding); std::vector items{}; for (auto& region : spritesheet->children) { if (region.type != ElementType::REGION) continue; 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)); items.push_back({region.id, minPoint.x, minPoint.y, size.x, size.y, size.x + packingPadding * 2, size.y + packingPadding * 2}); } if (items.empty()) return false; 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; std::vector packedPixels((std::size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0); for (auto& item : items) { if (!packedRects.contains(item.regionId)) continue; auto destinationRect = packedRects.at(item.regionId); for (int y = 0; y < item.height; ++y) for (int x = 0; x < item.width; ++x) { int sourceX = item.srcX + x; int sourceY = item.srcY + y; int destinationX = destinationRect.x + packingPadding + x; int destinationY = destinationRect.y + packingPadding + y; if (sourceX < 0 || sourceY < 0 || sourceX >= texture->size.x || sourceY >= texture->size.y) continue; if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight) continue; auto sourceIndex = ((std::size_t)sourceY * texture->size.x + sourceX) * resource::texture::CHANNELS; auto destinationIndex = ((std::size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS; std::copy_n(texture->pixels.data() + sourceIndex, resource::texture::CHANNELS, packedPixels.data() + destinationIndex); } } textures[id] = resource::Texture(packedPixels.data(), {packedWidth, packedHeight}); for (auto& region : spritesheet->children) if (region.type == ElementType::REGION && packedRects.contains(region.id)) { auto& rect = packedRects.at(region.id); region.crop = {rect.x + packingPadding, rect.y + packingPadding}; } assets_sync(SPRITESHEETS); return true; } bool Document::spritesheets_merge(const std::set& ids, bool isAppendRight, bool isMakeRegions, bool isMakePrimaryRegion, origin::Type regionOrigin) { if (ids.size() < 2) return false; auto baseId = *ids.begin(); auto base = anm2.element_get(ElementType::SPRITESHEET, baseId); auto baseTexture = texture_get(baseId); if (!base || !baseTexture || !baseTexture->is_valid()) return false; for (auto id : ids) if (!anm2.element_get(ElementType::SPRITESHEET, id) || !texture_get(id) || !texture_get(id)->is_valid()) return false; auto origin = regionOrigin == origin::ORIGIN_CENTER ? Origin::CENTER : Origin::TOP_LEFT; auto baseTextureSize = baseTexture->size; auto mergedTexture = *baseTexture; std::unordered_map offsets{{baseId, {}}}; for (auto id : ids) { if (id == baseId) continue; auto texture = texture_get(id); offsets[id] = isAppendRight ? glm::ivec2(mergedTexture.size.x, 0) : glm::ivec2(0, mergedTexture.size.y); mergedTexture = resource::Texture::merge_append(mergedTexture, *texture, isAppendRight); } textures[baseId] = std::move(mergedTexture); std::unordered_map> regionIdMap{}; if (isMakeRegions) { auto add_location_region = [&](int sourceId, glm::ivec2 crop, glm::ivec2 size) { auto source = anm2.element_get(ElementType::SPRITESHEET, sourceId); if (!source) return; auto region = element_make(ElementType::REGION); region.id = element_child_next_id_get(*base, ElementType::REGION); auto stem = path::to_utf8(source->path.stem()); region.name = stem.empty() ? std::format("#{}", sourceId) : stem; region.crop = crop; region.size = size; region.pivot = origin == Origin::CENTER ? glm::vec2(size) * 0.5f : glm::vec2(); region.origin = origin; base->children.push_back(region); }; if (isMakePrimaryRegion) add_location_region(baseId, {}, baseTextureSize); for (auto id : ids) { if (id == baseId) continue; auto source = anm2.element_get(ElementType::SPRITESHEET, id); auto sourceTexture = texture_get(id); auto sheetOffset = offsets.at(id); add_location_region(id, sheetOffset, sourceTexture->size); for (auto& sourceRegion : source->children) { if (sourceRegion.type != ElementType::REGION) continue; auto destinationRegion = sourceRegion; destinationRegion.id = element_child_next_id_get(*base, ElementType::REGION); destinationRegion.crop += sheetOffset; base->children.push_back(destinationRegion); regionIdMap[id][sourceRegion.id] = destinationRegion.id; } } } std::unordered_map layerSpritesheetBefore{}; if (auto layers = anm2.element_get(ElementType::LAYERS)) for (auto& layer : layers->children) if (layer.type == ElementType::LAYER_ELEMENT && ids.contains(layer.spritesheetId)) { layerSpritesheetBefore[layer.id] = layer.spritesheetId; layer.spritesheetId = baseId; } if (auto animations = anm2.element_get(ElementType::ANIMATIONS)) for (auto& animation : animations->children) { if (animation.type != ElementType::ANIMATION) continue; auto layerAnimations = element_child_first_get(animation, ElementType::LAYER_ANIMATIONS); if (!layerAnimations) continue; auto layer_animation_update = [&](auto&& self, Element& layerAnimation) -> void { if (layerAnimation.type == ElementType::GROUP) { for (auto& child : layerAnimation.children) self(self, child); return; } if (layerAnimation.type != ElementType::LAYER_ANIMATION || !layerSpritesheetBefore.contains(layerAnimation.layerId)) return; auto sourceSpritesheetId = layerSpritesheetBefore.at(layerAnimation.layerId); if (sourceSpritesheetId == baseId) return; for (auto& frame : layerAnimation.children) { if (frame.type != ElementType::FRAME || 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& layerAnimation : layerAnimations->children) layer_animation_update(layer_animation_update, layerAnimation); } auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS); if (!spritesheets) return false; for (auto id : ids) if (id != baseId) { element_child_id_erase(*spritesheets, ElementType::SPRITESHEET, id); textures.erase(id); } assets_sync(ALL); return true; } void Document::scan_and_set_regions() { auto animations = anm2.element_get(ElementType::ANIMATIONS); if (animations) { for (auto& animation : animations->children) { if (animation.type != ElementType::ANIMATION) continue; auto layerAnimations = element_child_first_get(animation, ElementType::LAYER_ANIMATIONS); if (!layerAnimations) continue; auto layer_animation_update = [&](auto&& self, Element& layerAnimation) -> void { if (layerAnimation.type == ElementType::GROUP) { for (auto& child : layerAnimation.children) self(self, child); return; } if (layerAnimation.type != ElementType::LAYER_ANIMATION) return; auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, layerAnimation.layerId); auto spritesheet = layer ? anm2.element_get(ElementType::SPRITESHEET, layer->spritesheetId) : nullptr; if (!spritesheet) return; for (auto& frame : layerAnimation.children) { if (frame.type != ElementType::FRAME || frame.regionId != -1) continue; auto frameCrop = glm::ivec2(frame.crop); auto frameSize = glm::ivec2(frame.size); auto framePivot = glm::ivec2(frame.pivot); for (const auto& region : spritesheet->children) if (region.type == ElementType::REGION && glm::ivec2(region.crop) == frameCrop && glm::ivec2(region.size) == frameSize && glm::ivec2(region.pivot) == framePivot) { frame.regionId = region.id; break; } } }; for (auto& layerAnimation : layerAnimations->children) layer_animation_update(layer_animation_update, layerAnimation); } } } bool Document::file_merge(const std::filesystem::path& path) { Anm2 source(path); if (!source.isValid) return false; auto remap_path = [&](const std::filesystem::path& original) -> std::filesystem::path { if (directory_get().empty()) return original; std::error_code ec{}; std::filesystem::path absolute{}; bool isAbsolute{}; if (!original.empty()) { if (original.is_absolute()) { absolute = original; isAbsolute = true; } else { absolute = std::filesystem::weakly_canonical(path.parent_path() / original, ec); if (ec) { ec.clear(); absolute = path.parent_path() / original; } isAbsolute = true; } } if (!isAbsolute) return original; auto relative = std::filesystem::relative(absolute, directory_get(), ec); if (!ec) return relative; return original.empty() ? absolute : original; }; auto remap_id = [](const std::unordered_map& remap, int id) { auto it = remap.find(id); return it == remap.end() ? -1 : it->second; }; auto find_by_name = [](Element& container, ElementType type, const std::string& name) { for (auto& child : container.children) if (child.type == type && child.name == name) return &child; return (Element*)nullptr; }; auto child_set = [](Element& container, Element child, ElementType type) { if (auto existing = element_child_first_get(container, type)) *existing = std::move(child); else container.children.push_back(std::move(child)); }; auto track_find = [](Element& container, const Element& source) { auto find = [](auto&& self, Element& parent, const Element& source) -> Element* { for (auto& track : parent.children) { if (track.type == ElementType::GROUP) if (auto result = self(self, track, source)) return result; if (track.type != source.type) continue; if (track.type == ElementType::LAYER_ANIMATION && track.layerId == source.layerId) return &track; if (track.type == ElementType::NULL_ANIMATION && track.nullId == source.nullId) return &track; } return (Element*)nullptr; }; return find(find, container, source); }; auto track_container_get = [](Element& animation, ElementType type) { if (auto container = element_child_first_get(animation, type)) return container; animation.children.push_back(element_make(type)); return &animation.children.back(); }; std::unordered_map spritesheetRemap{}; std::unordered_map layerRemap{}; std::unordered_map nullRemap{}; std::unordered_map eventRemap{}; std::unordered_map soundRemap{}; if (auto sourceSpritesheets = source.element_get(ElementType::SPRITESHEETS)) if (auto destinationSpritesheets = anm2.element_get(ElementType::SPRITESHEETS)) for (auto spritesheet : sourceSpritesheets->children) { if (spritesheet.type != ElementType::SPRITESHEET) continue; auto sourceId = spritesheet.id; spritesheet.id = element_child_next_id_get(*destinationSpritesheets, ElementType::SPRITESHEET); spritesheet.path = remap_path(spritesheet.path); destinationSpritesheets->children.push_back(spritesheet); spritesheetRemap[sourceId] = spritesheet.id; } if (auto sourceSounds = source.element_get(ElementType::SOUNDS)) if (auto destinationSounds = anm2.element_get(ElementType::SOUNDS)) for (auto sound : sourceSounds->children) { if (sound.type != ElementType::SOUND_ELEMENT) continue; auto sourceId = sound.id; sound.path = remap_path(sound.path); int destinationId{-1}; for (auto& existing : destinationSounds->children) if (existing.type == ElementType::SOUND_ELEMENT && existing.path == sound.path) { destinationId = existing.id; sound.id = destinationId; existing = sound; break; } if (destinationId == -1) { destinationId = element_child_next_id_get(*destinationSounds, ElementType::SOUND_ELEMENT); sound.id = destinationId; destinationSounds->children.push_back(sound); } soundRemap[sourceId] = destinationId; } if (auto sourceLayers = source.element_get(ElementType::LAYERS)) if (auto destinationLayers = anm2.element_get(ElementType::LAYERS)) for (auto layer : sourceLayers->children) { if (layer.type != ElementType::LAYER_ELEMENT) continue; auto sourceId = layer.id; layer.spritesheetId = remap_id(spritesheetRemap, layer.spritesheetId); if (auto existing = find_by_name(*destinationLayers, ElementType::LAYER_ELEMENT, layer.name)) { layer.id = existing->id; *existing = layer; } else { layer.id = element_child_next_id_get(*destinationLayers, ElementType::LAYER_ELEMENT); destinationLayers->children.push_back(layer); } layerRemap[sourceId] = layer.id; } if (auto sourceNulls = source.element_get(ElementType::NULLS)) if (auto destinationNulls = anm2.element_get(ElementType::NULLS)) for (auto null : sourceNulls->children) { if (null.type != ElementType::NULL_ELEMENT) continue; auto sourceId = null.id; if (auto existing = find_by_name(*destinationNulls, ElementType::NULL_ELEMENT, null.name)) { null.id = existing->id; *existing = null; } else { null.id = element_child_next_id_get(*destinationNulls, ElementType::NULL_ELEMENT); destinationNulls->children.push_back(null); } nullRemap[sourceId] = null.id; } if (auto sourceEvents = source.element_get(ElementType::EVENTS)) if (auto destinationEvents = anm2.element_get(ElementType::EVENTS)) for (auto event : sourceEvents->children) { if (event.type != ElementType::EVENT_ELEMENT) continue; auto sourceId = event.id; if (auto existing = find_by_name(*destinationEvents, ElementType::EVENT_ELEMENT, event.name)) { event.id = existing->id; *existing = event; } else { event.id = element_child_next_id_get(*destinationEvents, ElementType::EVENT_ELEMENT); destinationEvents->children.push_back(event); } eventRemap[sourceId] = event.id; } auto item_remap = [&](Element& item) { for (auto& frame : item.children) { if (frame.type != ElementType::FRAME && frame.type != ElementType::TRIGGER) continue; for (auto& soundId : frame.soundIds) soundId = remap_id(soundRemap, soundId); frame.eventId = remap_id(eventRemap, frame.eventId); } }; auto track_tree_remap = [&](auto&& self, Element item, int itemType, Element& container, int parentGroupId = -1) -> void { auto trackType = itemType == LAYER ? ElementType::LAYER_ANIMATION : ElementType::NULL_ANIMATION; if (item.type == ElementType::GROUP) { auto group = element_make(ElementType::GROUP); group.id = item.id == -1 ? element_child_next_id_get(container, ElementType::GROUP) : item.id; group.name = item.name; group.isExpanded = item.isExpanded; container.children.push_back(group); for (auto child : item.children) self(self, child, itemType, container, group.id); return; } if (item.type != trackType) return; if (itemType == LAYER) item.layerId = remap_id(layerRemap, item.layerId); else item.nullId = remap_id(nullRemap, item.nullId); if ((itemType == LAYER && item.layerId < 0) || (itemType == NULL_ && item.nullId < 0)) return; if (parentGroupId != -1) item.groupId = parentGroupId; item_remap(item); container.children.push_back(item); }; auto animation_build = [&](const Element& incoming) { auto animation = element_make(ElementType::ANIMATION); animation.name = incoming.name; animation.frameNum = incoming.frameNum; animation.isLoop = incoming.isLoop; if (auto root = element_child_first_get(incoming, ElementType::ROOT_ANIMATION)) { auto item = *root; item_remap(item); animation.children.push_back(item); } if (auto layerAnimations = element_child_first_get(incoming, ElementType::LAYER_ANIMATIONS)) { auto container = element_make(ElementType::LAYER_ANIMATIONS); for (auto item : layerAnimations->children) track_tree_remap(track_tree_remap, item, LAYER, container); animation.children.push_back(container); } if (auto nullAnimations = element_child_first_get(incoming, ElementType::NULL_ANIMATIONS)) { auto container = element_make(ElementType::NULL_ANIMATIONS); for (auto item : nullAnimations->children) track_tree_remap(track_tree_remap, item, NULL_, container); animation.children.push_back(container); } if (auto triggers = element_child_first_get(incoming, ElementType::TRIGGERS)) { auto item = *triggers; item_remap(item); animation.children.push_back(item); } return animation; }; auto track_merge = [&](Element& destinationContainer, const Element& sourceTrack) { if (auto destinationTrack = track_find(destinationContainer, sourceTrack)) { if (!sourceTrack.children.empty()) *destinationTrack = sourceTrack; } else destinationContainer.children.push_back(sourceTrack); }; auto track_container_merge = [&](Element& destinationContainer, const Element& sourceContainer, ElementType trackType) { std::unordered_map groupRemap{}; for (auto item : sourceContainer.children) { if (item.type != ElementType::GROUP) continue; auto sourceGroupId = item.id; item.id = element_child_next_id_get(destinationContainer, ElementType::GROUP); item.children.clear(); destinationContainer.children.push_back(item); groupRemap[sourceGroupId] = item.id; } for (auto item : sourceContainer.children) { if (item.type != trackType) continue; if (item.groupId != -1) item.groupId = groupRemap.contains(item.groupId) ? groupRemap.at(item.groupId) : -1; track_merge(destinationContainer, item); } }; if (auto sourceAnimations = source.element_get(ElementType::ANIMATIONS)) if (auto destinationAnimations = anm2.element_get(ElementType::ANIMATIONS)) { for (const auto& incoming : sourceAnimations->children) { if (incoming.type != ElementType::ANIMATION) continue; auto processed = animation_build(incoming); auto destination = find_by_name(*destinationAnimations, ElementType::ANIMATION, processed.name); if (!destination) { destinationAnimations->children.push_back(processed); continue; } destination->frameNum = std::max(destination->frameNum, processed.frameNum); destination->isLoop = processed.isLoop; if (auto root = element_child_first_get(processed, ElementType::ROOT_ANIMATION); root && !root->children.empty()) child_set(*destination, *root, ElementType::ROOT_ANIMATION); if (auto triggers = element_child_first_get(processed, ElementType::TRIGGERS); triggers && !triggers->children.empty()) child_set(*destination, *triggers, ElementType::TRIGGERS); if (auto layerAnimations = element_child_first_get(processed, ElementType::LAYER_ANIMATIONS)) { auto destinationLayerAnimations = track_container_get(*destination, ElementType::LAYER_ANIMATIONS); track_container_merge(*destinationLayerAnimations, *layerAnimations, ElementType::LAYER_ANIMATION); } if (auto nullAnimations = element_child_first_get(processed, ElementType::NULL_ANIMATIONS)) { auto destinationNullAnimations = track_container_get(*destination, ElementType::NULL_ANIMATIONS); track_container_merge(*destinationNullAnimations, *nullAnimations, ElementType::NULL_ANIMATION); } destination->frameNum = std::max(destination->frameNum, animation_length_get(*destination)); } if (destinationAnimations->defaultAnimation.empty() && !sourceAnimations->defaultAnimation.empty()) destinationAnimations->defaultAnimation = sourceAnimations->defaultAnimation; } assets_sync(ALL); return true; } void Document::hash_set() { hash = anm2.hash(); } void Document::clean() { assets_sync(); saveHash = anm2.hash(); hash = saveHash; lastAutosaveTime = 0.0f; isForceDirty = false; } void Document::spritesheet_hashes_reset() { spritesheetHashes.clear(); spritesheetSaveHashes.clear(); if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) if (spritesheet.type == ElementType::SPRITESHEET) { auto currentHash = document::spritesheet_hash_get(spritesheet, texture_get(spritesheet.id)); spritesheetHashes[spritesheet.id] = currentHash; spritesheetSaveHashes[spritesheet.id] = currentHash; } } void Document::spritesheet_hashes_sync() { std::set validIds{}; if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) if (spritesheet.type == ElementType::SPRITESHEET) validIds.insert(spritesheet.id); for (auto it = spritesheetHashes.begin(); it != spritesheetHashes.end();) { if (!validIds.contains(it->first)) it = spritesheetHashes.erase(it); else ++it; } for (auto it = spritesheetSaveHashes.begin(); it != spritesheetSaveHashes.end();) { if (!validIds.contains(it->first)) it = spritesheetSaveHashes.erase(it); else ++it; } if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) if (spritesheet.type == ElementType::SPRITESHEET) { auto currentHash = document::spritesheet_hash_get(spritesheet, texture_get(spritesheet.id)); spritesheetHashes[spritesheet.id] = currentHash; if (!spritesheetSaveHashes.contains(spritesheet.id)) spritesheetSaveHashes[spritesheet.id] = currentHash; } } void Document::change(ChangeType type) { hash_set(); assets_sync(type); auto events_set = [&]() { auto events = anm2.element_get(ElementType::EVENTS); event.labels_set(document::element_name_labels_get(events, ElementType::EVENT_ELEMENT, true), document::element_ids_get(events, ElementType::EVENT_ELEMENT, true)); }; auto animations_set = [&]() { animation.labels_set(document::animation_labels_get(anm2)); }; auto spritesheets_set = [&]() { spritesheet.labels_set(document::spritesheet_labels_get(anm2), document::spritesheet_ids_get(anm2)); spritesheet_hashes_sync(); }; auto sounds_set = [&]() { sound.labels_set(document::sound_labels_get(anm2), document::sound_ids_get(anm2)); }; auto regions_set = [&]() { regionBySpritesheet.clear(); if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) if (spritesheet.type == ElementType::SPRITESHEET) { Storage storage{}; storage.labels_set(document::element_name_labels_get(&spritesheet, ElementType::REGION, true), document::element_ids_get(&spritesheet, ElementType::REGION, true)); regionBySpritesheet.emplace(spritesheet.id, std::move(storage)); } }; switch (type) { case LAYERS: break; case NULLS: break; case EVENTS: events_set(); break; case SPRITESHEETS: spritesheets_set(); regions_set(); break; case SOUNDS: sounds_set(); break; case FRAMES: events_set(); sounds_set(); break; case ITEMS: spritesheets_set(); break; case ANIMATIONS: case ALL: events_set(); spritesheets_set(); regions_set(); animations_set(); sounds_set(); break; default: break; } } bool Document::is_dirty() const { return hash != saveHash; } bool Document::is_autosave_dirty() const { return hash != autosaveHash; } void Document::spritesheet_hash_update(int id) { auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); if (!spritesheet) return; assets_sync(TEXTURES); spritesheetHashes[id] = document::spritesheet_hash_get(*spritesheet, texture_get(id)); } void Document::spritesheet_hash_set_saved(int id) { auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id); if (!spritesheet) return; assets_sync(TEXTURES); auto currentHash = document::spritesheet_hash_get(*spritesheet, texture_get(id)); spritesheetHashes[id] = currentHash; spritesheetSaveHashes[id] = currentHash; } bool Document::spritesheet_is_dirty(int id) { if (!anm2.element_get(ElementType::SPRITESHEET, id)) return false; if (!spritesheetHashes.contains(id)) spritesheet_hash_update(id); auto saveIt = spritesheetSaveHashes.find(id); if (saveIt == spritesheetSaveHashes.end()) return false; return spritesheetHashes.at(id) != saveIt->second; } bool Document::spritesheet_any_dirty() { if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS)) for (auto& spritesheet : spritesheets->children) if (spritesheet.type == ElementType::SPRITESHEET && spritesheet_is_dirty(spritesheet.id)) return true; return false; } std::filesystem::path Document::directory_get() const { return path.parent_path(); } std::filesystem::path Document::filename_get() const { return path.filename(); } bool Document::is_valid() const { return isValid && !path.empty(); } void Document::command_run(Manager& manager, Command& command) { if (command.run) command.run(manager, *this); } Element* Document::frame_get() { return anm2.element_get(reference.animationIndex, document::item_type_get(reference.itemType), reference.frameIndex, reference.itemID); } Element* Document::item_get() { return anm2.element_get(reference.animationIndex, document::item_type_get(reference.itemType), reference.itemID); } Element* Document::animation_get() { return anm2.element_get(ElementType::ANIMATION, reference.animationIndex); } Element* Document::spritesheet_get() { return anm2.element_get(ElementType::SPRITESHEET, spritesheet.reference); } void Document::spritesheet_add(const std::filesystem::path& path) { spritesheets_add({path}); } void Document::spritesheets_add(const std::vector& paths) { struct LoadedSpritesheet { std::filesystem::path relativePath{}; resource::Texture texture{}; }; auto items = anm2.element_get(ElementType::SPRITESHEETS); if (!items) return; auto directory = directory_get(); std::vector loaded{}; for (auto& path : paths) { auto pathCopy = path; auto loadPath = path::lower_case_backslash_handle(pathCopy); auto texture = resource::Texture(loadPath); if (!texture.is_valid()) { auto pathUtf8 = path::to_utf8(pathCopy); toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED), std::make_format_args(pathUtf8))); logger.error(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED, anm2ed::ENGLISH), std::make_format_args(pathUtf8))); continue; } loaded.push_back({.relativePath = path::backslash_replace(path::make_relative(loadPath, directory)), .texture = std::move(texture)}); } if (loaded.empty()) return; snapshot(localize.get(EDIT_ADD_SPRITESHEET)); std::set added{}; for (auto& loadedSpritesheet : loaded) { auto id = element_child_next_id_get(*items, ElementType::SPRITESHEET); auto spritesheet = element_make(ElementType::SPRITESHEET); spritesheet.id = id; spritesheet.path = loadedSpritesheet.relativePath; auto pathString = path::to_utf8(spritesheet.path); items->children.push_back(spritesheet); textures[id] = std::move(loadedSpritesheet.texture); texturePaths[id] = spritesheet.path; added.insert(id); this->spritesheet.reference = id; spritesheet_hash_set_saved(id); toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, pathString))); logger.info(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED, anm2ed::ENGLISH), std::make_format_args(id, pathString))); } this->spritesheet.selection = added; change(Document::SPRITESHEETS); } void Document::sound_add(const std::filesystem::path& path) { sounds_add({path}); } void Document::sounds_add(const std::vector& paths) { auto items = anm2.element_get(ElementType::SOUNDS); if (!items) return; auto directory = directory_get(); std::vector validPaths{}; for (auto& path : paths) if (!path.empty()) validPaths.push_back(path); if (validPaths.empty()) return; snapshot(localize.get(EDIT_ADD_SOUND)); std::set added{}; for (auto& path : validPaths) { auto id = element_child_next_id_get(*items, ElementType::SOUND_ELEMENT); auto soundElement = element_make(ElementType::SOUND_ELEMENT); soundElement.id = id; auto loadPath = path::lower_case_backslash_handle(path); auto relativePath = path::backslash_replace(path::make_relative(loadPath, directory)); { WorkingDirectory workingDirectory(directory_get()); soundElement.path = relativePath; sounds[id] = resource::Audio(soundElement.path); soundPaths[id] = soundElement.path; } auto soundPath = path::to_utf8(soundElement.path); items->children.push_back(soundElement); added.insert(id); sound.reference = id; toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, soundPath))); logger.info( std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH), std::make_format_args(id, soundPath))); } sound.selection = added; change(Document::SOUNDS); } void Document::snapshot(const std::string& message) { this->message = message; snapshots.push(current); } void Document::undo() { if (!snapshots.undo()) return; document::restored_snapshot_sanitize(*this); toasts.push(std::vformat(localize.get(TOAST_UNDO), std::make_format_args(message))); logger.info(std::vformat(localize.get(TOAST_UNDO, anm2ed::ENGLISH), std::make_format_args(message))); change(Document::ALL); } void Document::redo() { if (!snapshots.redo()) return; document::restored_snapshot_sanitize(*this); toasts.push(std::vformat(localize.get(TOAST_REDO), std::make_format_args(message))); logger.info(std::vformat(localize.get(TOAST_REDO, anm2ed::ENGLISH), std::make_format_args(message))); change(Document::ALL); } bool Document::is_able_to_undo() { return !snapshots.undoStack.is_empty(); } bool Document::is_able_to_redo() { return !snapshots.redoStack.is_empty(); } }