Merge branch 'staging'

This commit is contained in:
2026-04-08 20:01:35 -04:00
28 changed files with 240 additions and 75 deletions

0
.codex Normal file
View File

4
.vscode/tasks.json vendored
View File

@@ -24,7 +24,7 @@
"--build",
"out/build/linux-debug",
"--parallel",
"8",
"16",
"--target",
"anm2ed"
],
@@ -67,7 +67,7 @@
"--build",
"out/build/linux-release",
"--parallel",
"8",
"16",
"--target",
"anm2ed"
],

View File

@@ -2,10 +2,13 @@
#include <algorithm>
#include <filesystem>
#include <limits>
#include <set>
#include <unordered_map>
#include "file_.hpp"
#include "map_.hpp"
#include "math_.hpp"
#include "time_.hpp"
#include "vector_.hpp"
#include "working_directory.hpp"
@@ -58,6 +61,69 @@ namespace anm2ed::anm2
{
Anm2::Anm2() { info.createdOn = time::get("%m/%d/%Y %I:%M:%S %p"); }
Frame Anm2::frame_effective(int layerId, const Frame& frame) const
{
auto resolved = frame;
if (frame.regionID == -1) return resolved;
if (!content.layers.contains(layerId)) return resolved;
auto spritesheet = const_cast<Anm2*>(this)->spritesheet_get(content.layers.at(layerId).spritesheetID);
if (!spritesheet) return resolved;
auto regionIt = spritesheet->regions.find(frame.regionID);
if (regionIt == spritesheet->regions.end()) return resolved;
resolved.crop = regionIt->second.crop;
resolved.size = regionIt->second.size;
resolved.pivot = regionIt->second.pivot;
return resolved;
}
vec4 Anm2::animation_rect(Animation& animation, bool isRootTransform) const
{
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
float minX = std::numeric_limits<float>::infinity();
float minY = std::numeric_limits<float>::infinity();
float maxX = -std::numeric_limits<float>::infinity();
float maxY = -std::numeric_limits<float>::infinity();
bool any = false;
for (float t = 0.0f; t < (float)animation.frameNum; t += 1.0f)
{
mat4 transform(1.0f);
if (isRootTransform)
{
auto root = animation.rootAnimation.frame_generate(t, ROOT);
transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation);
}
for (auto& [id, layerAnimation] : animation.layerAnimations)
{
if (!layerAnimation.isVisible) continue;
auto frame = frame_effective(id, layerAnimation.frame_generate(t, LAYER));
if (frame.size == vec2() || !frame.isVisible) continue;
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
math::percent_to_unit(frame.scale), frame.rotation);
for (auto& corner : CORNERS)
{
vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f);
minX = std::min(minX, world.x);
minY = std::min(minY, world.y);
maxX = std::max(maxX, world.x);
maxY = std::max(maxY, world.y);
any = true;
}
}
}
if (!any) return vec4(-1.0f);
return {minX, minY, maxX - minX, maxY - minY};
}
Anm2::Anm2(const std::filesystem::path& path, std::string* errorString)
{
XMLDocument document;
@@ -134,6 +200,28 @@ namespace anm2ed::anm2
Anm2 Anm2::normalized_for_serialize() const
{
auto normalized = *this;
auto sanitize_layer_order = [](Animation& animation)
{
std::vector<int> sanitized{};
sanitized.reserve(animation.layerAnimations.size());
std::set<int> seen{};
for (auto id : animation.layerOrder)
{
if (!animation.layerAnimations.contains(id)) continue;
if (!seen.insert(id).second) continue;
sanitized.push_back(id);
}
std::vector<int> missing{};
missing.reserve(animation.layerAnimations.size());
for (auto& id : animation.layerAnimations | std::views::keys)
if (!seen.contains(id)) missing.push_back(id);
std::sort(missing.begin(), missing.end());
sanitized.insert(sanitized.end(), missing.begin(), missing.end());
animation.layerOrder = std::move(sanitized);
};
std::unordered_map<int, int> layerRemap{};
int normalizedID = 0;
@@ -149,6 +237,7 @@ namespace anm2ed::anm2
for (auto& animation : normalized.animations.items)
{
sanitize_layer_order(animation);
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
@@ -166,6 +255,7 @@ namespace anm2ed::anm2
animation.layerAnimations = std::move(layerAnimations);
animation.layerOrder = std::move(layerOrder);
sanitize_layer_order(animation);
}
return normalized;

View File

@@ -45,7 +45,7 @@ namespace anm2ed::anm2
std::vector<std::string> spritesheet_labels_get();
std::vector<int> spritesheet_ids_get();
std::set<int> spritesheets_unused();
bool spritesheets_merge(const std::set<int>&, SpritesheetMergeOrigin, bool, origin::Type);
bool spritesheets_merge(const std::set<int>&, SpritesheetMergeOrigin, bool, bool, origin::Type);
bool spritesheets_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type type,
std::string*);
std::vector<std::string> region_labels_get(Spritesheet&);
@@ -79,6 +79,8 @@ namespace anm2ed::anm2
std::vector<std::string> animation_labels_get();
int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
Frame frame_effective(int, const Frame&) const;
glm::vec4 animation_rect(Animation&, bool) const;
Item* item_get(int, Type, int = -1);
Reference layer_animation_add(Reference = {}, int = -1, std::string = {}, int = 0,

View File

@@ -136,4 +136,4 @@ namespace anm2ed::anm2
return finalIndex;
}
}
}

View File

@@ -1,5 +1,7 @@
#include "anm2.hpp"
#include <format>
#include "map_.hpp"
#include "path_.hpp"
#include "working_directory.hpp"

View File

@@ -404,7 +404,7 @@ namespace anm2ed::anm2
}
bool Anm2::spritesheets_merge(const std::set<int>& ids, SpritesheetMergeOrigin mergeOrigin, bool isMakeRegions,
origin::Type regionOrigin)
bool isMakePrimaryRegion, origin::Type regionOrigin)
{
if (ids.size() < 2) return false;
@@ -447,19 +447,22 @@ namespace anm2ed::anm2
base.regionOrder.push_back(id);
}
auto baseLocationRegionID = map::next_id_get(base.regions);
auto baseFilename = path::to_utf8(base.path.stem());
auto baseLocationRegionName = baseFilename.empty() ? std::format("#{}", baseId) : baseFilename;
auto baseLocationRegionPivot =
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(baseTextureSize) * 0.5f : glm::vec2();
base.regions[baseLocationRegionID] = {
.name = baseLocationRegionName,
.crop = {},
.pivot = glm::ivec2(baseLocationRegionPivot),
.size = baseTextureSize,
.origin = regionOrigin,
};
base.regionOrder.push_back(baseLocationRegionID);
if (isMakePrimaryRegion)
{
auto baseLocationRegionID = map::next_id_get(base.regions);
auto baseFilename = path::to_utf8(base.path.stem());
auto baseLocationRegionName = baseFilename.empty() ? std::format("#{}", baseId) : baseFilename;
auto baseLocationRegionPivot =
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(baseTextureSize) * 0.5f : glm::vec2();
base.regions[baseLocationRegionID] = {
.name = baseLocationRegionName,
.crop = {},
.pivot = glm::ivec2(baseLocationRegionPivot),
.size = baseTextureSize,
.origin = regionOrigin,
};
base.regionOrder.push_back(baseLocationRegionID);
}
for (auto id : ids)
{

View File

@@ -1,5 +1,7 @@
#include "toast.hpp"
#include <format>
#include "log.hpp"
#include <imgui/imgui.h>

View File

@@ -488,10 +488,10 @@ namespace anm2ed::imgui
auto center_view = [&]() { pan = vec2(); };
auto fit_view = [&]()
{
if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform));
};
auto fit_view = [&]()
{
if (animation) set_to_rect(zoom, pan, anm2.animation_rect(*animation, isRootTransform));
};
auto zoom_adjust = [&](float delta)
{
@@ -627,7 +627,7 @@ namespace anm2ed::imgui
savedZoom = zoom;
savedPan = pan;
if (auto rect = document.animation_get()->rect(isRootTransform); rect != vec4(-1.0f))
if (auto rect = anm2.animation_rect(*document.animation_get(), isRootTransform); rect != vec4(-1.0f))
{
size_set(vec2(rect.z, rect.w) * settings.renderScale);
set_to_rect(zoom, pan, rect);
@@ -779,9 +779,11 @@ namespace anm2ed::imgui
for (auto& id : animation->layerOrder)
{
if (!animation->layerAnimations.contains(id)) continue;
auto& layerAnimation = animation->layerAnimations[id];
if (!layerAnimation.isVisible) continue;
if (!anm2.content.layers.contains(id)) continue;
auto& layer = anm2.content.layers.at(id);
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
@@ -797,21 +799,11 @@ namespace anm2ed::imgui
auto texSize = vec2(texture.size);
if (texSize.x <= 0.0f || texSize.y <= 0.0f) return;
frame = anm2.frame_effective(id, frame);
auto crop = frame.crop;
auto size = frame.size;
auto pivot = frame.pivot;
if (frame.regionID != -1)
{
auto regionIt = spritesheet->regions.find(frame.regionID);
if (regionIt != spritesheet->regions.end())
{
crop = regionIt->second.crop;
size = regionIt->second.size;
pivot = regionIt->second.pivot;
}
}
auto layerModel =
math::quad_model_get(size, frame.position, pivot, math::percent_to_unit(frame.scale), frame.rotation);
auto layerTransform = sampleTransform * layerModel;

View File

@@ -1,5 +1,6 @@
#include "events.hpp"
#include <format>
#include <ranges>
#include "log.hpp"

View File

@@ -42,6 +42,9 @@ namespace anm2ed::imgui
{
auto frame = document.frame_get();
auto useFrame = frame ? *frame : anm2::Frame();
auto displayFrame = frame && type == anm2::LAYER && document.reference.itemID != -1
? document.anm2.frame_effective(document.reference.itemID, *frame)
: useFrame;
ImGui::BeginDisabled(!frame);
{
@@ -112,18 +115,20 @@ namespace anm2ed::imgui
bool isRegionSet = frame && frame->regionID != -1;
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet);
{
auto cropDisplay = frame ? displayFrame.crop : vec2();
auto cropEdit =
drag_float2_persistent(localize.get(BASIC_CROP), frame ? &frame->crop : &dummy_value<vec2>(),
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->crop) : "");
drag_float2_persistent(localize.get(BASIC_CROP), frame ? &cropDisplay : &dummy_value<vec2>(),
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.crop) : "");
if (cropEdit == edit::START)
document.snapshot(localize.get(EDIT_FRAME_CROP));
else if (cropEdit == edit::END)
document.change(Document::FRAMES);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CROP));
auto sizeDisplay = frame ? displayFrame.size : vec2();
auto sizeEdit =
drag_float2_persistent(localize.get(BASIC_SIZE), frame ? &frame->size : &dummy_value<vec2>(),
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->size) : "");
drag_float2_persistent(localize.get(BASIC_SIZE), frame ? &sizeDisplay : &dummy_value<vec2>(),
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.size) : "");
if (sizeEdit == edit::START)
document.snapshot(localize.get(EDIT_FRAME_SIZE));
else if (sizeEdit == edit::END)
@@ -143,9 +148,10 @@ namespace anm2ed::imgui
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet);
{
auto pivotDisplay = frame ? displayFrame.pivot : vec2();
auto pivotEdit =
drag_float2_persistent(localize.get(BASIC_PIVOT), frame ? &frame->pivot : &dummy_value<vec2>(),
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->pivot) : "");
drag_float2_persistent(localize.get(BASIC_PIVOT), frame ? &pivotDisplay : &dummy_value<vec2>(),
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.pivot) : "");
if (pivotEdit == edit::START)
document.snapshot(localize.get(EDIT_FRAME_PIVOT));
else if (pivotEdit == edit::END)
@@ -201,7 +207,11 @@ namespace anm2ed::imgui
regionIds, regionLabels) &&
frame)
DOCUMENT_EDIT(document, localize.get(EDIT_SET_REGION_PROPERTIES), Document::FRAMES,
frame->regionID = useFrame.regionID);
frame->regionID = useFrame.regionID;
auto effectiveFrame = document.anm2.frame_effective(document.reference.itemID, *frame);
frame->crop = effectiveFrame.crop;
frame->size = effectiveFrame.size;
frame->pivot = effectiveFrame.pivot);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION));
ImGui::EndDisabled();

View File

@@ -1,5 +1,6 @@
#include "layers.hpp"
#include <format>
#include <ranges>
#include "log.hpp"

View File

@@ -1,5 +1,6 @@
#include "nulls.hpp"
#include <format>
#include <ranges>
#include "log.hpp"

View File

@@ -1,6 +1,7 @@
#include "sounds.hpp"
#include <algorithm>
#include <format>
#include <ranges>
#include <vector>

View File

@@ -185,6 +185,7 @@ namespace anm2ed::imgui
auto baseID = *mergeSelection.begin();
if (anm2.spritesheets_merge(mergeSelection, (anm2::SpritesheetMergeOrigin)settings.mergeSpritesheetsOrigin,
settings.mergeSpritesheetsIsMakeRegions,
settings.mergeSpritesheetsIsMakePrimaryRegion,
(origin::Type)settings.mergeSpritesheetsRegionOrigin))
{
selection = {baseID};
@@ -594,7 +595,7 @@ namespace anm2ed::imgui
mergePopup.close();
};
auto optionsSize = child_size_get(5);
auto optionsSize = child_size_get(6);
if (ImGui::BeginChild("##Merge Spritesheets Options", optionsSize, ImGuiChildFlags_Borders))
{
ImGui::SeparatorText(localize.get(LABEL_REGION_PROPERTIES_ORIGIN));
@@ -610,9 +611,13 @@ namespace anm2ed::imgui
ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_SPRITESHEET_REGIONS), &settings.mergeSpritesheetsIsMakeRegions);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_SPRITESHEET_REGIONS));
ImGui::BeginDisabled(!settings.mergeSpritesheetsIsMakeRegions);
ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION),
&settings.mergeSpritesheetsIsMakePrimaryRegion);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION));
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();

View File

@@ -3,6 +3,7 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <format>
#include <imgui_internal.h>

View File

@@ -1,5 +1,6 @@
#include "welcome.hpp"
#include <format>
#include <ranges>
#include "path_.hpp"

View File

@@ -1,6 +1,9 @@
#include "configure.hpp"
#include "imgui_.hpp"
#include "log.hpp"
#include "path_.hpp"
#include "sdl.hpp"
using namespace anm2ed::types;
@@ -180,6 +183,7 @@ namespace anm2ed::imgui::wizard
shortcut(manager.chords[SHORTCUT_CONFIRM]);
if (ImGui::Button(localize.get(BASIC_SAVE), widgetSize))
{
auto settingsPath = util::sdl::preferences_directory_get() / "settings.ini";
settings = temporary;
ImGui::GetIO().KeyRepeatDelay = settings.keyboardRepeatDelay;
@@ -193,6 +197,8 @@ namespace anm2ed::imgui::wizard
for (auto& document : manager.documents)
document.snapshots.apply_limit();
settings.save(settingsPath, ImGui::SaveIniSettingsToMemory(nullptr));
isSet = true;
}
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SETTINGS_SAVE));

View File

@@ -181,7 +181,7 @@ namespace anm2ed::imgui::wizard
ImGui::BeginDisabled(!isRaw);
{
input_float_range(localize.get(BASIC_SCALE), scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx");
input_float_range(localize.get(BASIC_SCALE), scale, 0.1f, 100.0f, 0.1f, STEP_FAST, "%.1fx");
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE_OUTPUT));
}
ImGui::EndDisabled();

View File

@@ -3,6 +3,7 @@
#include <cerrno>
#include <cstdint>
#include <filesystem>
#include <format>
#include <imgui/backends/imgui_impl_opengl3.h>
#include <imgui/backends/imgui_impl_sdl3.h>
@@ -22,7 +23,6 @@
#ifdef _WIN32
#include "util/path_.hpp"
#include <DbgHelp.h>
#include <format>
#include <windows.h>
#endif

View File

@@ -1,8 +1,10 @@
#include "log.hpp"
#include <array>
#include <format>
#include <print>
#include "file_.hpp"
#include "path_.hpp"
#include "sdl.hpp"
#include "time_.hpp"
@@ -22,7 +24,7 @@ namespace anm2ed
{
std::lock_guard lock(mutex);
std::println("{}", message);
if (file.is_open()) file << message << '\n' << std::flush;
if (!logPath.empty()) file::write_string(logPath, message + '\n', "ab");
}
void Logger::write(const Level level, const std::string& message)
@@ -132,7 +134,7 @@ namespace anm2ed
void Logger::open(const std::filesystem::path& path)
{
file.open(path, std::ios::out | std::ios::app);
logPath = path;
stderr_redirect_start();
}
@@ -148,7 +150,6 @@ namespace anm2ed
{
info("Exiting Anm2Ed");
stderr_redirect_stop();
if (file.is_open()) file.close();
}
}

View File

@@ -1,7 +1,6 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <mutex>
@@ -45,7 +44,7 @@ namespace anm2ed
class Logger
{
std::ofstream file{};
std::filesystem::path logPath{};
std::mutex mutex{};
std::thread stderrThread{};
bool isStderrRedirecting{};

View File

@@ -5,6 +5,7 @@
#include <format>
#include "file_.hpp"
#include "log.hpp"
#include "path_.hpp"
#include "sdl.hpp"
@@ -279,12 +280,13 @@ namespace anm2ed
{
auto path = recent_files_path_get();
std::ifstream file(path);
if (!file)
std::string fileData{};
if (!file::read_to_string(path, &fileData, "rb"))
{
logger.warning(std::format("Could not load recent files from: {}. Skipping...", path::to_utf8(path)));
return;
}
std::istringstream file(fileData);
logger.info(std::format("Loading recent files from: {}", path::to_utf8(path)));
@@ -322,18 +324,12 @@ namespace anm2ed
auto path = recent_files_path_get();
ensure_parent_directory_exists(path);
std::ofstream file;
file.open(path, std::ofstream::out | std::ofstream::trunc);
if (!file.is_open())
{
logger.warning(std::format("Could not write recent files to: {}. Skipping...", path::to_utf8(path)));
return;
}
std::ostringstream file;
auto ordered = recent_files_ordered();
for (auto& entry : ordered)
file << path::to_utf8(entry) << '\n';
if (!file::write_string(path, file.str(), "wb"))
logger.warning(std::format("Could not write recent files to: {}. Skipping...", path::to_utf8(path)));
}
void Manager::recent_files_clear()
@@ -370,12 +366,13 @@ namespace anm2ed
{
auto path = autosave_path_get();
std::ifstream file(path);
if (!file)
std::string fileData{};
if (!file::read_to_string(path, &fileData, "rb"))
{
logger.warning(std::format("Could not load autosave files from: {}. Skipping...", path::to_utf8(path)));
return;
}
std::istringstream file(fileData);
logger.info(std::format("Loading autosave files from: {}", path::to_utf8(path)));
@@ -393,19 +390,13 @@ namespace anm2ed
void Manager::autosave_files_write()
{
std::ofstream autosaveWriteFile;
ensure_parent_directory_exists(autosave_path_get());
autosaveWriteFile.open(autosave_path_get(), std::ofstream::out | std::ofstream::trunc);
if (!autosaveWriteFile.is_open())
{
logger.warning(
std::format("Could not write autosave files to: {}. Skipping...", path::to_utf8(autosave_path_get())));
return;
}
std::ostringstream autosaveWriteFile;
for (auto& path : autosaveFiles)
autosaveWriteFile << path::to_utf8(path) << "\n";
if (!file::write_string(autosave_path_get(), autosaveWriteFile.str(), "wb"))
logger.warning(
std::format("Could not write autosave files to: {}. Skipping...", path::to_utf8(autosave_path_get())));
}
void Manager::autosave_files_clear()

View File

@@ -304,6 +304,7 @@ namespace anm2ed
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_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION, "Make Primary Spritesheet Region", "Crear region de spritesheet principal", "Создать регион основного спрайт-листа", "生成主图集区域", "기본 스프라이트 시트 영역 만들기") \
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", "Новый", "新", "새로") \
@@ -576,6 +577,7 @@ namespace anm2ed
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_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION, "If disabled, the base spritesheet will not be added as a region in the merged spritesheet.", "Si se desactiva, el spritesheet base no se agregará como región en el spritesheet combinado.", "Если отключено, базовый спрайт-лист не будет добавлен как регион в объединённый спрайт-лист.", "禁用后,基础图集不会作为区域添加到合并后的图集中。", "비활성화하면 기본 스프라이트 시트가 병합된 스프라이트 시트에 영역으로 추가되지 않습니다.") \
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.", "Установить анимацию, которая будет выведена над текущей анимацией.", "设置一个当前动画的覆盖动画.", "현재 애니메이션 위에 그려질 애니메이션을 설정합니다.") \

View File

@@ -338,7 +338,15 @@ DockSpace ID=0x123F8F08 Window=0x6D581B32 Pos=8,62 Size=1496,860 S
void Settings::save(const std::filesystem::path& path, const std::string& imguiData)
{
auto pathUtf8 = path::to_utf8(path);
logger.info(std::format("Saving settings to: {}", pathUtf8));
std::ofstream file(path, std::ios::out | std::ios::binary);
if (!file.is_open())
{
logger.error(std::format("Failed to save settings file: {}", pathUtf8));
return;
}
file << "[Settings]\n";
auto value_save = [&](const std::string& key, const auto& value)
@@ -395,6 +403,12 @@ DockSpace ID=0x123F8F08 Window=0x6D581B32 Pos=8,62 Size=1496,860 S
<< imguiData;
file.flush();
if (file.fail())
{
logger.error(std::format("Failed while writing settings file: {}", pathUtf8));
return;
}
file.close();
logger.info(std::format("Saved settings to: {}", pathUtf8));
}
}

View File

@@ -155,6 +155,7 @@ namespace anm2ed
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_IS_MAKE_PRIMARY_REGION, mergeSpritesheetsIsMakePrimaryRegion, STRING_UNDEFINED, BOOL, true) \
X(MERGE_SPRITESHEETS_REGION_ORIGIN, mergeSpritesheetsRegionOrigin, STRING_UNDEFINED, INT, origin::TOP_LEFT) \
X(PACK_PADDING, packPadding, STRING_UNDEFINED, INT, 1) \
\

View File

@@ -1,5 +1,8 @@
#include "file_.hpp"
#include <array>
#include <cstring>
namespace anm2ed::util
{
File::File(const std::filesystem::path& path, const char* mode) { open(path, mode); }
@@ -38,4 +41,32 @@ namespace anm2ed::util
FILE* File::get() const { return handle; }
File::operator bool() const { return handle != nullptr; }
}
namespace file
{
bool read_to_string(const std::filesystem::path& path, std::string* output, const char* mode)
{
if (!output) return false;
File file(path, mode);
if (!file) return false;
output->clear();
std::array<char, 4096> buffer{};
while (true)
{
auto read = std::fread(buffer.data(), 1, buffer.size(), file.get());
if (read > 0) output->append(buffer.data(), read);
if (read < buffer.size()) return std::feof(file.get()) != 0;
}
}
bool write_string(const std::filesystem::path& path, std::string_view data, const char* mode)
{
File file(path, mode);
if (!file) return false;
return std::fwrite(data.data(), 1, data.size(), file.get()) == data.size();
}
}
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include <filesystem>
#include <string>
#include <string_view>
namespace anm2ed::util
{
@@ -19,4 +21,10 @@ namespace anm2ed::util
private:
FILE* handle{};
};
}
namespace file
{
bool read_to_string(const std::filesystem::path&, std::string*, const char* mode = "rb");
bool write_string(const std::filesystem::path&, std::string_view, const char* mode = "wb");
}
}