spritesheet hashing, navigation fix, etc.

This commit is contained in:
2026-02-06 11:52:33 -05:00
parent 64d6a1d95a
commit 85073a2ab9
14 changed files with 428 additions and 39 deletions

View File

@@ -39,7 +39,7 @@ namespace anm2ed::anm2
Spritesheet* spritesheet_get(int);
bool spritesheet_add(const std::filesystem::path&, const std::filesystem::path&, int&);
bool spritesheet_pack(int);
bool spritesheet_pack(int, int);
bool regions_trim(int, const std::set<int>&);
std::vector<std::string> spritesheet_labels_get();
std::vector<int> spritesheet_ids_get();

View File

@@ -29,9 +29,9 @@ namespace anm2ed::anm2
return true;
}
bool Anm2::spritesheet_pack(int id)
bool Anm2::spritesheet_pack(int id, int padding)
{
constexpr int PACKING_PADDING = 1;
const int packingPadding = std::max(0, padding);
struct RectI
{
@@ -256,8 +256,8 @@ namespace anm2ed::anm2
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;
int packWidth = size.x + packingPadding * 2;
int packHeight = size.y + packingPadding * 2;
items.push_back({regionID, minPoint.x, minPoint.y, size.x, size.y, packWidth, packHeight});
}
@@ -290,8 +290,8 @@ namespace anm2ed::anm2
{
int sourceX = item.srcX + x;
int sourceY = item.srcY + y;
int destinationX = destinationRect.x + PACKING_PADDING + x;
int destinationY = destinationRect.y + PACKING_PADDING + y;
int destinationX = destinationRect.x + packingPadding + x;
int destinationY = destinationRect.y + packingPadding + y;
if (sourceX < 0 || sourceY < 0 || sourceX >= textureSize.x || sourceY >= textureSize.y) continue;
if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight)
@@ -312,7 +312,7 @@ namespace anm2ed::anm2
if (packedRects.contains(regionID))
{
auto& rect = packedRects.at(regionID);
region.crop = {rect.x + PACKING_PADDING, rect.y + PACKING_PADDING};
region.crop = {rect.x + packingPadding, rect.y + packingPadding};
}
return true;

View File

@@ -1,7 +1,9 @@
#include "spritesheet.h"
#include <algorithm>
#include <functional>
#include <ranges>
#include <string_view>
#include <vector>
#include "map_.h"
@@ -52,6 +54,7 @@ namespace anm2ed::anm2
path = path::lower_case_backslash_handle(path);
texture = Texture(path);
regionOrder.clear();
for (auto child = element->FirstChildElement("Region"); child; child = child->NextSiblingElement("Region"))
{
Region region{};
@@ -73,13 +76,17 @@ namespace anm2ed::anm2
child->QueryFloatAttribute("YPivot", &region.pivot.y);
}
regions.emplace(id, std::move(region));
regionOrder.push_back(id);
}
if (regionOrder.size() != regions.size())
{
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)
{
@@ -231,4 +238,43 @@ namespace anm2ed::anm2
}
bool Spritesheet::is_valid() { return texture.is_valid(); }
uint64_t Spritesheet::hash() const
{
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<int>{}(texture.size.x));
hash_combine(seed, std::hash<int>{}(texture.size.y));
hash_combine(seed, std::hash<int>{}(texture.channels));
hash_combine(seed, std::hash<int>{}(texture.filter));
if (!texture.pixels.empty())
{
std::string_view bytes(reinterpret_cast<const char*>(texture.pixels.data()), texture.pixels.size());
hash_combine(seed, std::hash<std::string_view>{}(bytes));
}
else
{
hash_combine(seed, 0);
}
for (const auto& [id, region] : regions)
{
hash_combine(seed, std::hash<int>{}(id));
hash_combine(seed, std::hash<std::string>{}(region.name));
hash_combine(seed, std::hash<float>{}(region.crop.x));
hash_combine(seed, std::hash<float>{}(region.crop.y));
hash_combine(seed, std::hash<float>{}(region.size.x));
hash_combine(seed, std::hash<float>{}(region.size.y));
hash_combine(seed, std::hash<float>{}(region.pivot.x));
hash_combine(seed, std::hash<float>{}(region.pivot.y));
hash_combine(seed, std::hash<int>{}(static_cast<int>(region.origin)));
}
return static_cast<uint64_t>(seed);
}
}

View File

@@ -5,6 +5,7 @@
#include <set>
#include <vector>
#include <string>
#include <cstdint>
#include <tinyxml2/tinyxml2.h>
#include "texture.h"
@@ -48,5 +49,6 @@ namespace anm2ed::anm2
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int, Flags = 0);
void reload(const std::filesystem::path&, const std::filesystem::path& = {});
bool is_valid();
uint64_t hash() const;
};
}

View File

@@ -75,8 +75,10 @@ namespace anm2ed
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), isAnimationPreviewSet(other.isAnimationPreviewSet),
isSpritesheetEditorSet(other.isSpritesheetEditorSet)
isOpen(other.isOpen), isForceDirty(other.isForceDirty),
spritesheetHashes(std::move(other.spritesheetHashes)),
spritesheetSaveHashes(std::move(other.spritesheetSaveHashes)),
isAnimationPreviewSet(other.isAnimationPreviewSet), isSpritesheetEditorSet(other.isSpritesheetEditorSet)
{
}
@@ -99,6 +101,8 @@ namespace anm2ed
isValid = other.isValid;
isOpen = other.isOpen;
isForceDirty = other.isForceDirty;
spritesheetHashes = std::move(other.spritesheetHashes);
spritesheetSaveHashes = std::move(other.spritesheetSaveHashes);
isAnimationPreviewSet = other.isAnimationPreviewSet;
isSpritesheetEditorSet = other.isSpritesheetEditorSet;
}
@@ -182,6 +186,44 @@ namespace anm2ed
isForceDirty = false;
}
void Document::spritesheet_hashes_reset()
{
spritesheetHashes.clear();
spritesheetSaveHashes.clear();
for (auto& [id, spritesheet] : anm2.content.spritesheets)
{
auto currentHash = spritesheet.hash();
spritesheetHashes[id] = currentHash;
spritesheetSaveHashes[id] = currentHash;
}
}
void Document::spritesheet_hashes_sync()
{
for (auto it = spritesheetHashes.begin(); it != spritesheetHashes.end();)
{
if (!anm2.content.spritesheets.contains(it->first))
it = spritesheetHashes.erase(it);
else
++it;
}
for (auto it = spritesheetSaveHashes.begin(); it != spritesheetSaveHashes.end();)
{
if (!anm2.content.spritesheets.contains(it->first))
it = spritesheetSaveHashes.erase(it);
else
++it;
}
for (auto& [id, spritesheet] : anm2.content.spritesheets)
{
auto currentHash = spritesheet.hash();
spritesheetHashes[id] = currentHash;
if (!spritesheetSaveHashes.contains(id)) spritesheetSaveHashes[id] = currentHash;
}
}
void Document::change(ChangeType type)
{
hash_set();
@@ -191,7 +233,10 @@ namespace anm2ed
auto animations_set = [&]() { animation.labels_set(anm2.animation_labels_get()); };
auto spritesheets_set = [&]()
{ spritesheet.labels_set(anm2.spritesheet_labels_get(), anm2.spritesheet_ids_get()); };
{
spritesheet.labels_set(anm2.spritesheet_labels_get(), anm2.spritesheet_ids_get());
spritesheet_hashes_sync();
};
auto sounds_set = [&]() { sound.labels_set(anm2.sound_labels_get(), anm2.sound_ids_get()); };
@@ -244,6 +289,37 @@ namespace anm2ed
bool Document::is_dirty() const { return hash != saveHash; }
bool Document::is_autosave_dirty() const { return hash != autosaveHash; }
void Document::spritesheet_hash_update(int id)
{
if (!anm2.content.spritesheets.contains(id)) return;
spritesheetHashes[id] = anm2.content.spritesheets.at(id).hash();
}
void Document::spritesheet_hash_set_saved(int id)
{
if (!anm2.content.spritesheets.contains(id)) return;
auto currentHash = anm2.content.spritesheets.at(id).hash();
spritesheetHashes[id] = currentHash;
spritesheetSaveHashes[id] = currentHash;
}
bool Document::spritesheet_is_dirty(int id)
{
if (!anm2.content.spritesheets.contains(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()
{
for (auto& [id, spritesheet] : anm2.content.spritesheets)
{
if (spritesheet_is_dirty(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(); }
@@ -272,6 +348,7 @@ namespace anm2ed
auto pathString = path::to_utf8(spritesheet.path);
this->spritesheet.selection = {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)));

View File

@@ -2,6 +2,7 @@
#include <filesystem>
#include <map>
#include <unordered_map>
#include "snapshots.h"
@@ -64,6 +65,8 @@ namespace anm2ed
bool isValid{true};
bool isOpen{true};
bool isForceDirty{false};
std::unordered_map<int, uint64_t> spritesheetHashes{};
std::unordered_map<int, uint64_t> spritesheetSaveHashes{};
bool isAnimationPreviewSet{false};
bool isSpritesheetEditorSet{false};
@@ -82,6 +85,12 @@ namespace anm2ed
std::filesystem::path directory_get() const;
std::filesystem::path filename_get() const;
bool is_valid() const;
void spritesheet_hash_update(int);
void spritesheet_hash_set_saved(int);
bool spritesheet_is_dirty(int);
bool spritesheet_any_dirty();
void spritesheet_hashes_reset();
void spritesheet_hashes_sync();
anm2::Frame* frame_get();
anm2::Item* item_get();

View File

@@ -6,6 +6,8 @@
#include "path_.h"
#include "strings.h"
#include "time_.h"
#include "toast.h"
#include "log.h"
using namespace anm2ed::resource;
using namespace anm2ed::types;
@@ -61,7 +63,9 @@ namespace anm2ed::imgui
for (int i = 0; i < documentsCount; ++i)
{
auto& document = manager.documents[i];
auto isDirty = document.is_dirty() || document.isForceDirty;
auto isDocumentDirty = document.is_dirty() || document.isForceDirty;
auto isSpritesheetDirty = document.spritesheet_any_dirty();
auto isDirty = isDocumentDirty || isSpritesheetDirty;
if (!closePopup.is_open())
{
@@ -87,13 +91,13 @@ namespace anm2ed::imgui
}
auto isRequested = i == manager.pendingSelected;
auto font = isDirty ? font::ITALICS : font::REGULAR;
auto font = isDocumentDirty ? font::ITALICS : font::REGULAR;
auto filename = path::to_utf8(document.filename_get());
auto string =
isDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) : filename;
isDocumentDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) : filename;
auto label = std::format("{}###Document{}", string, i);
auto flags = isDirty ? ImGuiTabItemFlags_UnsavedDocument : 0;
auto flags = isDocumentDirty ? ImGuiTabItemFlags_UnsavedDocument : 0;
if (isRequested) flags |= ImGuiTabItemFlags_SetSelected;
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
@@ -129,7 +133,13 @@ namespace anm2ed::imgui
auto& closeDocument = manager.documents[closeDocumentIndex];
auto filename = path::to_utf8(closeDocument.filename_get());
auto prompt = std::vformat(localize.get(LABEL_DOCUMENT_MODIFIED_PROMPT), std::make_format_args(filename));
auto isDocumentDirty = closeDocument.is_dirty() || closeDocument.isForceDirty;
auto isSpritesheetDirty = closeDocument.spritesheet_any_dirty();
auto promptLabel = isDocumentDirty && isSpritesheetDirty
? LABEL_DOCUMENT_AND_SPRITESHEETS_MODIFIED_PROMPT
: (isDocumentDirty ? LABEL_DOCUMENT_MODIFIED_PROMPT
: LABEL_SPRITESHEETS_MODIFIED_PROMPT);
auto prompt = std::vformat(localize.get(promptLabel), std::make_format_args(filename));
ImGui::TextUnformatted(prompt.c_str());
auto widgetSize = imgui::widget_size_with_row_get(3);
@@ -143,7 +153,31 @@ namespace anm2ed::imgui
shortcut(manager.chords[SHORTCUT_CONFIRM]);
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
{
if (isDocumentDirty)
manager.save(closeDocumentIndex, {}, (anm2::Compatibility)settings.fileCompatibility);
if (isSpritesheetDirty)
{
for (auto& [id, spritesheet] : closeDocument.anm2.content.spritesheets)
{
if (!closeDocument.spritesheet_is_dirty(id)) continue;
auto pathString = path::to_utf8(spritesheet.path);
if (spritesheet.save(closeDocument.directory_get()))
{
closeDocument.spritesheet_hash_set_saved(id);
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString)));
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
std::make_format_args(id, pathString)));
}
else
{
toasts.push(
std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString)));
logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH),
std::make_format_args(id, pathString)));
}
}
}
manager.close(closeDocumentIndex);
close();
}

View File

@@ -224,6 +224,41 @@ namespace anm2ed::imgui
selection.insert(id);
}
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
auto scroll_to_item = [&](float itemHeight, bool isTarget)
{
if (!isTarget) return;
auto windowHeight = ImGui::GetWindowHeight();
auto targetTop = ImGui::GetCursorPosY();
auto targetBottom = targetTop + itemHeight;
auto visibleTop = ImGui::GetScrollY();
auto visibleBottom = visibleTop + windowHeight;
if (targetTop < visibleTop)
ImGui::SetScrollY(targetTop);
else if (targetBottom > visibleBottom)
ImGui::SetScrollY(targetBottom - windowHeight);
};
int scrollTargetId = -1;
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
{
auto& order = spritesheet->regionOrder;
if (!order.empty())
{
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
int current = reference;
if (current == -1 && !selection.empty()) current = *selection.begin();
auto it = std::find(order.begin(), order.end(), current);
int index = it == order.end() ? 0 : (int)std::distance(order.begin(), it);
index = std::clamp(index + delta, 0, (int)order.size() - 1);
int nextId = order[index];
selection = {nextId};
reference = nextId;
document.reference = {document.reference.animationIndex};
frame.reference = -1;
frame.selection.clear();
scrollTargetId = nextId;
}
}
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);
@@ -241,6 +276,8 @@ namespace anm2ed::imgui
ImGui::PushID(id);
scroll_to_item(regionChildSize.y, scrollTargetId == id);
if (ImGui::BeginChild("##Region Child", regionChildSize, ImGuiChildFlags_Borders))
{
auto cursorPos = ImGui::GetCursorPos();
@@ -254,6 +291,7 @@ namespace anm2ed::imgui
frame.reference = -1;
frame.selection.clear();
}
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) propertiesPopup.open();
auto viewport = ImGui::GetMainViewport();

View File

@@ -1,6 +1,8 @@
#include "sounds.h"
#include <algorithm>
#include <ranges>
#include <vector>
#include "log.h"
#include "path_.h"
@@ -220,12 +222,49 @@ namespace anm2ed::imgui
selection.insert(id);
}
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
auto scroll_to_item = [&](float itemHeight, bool isTarget)
{
if (!isTarget) return;
auto windowHeight = ImGui::GetWindowHeight();
auto targetTop = ImGui::GetCursorPosY();
auto targetBottom = targetTop + itemHeight;
auto visibleTop = ImGui::GetScrollY();
auto visibleBottom = visibleTop + windowHeight;
if (targetTop < visibleTop)
ImGui::SetScrollY(targetTop);
else if (targetBottom > visibleBottom)
ImGui::SetScrollY(targetBottom - windowHeight);
};
int scrollTargetId = -1;
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
{
std::vector<int> ids{};
ids.reserve(anm2.content.sounds.size());
for (auto& [id, sound] : anm2.content.sounds)
ids.push_back(id);
if (!ids.empty())
{
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
int current = reference;
if (current == -1 && !selection.empty()) current = *selection.begin();
auto it = std::find(ids.begin(), ids.end(), current);
int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it);
index = std::clamp(index + delta, 0, (int)ids.size() - 1);
int nextId = ids[index];
selection = {nextId};
reference = nextId;
scrollTargetId = nextId;
}
}
for (auto& [id, sound] : anm2.content.sounds)
{
auto isNewSound = newSoundId == id;
ImGui::PushID(id);
scroll_to_item(soundChildSize.y, scrollTargetId == id);
if (ImGui::BeginChild("##Sound Child", soundChildSize, ImGuiChildFlags_Borders))
{
auto isSelected = selection.contains(id);
@@ -242,6 +281,7 @@ namespace anm2ed::imgui
reference = id;
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) play(sound);
}
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) open_directory(sound);
auto textWidth = ImGui::CalcTextSize(pathString.c_str()).x;

View File

@@ -655,7 +655,10 @@ namespace anm2ed::imgui
if (isMouseClicked)
document.snapshot(useTool == tool::DRAW ? localize.get(EDIT_DRAW) : localize.get(EDIT_ERASE));
if (isMouseDown) spritesheet->texture.pixel_line(ivec2(previousMousePos), ivec2(mousePos), color);
if (isMouseReleased) document.change(Document::SPRITESHEETS);
if (isMouseReleased)
{
document.change(Document::SPRITESHEETS);
}
break;
}
case tool::COLOR_PICKER:

View File

@@ -1,6 +1,8 @@
#include "spritesheets.h"
#include <algorithm>
#include <ranges>
#include <vector>
#include <filesystem>
#include <format>
@@ -19,6 +21,8 @@ using namespace glm;
namespace anm2ed::imgui
{
static constexpr auto PADDING_MAX = 100;
void Spritesheets::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog,
Clipboard& clipboard)
{
@@ -44,7 +48,8 @@ namespace anm2ed::imgui
auto id = *selection.begin();
if (!anm2.content.spritesheets.contains(id)) return;
if (anm2.content.spritesheets.at(id).regions.empty()) return;
if (pack) pack();
packId = id;
packPopup.open();
};
auto add = [&](const std::filesystem::path& path)
@@ -85,6 +90,7 @@ namespace anm2ed::imgui
{
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
spritesheet.reload(document.directory_get());
document.spritesheet_hash_set_saved(id);
auto pathString = path::to_utf8(spritesheet.path);
toasts.push(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET), std::make_format_args(id, pathString)));
logger.info(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET, anm2ed::ENGLISH),
@@ -104,6 +110,7 @@ namespace anm2ed::imgui
auto& id = *selection.begin();
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
spritesheet.reload(document.directory_get(), path);
document.spritesheet_hash_set_saved(id);
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),
@@ -113,16 +120,18 @@ namespace anm2ed::imgui
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SPRITESHEET), Document::SPRITESHEETS, behavior());
};
auto save = [&]()
auto save = [&](const std::set<int>& ids)
{
if (selection.empty()) return;
if (ids.empty()) return;
for (auto& id : selection)
for (auto& id : ids)
{
if (!anm2.content.spritesheets.contains(id)) continue;
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
auto pathString = path::to_utf8(spritesheet.path);
if (spritesheet.save(document.directory_get()))
{
document.spritesheet_hash_set_saved(id);
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString)));
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
std::make_format_args(id, pathString)));
@@ -136,6 +145,20 @@ namespace anm2ed::imgui
}
};
auto save_open = [&]()
{
if (selection.empty()) return;
if (settings.fileIsWarnOverwrite)
{
saveSelection = selection;
overwritePopup.open();
}
else
{
save(selection);
}
};
auto merge = [&]()
{
if (mergeSelection.size() <= 1) return;
@@ -165,12 +188,15 @@ namespace anm2ed::imgui
};
pack = [&]()
{
if (selection.size() != 1) return;
int id = packId != -1 ? packId : (selection.size() == 1 ? *selection.begin() : -1);
if (id == -1) return;
if (!anm2.content.spritesheets.contains(id)) return;
if (anm2.content.spritesheets.at(id).regions.empty()) return;
auto behavior = [&]()
{
auto id = *selection.begin();
if (anm2.spritesheet_pack(id))
auto padding = std::max(0, settings.packPadding);
if (anm2.spritesheet_pack(id, padding))
{
toasts.push(localize.get(TOAST_PACK_SPRITESHEET));
logger.info(localize.get(TOAST_PACK_SPRITESHEET, anm2ed::ENGLISH));
@@ -213,7 +239,8 @@ namespace anm2ed::imgui
auto behavior = [&]()
{
auto maxSpritesheetIdBefore = anm2.content.spritesheets.empty() ? -1 : anm2.content.spritesheets.rbegin()->first;
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))
@@ -271,8 +298,7 @@ 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()) &&
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();
@@ -282,7 +308,7 @@ namespace anm2ed::imgui
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();
if (ImGui::MenuItem(localize.get(BASIC_SAVE), nullptr, false, !selection.empty())) save_open();
ImGui::Separator();
@@ -313,12 +339,51 @@ namespace anm2ed::imgui
selection.insert(id);
}
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
auto scroll_to_item = [&](float itemHeight, bool isTarget)
{
if (!isTarget) return;
auto windowHeight = ImGui::GetWindowHeight();
auto targetTop = ImGui::GetCursorPosY();
auto targetBottom = targetTop + itemHeight;
auto visibleTop = ImGui::GetScrollY();
auto visibleBottom = visibleTop + windowHeight;
if (targetTop < visibleTop)
ImGui::SetScrollY(targetTop);
else if (targetBottom > visibleBottom)
ImGui::SetScrollY(targetBottom - windowHeight);
};
int scrollTargetId = -1;
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
{
std::vector<int> ids{};
ids.reserve(anm2.content.spritesheets.size());
for (auto& [id, sheet] : anm2.content.spritesheets)
ids.push_back(id);
if (!ids.empty())
{
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
int current = reference;
if (current == -1 && !selection.empty()) current = *selection.begin();
auto it = std::find(ids.begin(), ids.end(), current);
int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it);
index = std::clamp(index + delta, 0, (int)ids.size() - 1);
int nextId = ids[index];
selection = {nextId};
reference = nextId;
region.reference = -1;
region.selection.clear();
scrollTargetId = nextId;
}
}
for (auto& [id, spritesheet] : anm2.content.spritesheets)
{
auto isNewSpritesheet = newSpritesheetId == id;
ImGui::PushID(id);
scroll_to_item(spritesheetChildSize.y, scrollTargetId == id);
if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders))
{
auto isSelected = selection.contains(id);
@@ -338,6 +403,7 @@ namespace anm2ed::imgui
region.reference = -1;
region.selection.clear();
}
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
open_directory(spritesheet);
@@ -411,8 +477,11 @@ namespace anm2ed::imgui
spritesheetChildSize.y - spritesheetChildSize.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, pathCStr)).c_str());
auto spritesheetLabel = std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathCStr));
if (document.spritesheet_is_dirty(id))
spritesheetLabel =
std::vformat(localize.get(FORMAT_SPRITESHEET_NOT_SAVED), std::make_format_args(spritesheetLabel));
ImGui::TextUnformatted(spritesheetLabel.c_str());
if (isReferenced) ImGui::PopFont();
}
@@ -476,7 +545,7 @@ namespace anm2ed::imgui
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
if (ImGui::Button(localize.get(BASIC_SAVE), rowTwoWidgetSize)) save();
if (ImGui::Button(localize.get(BASIC_SAVE), rowTwoWidgetSize)) save_open();
ImGui::EndDisabled();
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SAVE_SPRITESHEETS));
@@ -543,5 +612,68 @@ namespace anm2ed::imgui
}
mergePopup.end();
packPopup.trigger();
if (ImGui::BeginPopupModal(packPopup.label(), &packPopup.isOpen, ImGuiWindowFlags_NoResize))
{
settings.packPadding = std::max(0, settings.packPadding);
auto close = [&]()
{
packId = -1;
packPopup.close();
};
auto optionsSize = child_size_get(1);
if (ImGui::BeginChild("##Pack Spritesheet Options", optionsSize, ImGuiChildFlags_Borders))
{
ImGui::DragInt(localize.get(LABEL_PACK_PADDING), &settings.packPadding, DRAG_SPEED, 0, PADDING_MAX);
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(2);
shortcut(manager.chords[SHORTCUT_CONFIRM]);
bool isPackable = packId != -1 && anm2.content.spritesheets.contains(packId) &&
!anm2.content.spritesheets.at(packId).regions.empty();
ImGui::BeginDisabled(!isPackable);
if (ImGui::Button(localize.get(BASIC_PACK), widgetSize))
{
if (pack) pack();
close();
}
ImGui::EndDisabled();
ImGui::SameLine();
shortcut(manager.chords[SHORTCUT_CANCEL]);
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close();
ImGui::EndPopup();
}
packPopup.end();
overwritePopup.trigger();
if (ImGui::BeginPopupModal(overwritePopup.label(), &overwritePopup.isOpen, ImGuiWindowFlags_NoResize))
{
ImGui::TextUnformatted(localize.get(LABEL_OVERWRITE_CONFIRMATION));
auto widgetSize = widget_size_with_row_get(2);
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
{
save(saveSelection);
saveSelection.clear();
overwritePopup.close();
}
ImGui::SameLine();
if (ImGui::Button(localize.get(BASIC_NO), widgetSize))
{
saveSelection.clear();
overwritePopup.close();
}
ImGui::EndPopup();
}
overwritePopup.end();
}
}

View File

@@ -12,7 +12,11 @@ namespace anm2ed::imgui
{
int newSpritesheetId{-1};
PopupHelper mergePopup{PopupHelper(LABEL_SPRITESHEETS_MERGE_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper packPopup{PopupHelper(LABEL_SPRITESHEETS_PACK_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper overwritePopup{PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, imgui::POPUP_SMALL_NO_HEIGHT)};
std::set<int> mergeSelection{};
int packId{-1};
std::set<int> saveSelection{};
public:
void update(Manager&, Settings&, Resources&, Dialog&, Clipboard& clipboard);

View File

@@ -200,6 +200,7 @@ namespace anm2ed
X(FORMAT_SIZE, "Size: ({0}, {1})", "Tamaño: ({0}, {1})", "Размер: ({0}, {1})", "大小: ({0}, {1})", "크기: ({0}, {1})") \
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_SPRITESHEET_NOT_SAVED, "{0} (Not Saved)", "{0} (No guardado)", "{0} (Не сохранено)", "{0} (未保存)", "{0} (저장되지 않음)") \
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}") \
@@ -216,6 +217,7 @@ namespace anm2ed
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_PACK_PADDING, "Padding", "Relleno", "Отступ", "填充", "패딩") \
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", "Длина анимации", "动画时长", "애니메이션 길이") \
@@ -246,6 +248,8 @@ namespace anm2ed
X(LABEL_DOCUMENTS_OPEN_NEW, "Open New Document", "Abrir Nuevo Documento", "Открыть новый документ", "打开新文件", "새 파일로 열기") \
X(LABEL_DOCUMENT_CLOSE, "Close Document", "Cerrar Documento", "Закрыть документ", "关闭文件", "파일 닫기") \
X(LABEL_DOCUMENT_MODIFIED_PROMPT, "The document \"{0}\" has been modified.\nDo you want to save it?", "El Documento \"{0}\" ha sido modificado.\n¿Quieres Guardarlo?", "Документ \"{0}\" был изменен. \nХотите сохранить его?", "此文件\"{0}\"已被更改.\n要保存吗", "\"{0}\" 파일이 수정되었습니다.\n저장하시겠습니까?") \
X(LABEL_DOCUMENT_AND_SPRITESHEETS_MODIFIED_PROMPT, "The document \"{0}\" and its spritesheets have been modified.\nDo you want to save them?", "El Documento \"{0}\" y sus spritesheets han sido modificados.\n¿Quieres guardarlos?", "Документ \"{0}\" и его спрайт-листы были изменены.\nХотите сохранить их?", "此文件\"{0}\"及其图集已被更改。\n要保存吗", "\"{0}\" 파일과 스프라이트 시트가 수정되었습니다.\n저장하시겠습니까?") \
X(LABEL_SPRITESHEETS_MODIFIED_PROMPT, "Spritesheets in \"{0}\" have been modified.\nDo you want to save them?", "Los spritesheets en \"{0}\" han sido modificados.\n¿Quieres guardarlos?", "Спрайт-листы в \"{0}\" были изменены.\nХотите сохранить их?", "\"{0}\" 中的图集已被修改。\n要保存吗", "\"{0}\"의 스프라이트 시트가 수정되었습니다.\n저장하시겠습니까?") \
X(LABEL_END, "End", "Fin", "Конец", "结尾", "끝") \
X(LABEL_EVENT, "Event", "Evento", "Событие", "事件", "이벤트") \
X(LABEL_EVENTS_WINDOW, "Events###Events", "Eventos###Events", "События###Events", "事件###Events", "이벤트###Events") \
@@ -424,8 +428,7 @@ 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(TOOLTIP_MERGE_SPRITESHEETS, "Merge selected spritesheets into the first selected spritesheet.", "Combina los spritesheets seleccionados en el primer spritesheet seleccionado.", "Объединить выбранные спрайт-листы в первый выбранный спрайт-лист.", "将所选图集合并到第一个选中的图集中。", "선택된 스프라이트 시트를 첫 번째 선택된 스프라이트 시트로 병합합니다.") \
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!", "Этот инструмент можно использовать только в \"Редакторе спрайт-листов\"!", "该工具只能在“图集编辑器”中使用!", "이 도구는 스프라이트 시트 편집기에서만 사용할 수 있습니다!") \

View File

@@ -155,6 +155,7 @@ namespace anm2ed
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(PACK_PADDING, packPadding, STRING_UNDEFINED, INT, 1) \
\
X(BAKE_INTERVAL, bakeInterval, STRING_UNDEFINED, INT, 1) \
X(BAKE_IS_ROUND_SCALE, bakeIsRoundScale, STRING_UNDEFINED, BOOL, true) \