Compare commits

...

8 Commits

29 changed files with 1440 additions and 266 deletions
+1
View File
@@ -8,3 +8,4 @@ workshop/resources
cmake-build-debug/
.vs/
.idea/
.codex
+2
View File
@@ -88,6 +88,8 @@ set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp)
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
src/anm2/*.cpp
src/anm2/*.hpp
src/anm2_new/*.cpp
src/anm2_new/*.hpp
src/resource/*.cpp
src/resource/*.hpp
src/imgui/*.cpp
+1 -1
View File
@@ -1 +1 @@
/home/anon/sda/Personal/Repos/anm2ed/out/build/linux-debug/compile_commands.json
/home/anon/sda/Personal/Repos/anm2ed/out/build/linux-release/compile_commands.json
+24
View File
@@ -146,6 +146,30 @@ namespace anm2ed::anm2
}
}
bool Anm2::has_special_interpolated_frames() const
{
auto item_has_special_frames = [](const Item& item)
{
for (const auto& frame : item.frames)
{
auto interpolation = frame.interpolation;
if (interpolation != Frame::Interpolation::NONE && interpolation != Frame::Interpolation::LINEAR) return true;
}
return false;
};
for (const auto& animation : animations.items)
{
if (item_has_special_frames(animation.rootAnimation)) return true;
for (const auto& item : animation.layerAnimations | std::views::values)
if (item_has_special_frames(item)) return true;
for (const auto& item : animation.nullAnimations | std::views::values)
if (item_has_special_frames(item)) return true;
}
return false;
}
Anm2::Anm2(const std::filesystem::path& path, std::string* errorString)
{
XMLDocument document;
+1
View File
@@ -82,6 +82,7 @@ namespace anm2ed::anm2
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;
bool has_special_interpolated_frames() const;
void bake_special_interpolated_frames(int, bool, bool);
Item* item_get(int, Type, int = -1);
+5 -1
View File
@@ -295,7 +295,11 @@ namespace anm2ed::anm2
if (!vector::in_bounds(frames, index)) return;
auto original = frames[index];
if (original.duration == FRAME_DURATION_MIN) return;
if (original.duration == FRAME_DURATION_MIN)
{
frames[index].interpolation = Frame::Interpolation::NONE;
return;
}
auto nextFrame = vector::in_bounds(frames, index + 1) ? frames[index + 1] : original;
+474
View File
@@ -0,0 +1,474 @@
#include "anm2.hpp"
#include <tinyxml2/tinyxml2.h>
#include "file_.hpp"
#include "path_.hpp"
#include "xml_.hpp"
namespace anm2ed::anm2_new
{
using namespace tinyxml2;
template <typename Enum, std::size_t N>
constexpr std::string_view enum_string_get(const std::array<std::pair<Enum, std::string_view>, N>& mappings,
Enum value)
{
for (const auto& [mappedValue, mappedString] : mappings)
if (mappedValue == value) return mappedString;
return {};
}
template <typename Enum, std::size_t N>
constexpr Enum string_enum_get(const std::array<std::pair<Enum, std::string_view>, N>& mappings,
std::string_view value, Enum fallback)
{
for (const auto& [mappedValue, mappedString] : mappings)
if (mappedString == value) return mappedValue;
return fallback;
}
constexpr bool is_flag_set(Anm2::Flags flags, Anm2::Flag flag) { return (flags & flag) != 0; }
std::filesystem::path asset_path_resolve(const std::filesystem::path& directory,
const std::filesystem::path& assetPath)
{
if (assetPath.empty()) return {};
auto resolved = assetPath.is_absolute() ? assetPath : directory / assetPath;
resolved = util::path::lower_case_backslash_handle(resolved);
if (util::path::is_exist(resolved)) return resolved;
return util::path::lower_case_backslash_handle(assetPath);
}
std::string_view Anm2::Element::tag_get(Type type) { return enum_string_get(TYPE_TAGS, type); }
Anm2::Element::Type Anm2::Element::type_get(std::string_view tag, Type parentType)
{
(void)parentType;
return string_enum_get(TYPE_TAGS, tag, UNKNOWN);
}
Anm2::Element::Element(const std::filesystem::path& path, std::string* errorString)
{
XMLDocument document{};
if (!document_load(document, path, errorString)) return;
if (auto* root = document.RootElement()) *this = from_xml(root);
}
Anm2::Element::Element(std::string_view xml, Source, std::string* errorString)
{
XMLDocument document{};
if (!document_parse(document, xml, errorString)) return;
if (auto* root = document.RootElement()) *this = from_xml(root);
}
bool Anm2::Element::document_load(XMLDocument& document, const std::filesystem::path& path, std::string* errorString)
{
util::File file(path, "rb");
if (!file)
{
if (errorString) *errorString = "Could not open file.";
return false;
}
if (document.LoadFile(file.get()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
bool Anm2::Element::document_parse(XMLDocument& document, std::string_view xml, std::string* errorString)
{
if (document.Parse(xml.data(), xml.size()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
void Anm2::Element::attributes_read(const XMLElement* element, Type parentType)
{
if (!element) return;
util::xml::query_string_attribute(element, "CreatedBy", createdBy);
util::xml::query_string_attribute(element, "CreatedOn", createdOn);
util::xml::query_string_attribute(element, "DefaultAnimation", defaultAnimation);
util::xml::query_string_attribute(element, "Name", name);
util::xml::query_path_attribute(element, "Path", path);
util::xml::query_int_attribute(element, "Id", id);
util::xml::query_int_attribute(element, "LayerId", layerId);
util::xml::query_int_attribute(element, "NullId", nullId);
util::xml::query_int_attribute(element, "SpritesheetId", spritesheetId);
util::xml::query_int_attribute(element, "EventId", eventId);
util::xml::query_int_attribute(element, "RegionId", regionId);
util::xml::query_int_attribute(element, "AtFrame", atFrame);
util::xml::query_int_attribute(element, "FrameNum", frameNum);
util::xml::query_int_attribute(element, "Fps", fps);
util::xml::query_int_attribute(element, "Version", version);
util::xml::query_int_attribute(element, "Delay", delay);
util::xml::query_float_attribute(element, "XCrop", crop.x);
util::xml::query_float_attribute(element, "YCrop", crop.y);
util::xml::query_float_attribute(element, "Width", size.x);
util::xml::query_float_attribute(element, "Height", size.y);
util::xml::query_float_attribute(element, "XPivot", pivot.x);
util::xml::query_float_attribute(element, "YPivot", pivot.y);
util::xml::query_float_attribute(element, "XPosition", position.x);
util::xml::query_float_attribute(element, "YPosition", position.y);
util::xml::query_float_attribute(element, "XScale", scale.x);
util::xml::query_float_attribute(element, "YScale", scale.y);
util::xml::query_float_attribute(element, "Rotation", rotation);
util::xml::query_color_attribute(element, "RedTint", tint.r);
util::xml::query_color_attribute(element, "GreenTint", tint.g);
util::xml::query_color_attribute(element, "BlueTint", tint.b);
util::xml::query_color_attribute(element, "AlphaTint", tint.a);
util::xml::query_color_attribute(element, "RedOffset", colorOffset.r);
util::xml::query_color_attribute(element, "GreenOffset", colorOffset.g);
util::xml::query_color_attribute(element, "BlueOffset", colorOffset.b);
util::xml::query_bool_attribute(element, "Visible", isVisible);
util::xml::query_bool_attribute(element, "ShowRect", isShowRect);
util::xml::query_bool_attribute(element, "Loop", isLoop);
if (const char* value = element->Attribute("Origin")) origin = string_enum_get(ORIGIN_STRINGS, value, CUSTOM);
if (const char* value = element->Attribute("Interpolated"))
{
auto view = std::string_view(value);
if (view == "true" || view == "1")
interpolation = LINEAR;
else if (view == "false" || view == "0")
interpolation = NONE;
else
interpolation = string_enum_get(INTERPOLATION_STRINGS, view, NONE);
}
if (type == SOUND && parentType == TRIGGER) path.clear();
}
void Anm2::Element::attributes_write(XMLElement* element, Type parentType, Flags flags) const
{
if (!element) return;
switch (type)
{
case INFO:
util::xml::set_string_attribute(element, "CreatedBy", createdBy);
util::xml::set_string_attribute(element, "CreatedOn", createdOn);
util::xml::set_int_attribute(element, "Fps", fps);
util::xml::set_int_attribute(element, "Version", version);
break;
case SPRITESHEET:
if (id > -1) util::xml::set_int_attribute(element, "Id", id);
util::xml::set_path_attribute(element, "Path", path);
break;
case REGION:
if (id > -1) util::xml::set_int_attribute(element, "Id", id);
util::xml::set_string_attribute(element, "Name", name);
util::xml::set_float_attribute(element, "XCrop", crop.x);
util::xml::set_float_attribute(element, "YCrop", crop.y);
util::xml::set_float_attribute(element, "Width", size.x);
util::xml::set_float_attribute(element, "Height", size.y);
if (origin == CUSTOM)
{
util::xml::set_float_attribute(element, "XPivot", pivot.x);
util::xml::set_float_attribute(element, "YPivot", pivot.y);
}
else if (auto value = enum_string_get(ORIGIN_STRINGS, origin); !value.empty())
element->SetAttribute("Origin", value.data());
break;
case LAYER:
if (id > -1) util::xml::set_int_attribute(element, "Id", id);
util::xml::set_string_attribute(element, "Name", name);
if (spritesheetId > -1) util::xml::set_int_attribute(element, "SpritesheetId", spritesheetId);
break;
case NULL_ELEMENT:
if (id > -1) util::xml::set_int_attribute(element, "Id", id);
util::xml::set_string_attribute(element, "Name", name);
util::xml::set_bool_attribute(element, "ShowRect", isShowRect);
break;
case EVENT:
if (id > -1) util::xml::set_int_attribute(element, "Id", id);
util::xml::set_string_attribute(element, "Name", name);
break;
case SOUND:
if (id > -1)
{
if (parentType == TRIGGER)
util::xml::set_int_attribute(element, "Id", id);
else
{
util::xml::set_int_attribute(element, "Id", id);
util::xml::set_path_attribute(element, "Path", path);
}
}
break;
case ANIMATIONS:
util::xml::set_string_attribute(element, "DefaultAnimation", defaultAnimation);
break;
case ANIMATION:
util::xml::set_string_attribute(element, "Name", name);
util::xml::set_int_attribute(element, "FrameNum", frameNum);
util::xml::set_bool_attribute(element, "Loop", isLoop);
break;
case LAYER_ANIMATION:
if (layerId > -1) util::xml::set_int_attribute(element, "LayerId", layerId);
util::xml::set_bool_attribute(element, "Visible", isVisible);
break;
case NULL_ANIMATION:
if (nullId > -1) util::xml::set_int_attribute(element, "NullId", nullId);
util::xml::set_bool_attribute(element, "Visible", isVisible);
break;
case TRIGGER:
if (eventId > -1) util::xml::set_int_attribute(element, "EventId", eventId);
util::xml::set_int_attribute(element, "AtFrame", atFrame);
break;
case FRAME:
{
bool isNoRegions = is_flag_set(flags, NO_REGIONS);
bool isFrameNoRegionValues = is_flag_set(flags, FRAME_NO_REGION_VALUES);
bool isRegionValid = parentType == LAYER_ANIMATION && !isNoRegions && regionId != -1;
bool isWriteRegionValues = parentType == LAYER_ANIMATION && (!isFrameNoRegionValues || !isRegionValid);
if (isRegionValid) util::xml::set_int_attribute(element, "RegionId", regionId);
util::xml::set_float_attribute(element, "XPosition", position.x);
util::xml::set_float_attribute(element, "YPosition", position.y);
if (isWriteRegionValues)
{
util::xml::set_float_attribute(element, "XPivot", pivot.x);
util::xml::set_float_attribute(element, "YPivot", pivot.y);
util::xml::set_float_attribute(element, "XCrop", crop.x);
util::xml::set_float_attribute(element, "YCrop", crop.y);
util::xml::set_float_attribute(element, "Width", size.x);
util::xml::set_float_attribute(element, "Height", size.y);
}
util::xml::set_float_attribute(element, "XScale", scale.x);
util::xml::set_float_attribute(element, "YScale", scale.y);
util::xml::set_int_attribute(element, "Delay", delay);
util::xml::set_bool_attribute(element, "Visible", isVisible);
util::xml::set_color_attribute(element, "RedTint", tint.r);
util::xml::set_color_attribute(element, "GreenTint", tint.g);
util::xml::set_color_attribute(element, "BlueTint", tint.b);
util::xml::set_color_attribute(element, "AlphaTint", tint.a);
util::xml::set_color_attribute(element, "RedOffset", colorOffset.r);
util::xml::set_color_attribute(element, "GreenOffset", colorOffset.g);
util::xml::set_color_attribute(element, "BlueOffset", colorOffset.b);
util::xml::set_float_attribute(element, "Rotation", rotation);
if (is_flag_set(flags, INTERPOLATION_BOOL_ONLY) || interpolation == LINEAR || interpolation == NONE)
element->SetAttribute("Interpolated", interpolation == LINEAR);
else if (auto value = enum_string_get(INTERPOLATION_STRINGS, interpolation); !value.empty())
element->SetAttribute("Interpolated", value.data());
break;
}
case ACTOR:
case CONTENT:
case SPRITESHEETS:
case LAYERS:
case NULLS:
case EVENTS:
case SOUNDS:
case ROOT_ANIMATION:
case LAYER_ANIMATIONS:
case NULL_ANIMATIONS:
case TRIGGERS:
case UNKNOWN:
case TYPE_COUNT:
break;
}
}
Anm2::Element Anm2::Element::from_xml(const XMLElement* element, Type parentType)
{
Element parsed{};
if (!element) return parsed;
parsed.type = type_get(element->Name(), parentType);
if (parsed.type == UNKNOWN) return parsed;
parsed.attributes_read(element, parentType);
for (const XMLElement* child = element->FirstChildElement(); child; child = child->NextSiblingElement())
if (auto parsedChild = from_xml(child, parsed.type); parsedChild.type != UNKNOWN)
parsed.children.push_back(std::move(parsedChild));
return parsed;
}
XMLElement* Anm2::Element::to_xml(XMLDocument& document, Type parentType, Flags flags) const
{
auto tagName = tag_get(type);
if (tagName.empty()) return nullptr;
auto* element = document.NewElement(tagName.data());
attributes_write(element, parentType, flags);
for (const auto& child : children)
{
if (is_flag_set(flags, NO_SOUNDS) && (child.type == SOUNDS || child.type == SOUND)) continue;
if (is_flag_set(flags, NO_REGIONS) && child.type == REGION) continue;
if (auto* childElement = child.to_xml(document, type, flags)) element->InsertEndChild(childElement);
}
return element;
}
bool Anm2::Element::serialize(const std::filesystem::path& path, std::string* errorString,
Compatibility compatibility) const
{
XMLDocument document{};
if (auto* element = to_xml(document, UNKNOWN, Anm2::flags_get(compatibility)))
document.InsertFirstChild(element);
else
{
if (errorString) *errorString = "No supported element to serialize.";
return false;
}
util::File file(path, "wb");
if (!file)
{
if (errorString) *errorString = "Could not open file for writing.";
return false;
}
if (document.SaveFile(file.get()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
std::string Anm2::Element::to_string(Compatibility compatibility) const
{
XMLDocument document{};
if (auto* element = to_xml(document, UNKNOWN, Anm2::flags_get(compatibility))) document.InsertFirstChild(element);
return util::xml::document_to_string(document);
}
Anm2::Element* Anm2::Element::child_get(Type childType, std::size_t index)
{
std::size_t matchIndex{};
for (auto& child : children)
if (child.type == childType && matchIndex++ == index) return &child;
return nullptr;
}
const Anm2::Element* Anm2::Element::child_get(Type childType, std::size_t index) const
{
std::size_t matchIndex{};
for (const auto& child : children)
if (child.type == childType && matchIndex++ == index) return &child;
return nullptr;
}
std::vector<Anm2::Element*> Anm2::Element::children_get(Type childType)
{
std::vector<Element*> matches{};
for (auto& child : children)
if (child.type == childType) matches.push_back(&child);
return matches;
}
std::vector<const Anm2::Element*> Anm2::Element::children_get(Type childType) const
{
std::vector<const Element*> matches{};
for (const auto& child : children)
if (child.type == childType) matches.push_back(&child);
return matches;
}
Anm2::Anm2(const std::filesystem::path& filepath, std::string* errorString)
: path(filepath), root(filepath, errorString)
{
isValid = root.type == Element::ACTOR;
assets_reload();
}
bool Anm2::serialize(const std::filesystem::path& filepath, std::string* errorString,
Compatibility compatibility) const
{
return root.serialize(filepath, errorString, compatibility);
}
std::string Anm2::to_string(Compatibility compatibility) const { return root.to_string(compatibility); }
void Anm2::assets_reload()
{
spritesheets.clear();
sounds.clear();
auto directory = path.parent_path();
for (auto* element : elements_get(Element::SPRITESHEET))
if (element->id > -1 && !element->path.empty())
spritesheets[element->id] = resource::Texture(asset_path_resolve(directory, element->path));
for (auto* element : elements_get(Element::SOUND))
if (element->id > -1 && !element->path.empty())
sounds[element->id] = resource::Audio(asset_path_resolve(directory, element->path));
}
Anm2::Element* Anm2::element_get(Element::Type type, std::size_t index)
{
auto matches = elements_get(type);
return index < matches.size() ? matches[index] : nullptr;
}
const Anm2::Element* Anm2::element_get(Element::Type type, std::size_t index) const
{
auto matches = elements_get(type);
return index < matches.size() ? matches[index] : nullptr;
}
std::vector<Anm2::Element*> Anm2::elements_get(Element::Type type)
{
std::vector<Element*> matches{};
elements_collect(root, type, matches);
return matches;
}
std::vector<const Anm2::Element*> Anm2::elements_get(Element::Type type) const
{
std::vector<const Element*> matches{};
elements_collect(root, type, matches);
return matches;
}
void Anm2::elements_collect(Element& element, Element::Type type, std::vector<Element*>& out)
{
if (element.type == type) out.push_back(&element);
for (auto& child : element.children)
elements_collect(child, type, out);
}
void Anm2::elements_collect(const Element& element, Element::Type type, std::vector<const Element*>& out)
{
if (element.type == type) out.push_back(&element);
for (const auto& child : element.children)
elements_collect(child, type, out);
}
}
+233
View File
@@ -0,0 +1,233 @@
#pragma once
#include <array>
#include <filesystem>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <glm/glm/vec2.hpp>
#include <glm/glm/vec3.hpp>
#include <glm/glm/vec4.hpp>
#include "audio.hpp"
#include "texture.hpp"
namespace tinyxml2
{
class XMLDocument;
class XMLElement;
}
namespace anm2ed::anm2_new
{
class Anm2
{
public:
enum Compatibility
{
ISAAC,
ANM2ED,
ANM2ED_LIMITED,
COMPATIBILITY_COUNT
};
enum Flag
{
NO_SOUNDS = 1 << 0,
NO_REGIONS = 1 << 1,
FRAME_NO_REGION_VALUES = 1 << 2,
INTERPOLATION_BOOL_ONLY = 1 << 3
};
using Flags = int;
inline static constexpr std::array<Flags, COMPATIBILITY_COUNT> COMPATIBILITY_FLAGS = {{
NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES | INTERPOLATION_BOOL_ONLY,
0,
FRAME_NO_REGION_VALUES,
}};
class Element
{
public:
enum Source
{
STRING
};
enum Type
{
UNKNOWN,
ACTOR,
INFO,
CONTENT,
SPRITESHEETS,
SPRITESHEET,
REGION,
LAYERS,
LAYER,
NULLS,
NULL_ELEMENT,
EVENTS,
EVENT,
SOUNDS,
SOUND,
ANIMATIONS,
ANIMATION,
ROOT_ANIMATION,
LAYER_ANIMATIONS,
LAYER_ANIMATION,
NULL_ANIMATIONS,
NULL_ANIMATION,
TRIGGERS,
TRIGGER,
FRAME,
TYPE_COUNT
};
enum Origin
{
TOP_LEFT,
ORIGIN_CENTER,
CUSTOM
};
enum Interpolation
{
NONE,
LINEAR,
EASE_IN,
EASE_OUT,
EASE_IN_OUT
};
inline static constexpr std::array<std::pair<Type, std::string_view>, TYPE_COUNT - 1> TYPE_TAGS = {{
{ACTOR, "AnimatedActor"},
{INFO, "Info"},
{CONTENT, "Content"},
{SPRITESHEETS, "Spritesheets"},
{SPRITESHEET, "Spritesheet"},
{REGION, "Region"},
{LAYERS, "Layers"},
{LAYER, "Layer"},
{NULLS, "Nulls"},
{NULL_ELEMENT, "Null"},
{EVENTS, "Events"},
{EVENT, "Event"},
{SOUNDS, "Sounds"},
{SOUND, "Sound"},
{ANIMATIONS, "Animations"},
{ANIMATION, "Animation"},
{ROOT_ANIMATION, "RootAnimation"},
{LAYER_ANIMATIONS, "LayerAnimations"},
{LAYER_ANIMATION, "LayerAnimation"},
{NULL_ANIMATIONS, "NullAnimations"},
{NULL_ANIMATION, "NullAnimation"},
{TRIGGERS, "Triggers"},
{TRIGGER, "Trigger"},
{FRAME, "Frame"},
}};
inline static constexpr std::array<std::pair<Origin, std::string_view>, 2> ORIGIN_STRINGS = {{
{TOP_LEFT, "TopLeft"},
{ORIGIN_CENTER, "Center"},
}};
inline static constexpr std::array<std::pair<Interpolation, std::string_view>, 3> INTERPOLATION_STRINGS = {{
{EASE_IN, "EaseIn"},
{EASE_OUT, "EaseOut"},
{EASE_IN_OUT, "EaseInOut"},
}};
Type type{UNKNOWN};
std::vector<Element> children{};
std::string createdBy{};
std::string createdOn{};
std::string defaultAnimation{};
std::string name{};
std::filesystem::path path{};
int id{-1};
int layerId{-1};
int nullId{-1};
int spritesheetId{-1};
int eventId{-1};
int regionId{-1};
int atFrame{-1};
int frameNum{};
int fps{30};
int version{};
int delay{1};
glm::vec2 crop{};
glm::vec2 size{};
glm::vec2 pivot{};
glm::vec2 position{};
glm::vec2 scale{100.0f, 100.0f};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
float rotation{};
bool isVisible{true};
bool isShowRect{false};
bool isLoop{false};
Origin origin{CUSTOM};
Interpolation interpolation{NONE};
Element() = default;
explicit Element(const std::filesystem::path&, std::string* = nullptr);
Element(std::string_view, Source, std::string* = nullptr);
bool serialize(const std::filesystem::path&, std::string* = nullptr, Compatibility = ANM2ED) const;
std::string to_string(Compatibility = ANM2ED) const;
Element* child_get(Type, std::size_t index = 0);
const Element* child_get(Type, std::size_t index = 0) const;
std::vector<Element*> children_get(Type);
std::vector<const Element*> children_get(Type) const;
static std::string_view tag_get(Type);
static Type type_get(std::string_view, Type = UNKNOWN);
static Element from_xml(const tinyxml2::XMLElement*, Type = UNKNOWN);
static bool document_load(tinyxml2::XMLDocument&, const std::filesystem::path&, std::string*);
static bool document_parse(tinyxml2::XMLDocument&, std::string_view, std::string*);
tinyxml2::XMLElement* to_xml(tinyxml2::XMLDocument&, Type = UNKNOWN, Flags = 0) const;
private:
void attributes_read(const tinyxml2::XMLElement*, Type = UNKNOWN);
void attributes_write(tinyxml2::XMLElement*, Type = UNKNOWN, Flags = 0) const;
};
bool isValid{true};
std::filesystem::path path{};
Element root{};
std::unordered_map<int, resource::Texture> spritesheets{};
std::unordered_map<int, resource::Audio> sounds{};
Anm2() = default;
explicit Anm2(const std::filesystem::path&, std::string* = nullptr);
bool serialize(const std::filesystem::path&, std::string* = nullptr, Compatibility = ANM2ED) const;
std::string to_string(Compatibility = ANM2ED) const;
void assets_reload();
static constexpr Flags flags_get(Compatibility compatibility)
{
return COMPATIBILITY_FLAGS[static_cast<std::size_t>(compatibility)];
}
Element* element_get(Element::Type, std::size_t index = 0);
const Element* element_get(Element::Type, std::size_t index = 0) const;
std::vector<Element*> elements_get(Element::Type);
std::vector<const Element*> elements_get(Element::Type) const;
private:
static void elements_collect(Element&, Element::Type, std::vector<Element*>&);
static void elements_collect(const Element&, Element::Type, std::vector<const Element*>&);
};
}
+4 -21
View File
@@ -1,5 +1,6 @@
#include "document.hpp"
#include <new>
#include <utility>
#include <format>
@@ -72,6 +73,7 @@ namespace anm2ed
items(current.items), layer(current.layer), merge(current.merge), null(current.null), region(current.region),
sound(current.sound), spritesheet(current.spritesheet), 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),
@@ -85,27 +87,8 @@ namespace anm2ed
Document& Document::operator=(Document&& other) noexcept
{
if (this != &other)
{
path = std::move(other.path);
snapshots = std::move(other.snapshots);
previewZoom = other.previewZoom;
previewPan = other.previewPan;
editorPan = other.editorPan;
editorZoom = other.editorZoom;
overlayIndex = other.overlayIndex;
regionBySpritesheet = std::move(other.regionBySpritesheet);
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);
isAnimationPreviewSet = other.isAnimationPreviewSet;
isSpritesheetEditorSet = other.isSpritesheetEditorSet;
}
this->~Document();
new (this) Document(std::move(other));
return *this;
}
+1
View File
@@ -51,6 +51,7 @@ namespace anm2ed
float& frameTime = current.frameTime;
std::string& message = current.message;
std::map<int, Storage> regionBySpritesheet{};
int changeAllFramePropertiesRegionId{-1};
float previewZoom{200};
glm::vec2 previewPan{};
+8 -3
View File
@@ -155,10 +155,15 @@ namespace anm2ed::imgui
shortcut(manager.chords[SHORTCUT_CONFIRM]);
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
{
bool isSaved = true;
if (isDocumentDirty)
manager.save(closeDocumentIndex, {}, (anm2::Compatibility)settings.fileCompatibility,
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
settings.bakeIsRoundRotation);
isSaved = taskbar.save_manual(manager, settings, closeDocumentIndex);
if (!isSaved)
{
ImGui::EndPopup();
return;
}
if (isSpritesheetDirty)
{
+92 -14
View File
@@ -21,6 +21,46 @@ using namespace glm;
namespace anm2ed::imgui
{
bool Taskbar::save_requires_special_prompt(Manager& manager, Settings& settings, int index) const
{
auto* document = manager.get(index);
return document && settings.fileIsSpecialInterpolatedFramesOnSaveReminder &&
document->anm2.has_special_interpolated_frames();
}
void Taskbar::save_execute(Manager& manager, Settings& settings, const PendingSave& request, bool bakeFrames)
{
manager.save(request.index, request.path, (anm2::Compatibility)settings.fileCompatibility, bakeFrames,
settings.bakeIsRoundScale, settings.bakeIsRoundRotation);
}
bool Taskbar::save_request(Manager& manager, Settings& settings, int index, const std::filesystem::path& path)
{
auto* document = manager.get(index);
if (!document) return false;
if (settings.fileIsSpecialInterpolatedFramesOnSaveReminder && document->anm2.has_special_interpolated_frames())
{
pendingSave = {.index = index,
.path = path,
.isOpen = true,
.disableReminder = false,
.autoBakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave};
specialInterpolatedFramesReminderPopup.open();
return false;
}
PendingSave request{.index = index, .path = path};
auto bakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave;
save_execute(manager, settings, request, bakeFrames);
return true;
}
bool Taskbar::save_manual(Manager& manager, Settings& settings, int index, const std::filesystem::path& path)
{
return save_request(manager, settings, index, path);
}
void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting)
{
auto document = manager.get();
@@ -71,12 +111,12 @@ namespace anm2ed::imgui
if (ImGui::MenuItem(localize.get(BASIC_SAVE), settings.shortcutSave.c_str(), false, document))
{
if (settings.fileIsWarnOverwrite)
if (save_requires_special_prompt(manager, settings, manager.selected))
save_request(manager, settings, manager.selected, document->path);
else if (settings.fileIsWarnOverwrite)
overwritePopup.open();
else
manager.save(document->path, (anm2::Compatibility)settings.fileCompatibility,
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
settings.bakeIsRoundRotation);
save_request(manager, settings, manager.selected, document->path);
}
if (ImGui::MenuItem(localize.get(LABEL_SAVE_AS), settings.shortcutSaveAs.c_str(), false, document))
@@ -102,9 +142,7 @@ namespace anm2ed::imgui
if (dialog.is_selected(Dialog::ANM2_SAVE))
{
manager.save(dialog.path, (anm2::Compatibility)settings.fileCompatibility,
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
settings.bakeIsRoundRotation);
save_request(manager, settings, manager.selected, dialog.path);
dialog.reset();
}
@@ -244,9 +282,7 @@ namespace anm2ed::imgui
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
{
manager.save({}, (anm2::Compatibility)settings.fileCompatibility,
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
settings.bakeIsRoundRotation);
save_request(manager, settings);
overwritePopup.close();
}
@@ -257,18 +293,60 @@ namespace anm2ed::imgui
ImGui::EndPopup();
}
specialInterpolatedFramesReminderPopup.trigger();
if (ImGui::BeginPopupModal(specialInterpolatedFramesReminderPopup.label(),
&specialInterpolatedFramesReminderPopup.isOpen, ImGuiWindowFlags_NoResize))
{
ImGui::TextWrapped("%s", localize.get(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_PROMPT));
ImGui::Spacing();
ImGui::Checkbox(localize.get(LABEL_DONT_NOTIFY_ME_AGAIN), &pendingSave.disableReminder);
ImGui::BeginDisabled(!pendingSave.disableReminder);
ImGui::Checkbox(localize.get(LABEL_AUTOMATICALLY_BAKE_THESE_FRAMES_ON_SAVE), &pendingSave.autoBakeFrames);
ImGui::EndDisabled();
auto widgetSize = widget_size_with_row_get(3);
if (ImGui::Button(localize.get(LABEL_SAVE_BAKE_FRAMES), widgetSize))
{
if (pendingSave.disableReminder) settings.fileIsSpecialInterpolatedFramesOnSaveReminder = false;
settings.fileBakeSpecialInterpolatedFramesOnSave = pendingSave.autoBakeFrames;
save_execute(manager, settings, pendingSave, true);
pendingSave = {};
specialInterpolatedFramesReminderPopup.close();
}
ImGui::SameLine();
if (ImGui::Button(localize.get(LABEL_SAVE_DONT_BAKE_FRAMES), widgetSize))
{
if (pendingSave.disableReminder) settings.fileIsSpecialInterpolatedFramesOnSaveReminder = false;
settings.fileBakeSpecialInterpolatedFramesOnSave = pendingSave.autoBakeFrames;
save_execute(manager, settings, pendingSave, false);
pendingSave = {};
specialInterpolatedFramesReminderPopup.close();
}
ImGui::SameLine();
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize))
{
pendingSave = {};
specialInterpolatedFramesReminderPopup.close();
}
ImGui::EndPopup();
}
specialInterpolatedFramesReminderPopup.end();
aboutPopup.end();
if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_NEW);
if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(Dialog::ANM2_OPEN);
if (shortcut(manager.chords[SHORTCUT_SAVE], shortcut::GLOBAL))
{
if (settings.fileIsWarnOverwrite)
if (save_requires_special_prompt(manager, settings))
save_request(manager, settings);
else if (settings.fileIsWarnOverwrite)
overwritePopup.open();
else
manager.save({}, (anm2::Compatibility)settings.fileCompatibility,
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
settings.bakeIsRoundRotation);
save_request(manager, settings);
}
if (shortcut(manager.chords[SHORTCUT_SAVE_AS], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_SAVE);
if (shortcut(manager.chords[SHORTCUT_EXIT], shortcut::GLOBAL)) isQuitting = true;
+19
View File
@@ -1,5 +1,7 @@
#pragma once
#include <filesystem>
#include "canvas.hpp"
#include "dialog.hpp"
#include "imgui_.hpp"
@@ -18,6 +20,15 @@ namespace anm2ed::imgui
{
class Taskbar
{
struct PendingSave
{
int index{-1};
std::filesystem::path path{};
bool isOpen{};
bool disableReminder{};
bool autoBakeFrames{};
};
wizard::ChangeAllFrameProperties changeAllFrameProperties{};
wizard::About about{};
wizard::Configure configure{};
@@ -28,15 +39,23 @@ namespace anm2ed::imgui
PopupHelper generatePopup{PopupHelper(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID)};
PopupHelper changePopup{PopupHelper(LABEL_CHANGE_ALL_FRAME_PROPERTIES, imgui::POPUP_NORMAL_NO_HEIGHT)};
PopupHelper overwritePopup{PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper specialInterpolatedFramesReminderPopup{
PopupHelper(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_POPUP, imgui::POPUP_NORMAL_NO_HEIGHT)};
PopupHelper renderPopup{PopupHelper(LABEL_TASKBAR_RENDER_ANIMATION, imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper configurePopup{PopupHelper(LABEL_TASKBAR_CONFIGURE)};
PopupHelper aboutPopup{PopupHelper(LABEL_TASKBAR_ABOUT)};
Settings editSettings{};
bool isQuittingMode{};
PendingSave pendingSave{};
bool save_requires_special_prompt(Manager&, Settings&, int = -1) const;
void save_execute(Manager&, Settings&, const PendingSave&, bool);
bool save_request(Manager&, Settings&, int = -1, const std::filesystem::path& = {});
public:
float height{};
void update(Manager&, Settings&, Resources&, Dialog&, bool&);
bool save_manual(Manager&, Settings&, int = -1, const std::filesystem::path& = {});
};
};
+37 -24
View File
@@ -950,6 +950,8 @@ namespace anm2ed::imgui
auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
auto isMouseRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
auto isMouseRightReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Right);
auto isMouseDown = isMouseLeftDown || isMouseMiddleDown || isMouseRightDown;
auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta);
auto mouseWheel = ImGui::GetIO().MouseWheel;
@@ -977,10 +979,6 @@ namespace anm2ed::imgui
auto isZoomIn = shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL);
auto isZoomOut = shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL);
auto isBegin = isMouseClicked || isKeyJustPressed;
auto isDuring = isMouseDown || isKeyDown;
auto isEnd = isMouseReleased || isKeyReleased;
auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift);
auto frame = document.frame_get();
@@ -993,6 +991,21 @@ namespace anm2ed::imgui
if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE;
if (tool == tool::SCALE && isMouseRightDown) useTool = tool::MOVE;
bool isToolMouseClicked = isMouseClicked;
bool isToolMouseReleased = isMouseReleased;
bool isToolMouseDown = isMouseLeftDown;
if ((tool == tool::MOVE && useTool == tool::SCALE) || (tool == tool::SCALE && useTool == tool::MOVE))
{
isToolMouseClicked = isMouseRightClicked;
isToolMouseReleased = isMouseRightReleased;
isToolMouseDown = isMouseRightDown;
}
auto isToolBegin = isToolMouseClicked || isKeyJustPressed;
auto isToolDuring = isToolMouseDown || isKeyDown;
auto isToolEnd = isToolMouseReleased || isKeyReleased;
auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST)
{ item->frames_change(frameChange, reference.itemType, changeType, frames); };
@@ -1012,17 +1025,17 @@ namespace anm2ed::imgui
if (isMouseDown || isMouseMiddleDown) pan += vec2(mouseDelta.x, mouseDelta.y);
break;
case tool::MOVE:
if (!item || frames.empty()) break;
if (isBegin)
if (!item || !frame || frames.empty()) break;
if (isToolBegin)
{
document.snapshot(localize.get(EDIT_FRAME_POSITION));
if (isMouseClicked)
if (isToolMouseClicked)
{
moveOffset = settings.inputIsMoveToolSnapToMouse ? vec2() : mousePos - frame->position;
isMoveDragging = true;
}
}
if (isMouseDown && isMoveDragging)
if (isToolMouseDown && isMoveDragging)
frame_change_apply(
{.positionX = (int)(mousePos.x - moveOffset.x), .positionY = (int)(mousePos.y - moveOffset.y)});
@@ -1031,9 +1044,9 @@ namespace anm2ed::imgui
if (isUpPressed) frame_change_apply({.positionY = step}, anm2::SUBTRACT);
if (isDownPressed) frame_change_apply({.positionY = step}, anm2::ADD);
if (isMouseReleased) isMoveDragging = false;
if (isEnd) document.change(Document::FRAMES);
if (isDuring)
if (isToolMouseReleased) isMoveDragging = false;
if (isToolEnd) document.change(Document::FRAMES);
if (isToolDuring)
{
if (ImGui::BeginTooltip())
{
@@ -1045,13 +1058,13 @@ namespace anm2ed::imgui
}
break;
case tool::SCALE:
if (!item || frames.empty()) break;
if (isBegin) document.snapshot(localize.get(EDIT_FRAME_SCALE));
if (isMouseDown)
if (!item || !frame || frames.empty()) break;
if (isToolBegin) document.snapshot(localize.get(EDIT_FRAME_SCALE));
if (isToolMouseDown)
{
frame->scale += vec2(mouseDelta.x, mouseDelta.y);
if (isMod) frame->scale = {frame->scale.x, frame->scale.x};
frame_change_apply({.scaleX = (int)frame->scale.x, .scaleY = (int)frame->scale.y});
auto scale = frame->scale + vec2(mouseDelta.x, mouseDelta.y);
if (isMod) scale = {scale.x, scale.x};
frame_change_apply({.scaleX = scale.x, .scaleY = scale.y});
}
if (isLeftPressed) frame_change_apply({.scaleX = step}, anm2::SUBTRACT);
@@ -1059,7 +1072,7 @@ namespace anm2ed::imgui
if (isUpPressed) frame_change_apply({.scaleY = step}, anm2::SUBTRACT);
if (isDownPressed) frame_change_apply({.scaleY = step}, anm2::ADD);
if (isDuring)
if (isToolDuring)
{
if (ImGui::BeginTooltip())
{
@@ -1070,16 +1083,16 @@ namespace anm2ed::imgui
}
}
if (isEnd) document.change(Document::FRAMES);
if (isToolEnd) document.change(Document::FRAMES);
break;
case tool::ROTATE:
if (!item || frames.empty()) break;
if (isBegin) document.snapshot(localize.get(EDIT_FRAME_ROTATION));
if (isMouseDown) frame_change_apply({.rotation = (int)mouseDelta.x}, anm2::ADD);
if (!item || !frame || frames.empty()) break;
if (isToolBegin) document.snapshot(localize.get(EDIT_FRAME_ROTATION));
if (isToolMouseDown) frame_change_apply({.rotation = (int)mouseDelta.x}, anm2::ADD);
if (isLeftPressed || isDownPressed) frame_change_apply({.rotation = step}, anm2::SUBTRACT);
if (isUpPressed || isRightPressed) frame_change_apply({.rotation = step}, anm2::ADD);
if (isDuring)
if (isToolDuring)
{
if (ImGui::BeginTooltip())
{
@@ -1089,7 +1102,7 @@ namespace anm2ed::imgui
}
}
if (isEnd) document.change(Document::FRAMES);
if (isToolEnd) document.change(Document::FRAMES);
break;
default:
break;
+10 -6
View File
@@ -39,13 +39,17 @@ namespace anm2ed::imgui
if (type == anm2::LAYER && document.reference.itemID != -1)
{
auto spritesheetID = document.anm2.content.layers.at(document.reference.itemID).spritesheetID;
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
if (regionIt != document.regionBySpritesheet.end() && !regionIt->second.ids.empty() &&
!regionIt->second.labels.empty())
if (auto layerIt = document.anm2.content.layers.find(document.reference.itemID);
layerIt != document.anm2.content.layers.end())
{
regionLabels = regionIt->second.labels;
regionIds = regionIt->second.ids;
auto spritesheetID = layerIt->second.spritesheetID;
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
if (regionIt != document.regionBySpritesheet.end() && !regionIt->second.ids.empty() &&
!regionIt->second.labels.empty())
{
regionLabels = regionIt->second.labels;
regionIds = regionIt->second.ids;
}
}
}
+7
View File
@@ -482,6 +482,13 @@ namespace anm2ed::imgui
if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
{
if (!spritesheet || (reference != -1 && !spritesheet->regions.contains(reference)))
{
propertiesPopup.close();
ImGui::EndPopup();
return;
}
auto childSize = child_size_get(5);
auto& region = reference == -1 ? editRegion : spritesheet->regions.at(reference);
+12 -7
View File
@@ -227,10 +227,13 @@ namespace anm2ed::imgui
}
}
auto layerIt = anm2.content.layers.find(reference.itemID);
bool isReferenceLayerOnSpritesheet =
frame && reference.itemID > -1 && layerIt != anm2.content.layers.end() &&
layerIt->second.spritesheetID == referenceSpritesheet;
int highlightedRegionId = -1;
if (frame && reference.itemID > -1 &&
anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet && frame->regionID != -1 &&
spritesheet->regions.contains(frame->regionID))
if (isReferenceLayerOnSpritesheet && frame->regionID != -1 && spritesheet->regions.contains(frame->regionID))
{
highlightedRegionId = frame->regionID;
}
@@ -271,8 +274,7 @@ namespace anm2ed::imgui
}
}
bool isFrameOnSpritesheet =
frame && reference.itemID > -1 && anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet;
bool isFrameOnSpritesheet = isReferenceLayerOnSpritesheet;
if (isFrameOnSpritesheet && frame->regionID == -1)
{
auto frameModel = math::quad_model_get(frame->size, frame->crop);
@@ -338,6 +340,10 @@ namespace anm2ed::imgui
auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift);
auto frame = document.frame_get();
auto layerIt = anm2.content.layers.find(reference.itemID);
bool isReferenceLayerOnSpritesheet =
frame && reference.itemID > -1 && layerIt != anm2.content.layers.end() &&
layerIt->second.spritesheetID == referenceSpritesheet;
auto useTool = tool;
auto step = isMod ? STEP_FAST : STEP;
auto stepX = isGridSnap ? step * gridSize.x : step;
@@ -440,8 +446,7 @@ namespace anm2ed::imgui
{
regionReference = hoveredRegionId;
regionSelection = {hoveredRegionId};
if (frame && reference.itemID > -1 &&
anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet)
if (isReferenceLayerOnSpritesheet)
{
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_REGION), Document::FRAMES,
{
+328 -147
View File
@@ -81,7 +81,6 @@ namespace anm2ed::imgui
{0.6353f, 0.2235f, 0.3647f, 1.0f}};
constexpr auto FRAME_MULTIPLE = 5;
constexpr auto FRAME_DRAG_PAYLOAD_ID = "Frame Drag Drop";
constexpr auto FRAME_TOOLTIP_HOVER_DELAY = 0.75f; // Extra delay for frame info tooltip.
#define ITEM_CHILD_WIDTH ImGui::GetTextLineHeightWithSpacing() * 12.5
@@ -381,6 +380,55 @@ namespace anm2ed::imgui
frames_selection_reset();
};
auto reference_set_timeline_item = [&](anm2::Type type, int id)
{
if (type == anm2::LAYER)
if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end())
document.spritesheet.reference = it->second.spritesheetID;
reference_set_item(type, id);
};
auto timeline_item_references_get = [&]()
{
std::vector<anm2::Reference> itemReferences;
if (!animation) return itemReferences;
itemReferences.push_back({reference.animationIndex, anm2::ROOT});
for (auto& id : animation->layerOrder | std::views::reverse)
{
auto item = animation->item_get(anm2::LAYER, id);
if (!item || (!settings.timelineIsShowUnused && item->frames.empty())) continue;
itemReferences.push_back({reference.animationIndex, anm2::LAYER, id});
}
for (auto& [id, item] : animation->nullAnimations)
{
if (!settings.timelineIsShowUnused && item.frames.empty()) continue;
itemReferences.push_back({reference.animationIndex, anm2::NULL_, id});
}
itemReferences.push_back({reference.animationIndex, anm2::TRIGGER});
return itemReferences;
};
auto reference_set_adjacent_item = [&](int direction)
{
auto itemReferences = timeline_item_references_get();
if (itemReferences.empty()) return;
auto it = std::find_if(itemReferences.begin(), itemReferences.end(), [&](const anm2::Reference& itemReference) {
return itemReference.itemType == reference.itemType && itemReference.itemID == reference.itemID;
});
int index = direction > 0 ? 0 : (int)itemReferences.size() - 1;
if (it != itemReferences.end()) index = (int)std::distance(itemReferences.begin(), it) + direction;
index = std::clamp(index, 0, (int)itemReferences.size() - 1);
auto& itemReference = itemReferences[index];
reference_set_timeline_item(itemReference.itemType, itemReference.itemID);
};
auto item_remove = [&]()
{
auto behavior = [&]()
@@ -653,17 +701,33 @@ namespace anm2ed::imgui
ImGui::PushID(index);
auto item = animation ? animation->item_get(type, id) : nullptr;
if (type != anm2::NONE && !item)
{
ImGui::PopID();
return;
}
auto isVisible = item ? item->isVisible : false;
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false;
auto isReferenced = reference.itemType == type && reference.itemID == id;
auto label = type == anm2::LAYER ? std::vformat(localize.get(FORMAT_LAYER),
std::make_format_args(id, anm2.content.layers[id].name,
anm2.content.layers[id].spritesheetID))
: type == anm2::NULL_
? std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, anm2.content.nulls[id].name))
: localize.get(anm2::TYPE_STRINGS[type]);
auto label = [&]() -> std::string
{
if (type == anm2::LAYER)
{
auto it = anm2.content.layers.find(id);
if (it == anm2.content.layers.end()) return localize.get(anm2::TYPE_STRINGS[type]);
return std::vformat(localize.get(FORMAT_LAYER),
std::make_format_args(id, it->second.name, it->second.spritesheetID));
}
if (type == anm2::NULL_)
{
auto it = anm2.content.nulls.find(id);
if (it == anm2.content.nulls.end()) return localize.get(anm2::TYPE_STRINGS[type]);
return std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, it->second.name));
}
return localize.get(anm2::TYPE_STRINGS[type]);
}();
auto icon = anm2::TYPE_ICONS[type];
auto iconTintCurrent = isLightTheme && type == anm2::NONE ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : itemIconTint;
auto baseColorVec = item_color_vec(type);
@@ -699,8 +763,7 @@ namespace anm2ed::imgui
ImGui::SetNextItemStorageID(id);
if (ImGui::Selectable("##Item Button", isReferenced, ImGuiSelectableFlags_SelectOnClick, itemSize))
{
if (type == anm2::LAYER) document.spritesheet.reference = anm2.content.layers[id].spritesheetID;
reference_set_item(type, id);
reference_set_timeline_item(type, id);
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
@@ -743,7 +806,9 @@ namespace anm2ed::imgui
}
case anm2::LAYER:
{
auto& layer = anm2.content.layers[id];
auto it = anm2.content.layers.find(id);
if (it == anm2.content.layers.end()) break;
auto& layer = it->second;
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(layer.name.c_str());
ImGui::PopFont();
@@ -760,7 +825,9 @@ namespace anm2ed::imgui
}
case anm2::NULL_:
{
auto& nullInfo = anm2.content.nulls[id];
auto it = anm2.content.nulls.find(id);
if (it == anm2.content.nulls.end()) break;
auto& nullInfo = it->second;
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(nullInfo.name.c_str());
ImGui::PopFont();
@@ -853,18 +920,21 @@ namespace anm2ed::imgui
if (type == anm2::NULL_)
{
auto& null = anm2.content.nulls.at(id);
auto& isShowRect = null.isShowRect;
auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT;
ImGui::SetCursorPos(
ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get()))
DOCUMENT_EDIT(document, localize.get(EDIT_TOGGLE_NULL_RECT), Document::FRAMES,
null.isShowRect = !null.isShowRect);
overlay_icon(resources.icons[rectIcon].id, iconTintCurrent);
ImGui::SetItemTooltip("%s", isShowRect ? localize.get(TOOLTIP_NULL_RECT_SHOWN)
: localize.get(TOOLTIP_NULL_RECT_HIDDEN));
if (auto it = anm2.content.nulls.find(id); it != anm2.content.nulls.end())
{
auto& null = it->second;
auto& isShowRect = null.isShowRect;
auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT;
ImGui::SetCursorPos(
ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get()))
DOCUMENT_EDIT(document, localize.get(EDIT_TOGGLE_NULL_RECT), Document::FRAMES,
null.isShowRect = !null.isShowRect);
overlay_icon(resources.icons[rectIcon].id, iconTintCurrent);
ImGui::SetItemTooltip("%s", isShowRect ? localize.get(TOOLTIP_NULL_RECT_SHOWN)
: localize.get(TOOLTIP_NULL_RECT_HIDDEN));
}
}
ImGui::PopStyleVar(2);
@@ -909,11 +979,13 @@ namespace anm2ed::imgui
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::Text("(?)");
ImGui::PopStyleColor();
auto tooltipShortcuts = std::vformat(
localize.get(TOOLTIP_TIMELINE_SHORTCUTS),
std::make_format_args(settings.shortcutMovePlayheadBack, settings.shortcutMovePlayheadForward,
settings.shortcutShortenFrame, settings.shortcutExtendFrame,
settings.shortcutPreviousFrame, settings.shortcutNextFrame));
auto tooltipShortcuts =
std::vformat(localize.get(TOOLTIP_TIMELINE_SHORTCUTS),
std::make_format_args(settings.shortcutMovePlayheadBack,
settings.shortcutMovePlayheadForward, settings.shortcutShortenFrame,
settings.shortcutExtendFrame, settings.shortcutPreviousFrame,
settings.shortcutNextFrame, settings.shortcutPreviousItem,
settings.shortcutNextItem));
ImGui::SetItemTooltip("%s", tooltipShortcuts.c_str());
ImGui::EndDisabled();
}
@@ -1027,6 +1099,96 @@ namespace anm2ed::imgui
ImGui::EndChild();
};
anm2::Type frameMoveDropType = anm2::NONE;
int frameMoveDropItemID = -1;
int frameMoveDropIndex = -1;
bool isFrameMoveDropTarget = false;
auto frame_move_drag_clear = [&]()
{
frameMoveDrag = {};
frameSelectionLocked.clear();
};
auto time_from_index = [](anm2::Item* target, int index)
{
if (!target || target->frames.empty()) return 0.0f;
index = std::clamp(index, 0, (int)target->frames.size());
float timeAccum = 0.0f;
for (int n = 0; n < index && n < (int)target->frames.size(); ++n)
timeAccum += target->frames[n].duration;
return timeAccum;
};
auto frames_move_to = [&](anm2::Type targetType, int targetID, int insertIndex)
{
if (!frameMoveDrag.isActive || !animation || frameMoveDrag.animationIndex != reference.animationIndex) return;
if (frameMoveDrag.type == anm2::TRIGGER || targetType == anm2::TRIGGER) return;
auto sourceItem = animation->item_get(frameMoveDrag.type, frameMoveDrag.itemID);
auto targetItem = animation->item_get(targetType, targetID);
if (!sourceItem || !targetItem) return;
std::vector<int> indices = frameMoveDrag.indices;
if (indices.empty() && frameMoveDrag.frameIndex >= 0) indices.push_back(frameMoveDrag.frameIndex);
std::sort(indices.begin(), indices.end());
indices.erase(std::unique(indices.begin(), indices.end()), indices.end());
indices.erase(std::remove_if(indices.begin(), indices.end(),
[&](int i) { return i < 0 || i >= (int)sourceItem->frames.size(); }),
indices.end());
if (indices.empty()) return;
int insertPosResult = -1;
int insertedCount = 0;
DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_FRAMES), Document::FRAMES, {
std::vector<anm2::Frame> movedFrames;
movedFrames.reserve(indices.size());
for (int i : indices)
movedFrames.push_back(std::move(sourceItem->frames[i]));
for (auto it = indices.rbegin(); it != indices.rend(); ++it)
sourceItem->frames.erase(sourceItem->frames.begin() + *it);
int desired = std::clamp(insertIndex, 0, (int)targetItem->frames.size());
if (sourceItem == targetItem)
{
int removedBefore = 0;
for (int i : indices)
if (i < desired) ++removedBefore;
desired -= removedBefore;
}
desired = std::clamp(desired, 0, (int)targetItem->frames.size());
insertPosResult = desired;
insertedCount = (int)movedFrames.size();
targetItem->frames.insert(targetItem->frames.begin() + insertPosResult,
std::make_move_iterator(movedFrames.begin()),
std::make_move_iterator(movedFrames.end()));
});
if (insertedCount > 0)
{
frames.selection.clear();
for (int offset = 0; offset < insertedCount; ++offset)
frames.selection.insert(insertPosResult + offset);
reference = {reference.animationIndex, targetType, targetID, insertPosResult};
document.frameTime = time_from_index(targetItem, reference.frameIndex);
frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end());
frameSelectionSnapshotReference = reference;
frameSelectionLocked.clear();
isFrameSelectionLocked = false;
frameFocusIndex = reference.frameIndex;
frameFocusRequested = true;
if (targetType == anm2::LAYER)
{
if (auto it = anm2.content.layers.find(targetID); it != anm2.content.layers.end())
document.spritesheet.reference = it->second.spritesheetID;
}
}
};
auto frame_child = [&](anm2::Type type, int id, int& index, float width)
{
auto item = animation ? animation->item_get(type, id) : nullptr;
@@ -1164,7 +1326,7 @@ namespace anm2ed::imgui
{
float frameTime{};
if (ImGui::IsWindowHovered() &&
if (!frameMoveDrag.isActive && ImGui::IsWindowHovered() &&
(ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right)) &&
!ImGui::IsAnyItemHovered())
reference_set_item(type, id);
@@ -1180,7 +1342,65 @@ namespace anm2ed::imgui
drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(frameMultipleOverlayColor));
}
if (type != anm2::TRIGGER) frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape);
bool isFrameSelectionStarted = type != anm2::TRIGGER && !frameMoveDrag.isActive;
if (isFrameSelectionStarted) frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape);
bool isFrameMovePreview = false;
ImVec2 frameMovePreviewMin{};
ImVec2 frameMovePreviewMax{};
bool isFrameMoveHoveredFrame = false;
ImVec2 frameMoveHoveredFrameMin{};
ImVec2 frameMoveHoveredFrameMax{};
if (frameMoveDrag.isActive && type != anm2::TRIGGER)
{
auto mousePos = ImGui::GetIO().MousePos;
auto rowMin = cursorScreenPos;
auto rowMax = ImVec2(cursorScreenPos.x + width, cursorScreenPos.y + frameSize.y);
if (mousePos.x >= rowMin.x && mousePos.x < rowMax.x && mousePos.y >= rowMin.y && mousePos.y < rowMax.y)
{
auto mouseX = mousePos.x - cursorScreenPos.x;
auto targetTime = glm::max(0.0f, mouseX / frameSize.x);
int dropIndex = (int)item->frames.size();
float dropFrameTime{};
float frameTime{};
for (auto [i, frame] : std::views::enumerate(item->frames))
{
auto frameStart = frameTime;
auto frameEnd = frameStart + frame.duration;
if (!isFrameMoveHoveredFrame && targetTime >= frameStart && targetTime < frameEnd)
{
isFrameMoveHoveredFrame = true;
frameMoveHoveredFrameMin = ImVec2(cursorScreenPos.x + frameStart * frameSize.x, cursorScreenPos.y);
frameMoveHoveredFrameMax = ImVec2(cursorScreenPos.x + frameEnd * frameSize.x,
cursorScreenPos.y + frameSize.y);
}
auto midpoint = frameStart + ((float)frame.duration * 0.5f);
if (targetTime < midpoint)
{
dropIndex = (int)i;
dropFrameTime = frameStart;
break;
}
frameTime = frameEnd;
dropFrameTime = frameTime;
}
frameMoveDropType = type;
frameMoveDropItemID = id;
frameMoveDropIndex = dropIndex;
isFrameMoveDropTarget = true;
auto dropX = cursorScreenPos.x + dropFrameTime * frameSize.x;
auto previewWidth = glm::max(frameSize.x, (float)frameMoveDrag.duration * frameSize.x);
frameMovePreviewMin = ImVec2(dropX, cursorScreenPos.y);
frameMovePreviewMax = ImVec2(dropX + previewWidth, cursorScreenPos.y + frameSize.y);
isFrameMovePreview = true;
}
}
for (auto [i, frame] : std::views::enumerate(item->frames))
{
@@ -1229,7 +1449,9 @@ namespace anm2ed::imgui
bool isDifferentItem = reference.itemType != type || reference.itemID != id;
if (ImGui::Selectable("##Frame Button", isSelected, ImGuiSelectableFlags_None, buttonSize))
{
if (type == anm2::LAYER) document.spritesheet.reference = anm2.content.layers[id].spritesheetID;
if (type == anm2::LAYER)
if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end())
document.spritesheet.reference = it->second.spritesheetID;
if (type != anm2::TRIGGER)
{
@@ -1269,131 +1491,60 @@ namespace anm2ed::imgui
if (type != anm2::TRIGGER)
{
if (!draggedFrame && ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip))
if (!draggedFrame && !frameMoveDrag.isActive && ImGui::IsItemActive() &&
ImGui::IsMouseDragging(ImGuiMouseButton_Left))
{
frameDragDrop = {};
frameDragDrop.type = type;
frameDragDrop.itemID = id;
frameDragDrop.animationIndex = reference.animationIndex;
frameSelectionLocked.clear();
auto append_valid_indices = [&](const auto& container)
{
for (auto idx : container)
if (idx >= 0 && idx < (int)item->frames.size()) frameDragDrop.selection.push_back(idx);
if (idx >= 0 && idx < (int)item->frames.size()) frameSelectionLocked.push_back(idx);
};
if (isReferenced) append_valid_indices(frames.selection);
if (frameSelectionSnapshotReference.animationIndex == reference.animationIndex &&
frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id &&
std::find(frameSelectionSnapshot.begin(), frameSelectionSnapshot.end(), (int)i) !=
frameSelectionSnapshot.end())
append_valid_indices(frameSelectionSnapshot);
else if (isReferenced)
append_valid_indices(frames.selection);
auto contains_index = [&](const std::vector<int>& container, int index)
{ return std::find(container.begin(), container.end(), index) != container.end(); };
if ((!contains_index(frameDragDrop.selection, (int)i) || frameDragDrop.selection.size() <= 1) &&
if ((!contains_index(frameSelectionLocked, (int)i) || frameSelectionLocked.size() <= 1) &&
frameSelectionSnapshotReference.animationIndex == reference.animationIndex &&
frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id &&
contains_index(frameSelectionSnapshot, (int)i))
{
frameDragDrop.selection = frameSelectionSnapshot;
frameSelectionLocked = frameSelectionSnapshot;
frames.selection.clear();
for (int idx : frameSelectionSnapshot)
if (idx >= 0 && idx < (int)item->frames.size()) frames.selection.insert(idx);
frameSelectionLocked = frameDragDrop.selection;
isFrameSelectionLocked = true;
}
if (frameDragDrop.selection.empty()) frameDragDrop.selection.push_back((int)i);
if (frameSelectionLocked.empty()) frameSelectionLocked.push_back((int)i);
std::sort(frameDragDrop.selection.begin(), frameDragDrop.selection.end());
frameDragDrop.selection.erase(
std::unique(frameDragDrop.selection.begin(), frameDragDrop.selection.end()),
frameDragDrop.selection.end());
std::sort(frameSelectionLocked.begin(), frameSelectionLocked.end());
frameSelectionLocked.erase(std::unique(frameSelectionLocked.begin(), frameSelectionLocked.end()),
frameSelectionLocked.end());
ImGui::SetDragDropPayload(FRAME_DRAG_PAYLOAD_ID, &frameDragDrop, sizeof(FrameDragDrop));
ImGui::EndDragDropSource();
}
int dragDuration = 0;
for (int idx : frameSelectionLocked)
if (idx >= 0 && idx < (int)item->frames.size()) dragDuration += item->frames[idx].duration;
dragDuration = glm::max(1, dragDuration);
if (!draggedFrame && ImGui::BeginDragDropTarget())
{
if (auto payload = ImGui::AcceptDragDropPayload(FRAME_DRAG_PAYLOAD_ID))
{
auto source = static_cast<const FrameDragDrop*>(payload->Data);
auto sameAnimation = source && source->animationIndex == reference.animationIndex;
auto sourceItem =
sameAnimation && animation ? animation->item_get(source->type, source->itemID) : nullptr;
auto targetItem = animation ? animation->item_get(type, id) : nullptr;
auto time_from_index = [&](anm2::Item* target, int index)
{
if (!target || target->frames.empty()) return 0.0f;
index = std::clamp(index, 0, (int)target->frames.size());
float timeAccum = 0.0f;
for (int n = 0; n < index && n < (int)target->frames.size(); ++n)
timeAccum += target->frames[n].duration;
return timeAccum;
};
if (source && sourceItem && targetItem && source->type != anm2::TRIGGER && type != anm2::TRIGGER)
{
std::vector<int> indices = source->selection;
if (indices.empty()) indices.push_back((int)i);
std::sort(indices.begin(), indices.end());
indices.erase(std::unique(indices.begin(), indices.end()), indices.end());
int insertPosResult = -1;
int insertedCount = 0;
DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_FRAMES), Document::FRAMES, {
std::vector<anm2::Frame> movedFrames;
movedFrames.reserve(indices.size());
for (int i : indices)
if (i >= 0 && i < (int)sourceItem->frames.size())
movedFrames.push_back(std::move(sourceItem->frames[i]));
for (auto it = indices.rbegin(); it != indices.rend(); ++it)
if (*it >= 0 && *it < (int)sourceItem->frames.size())
sourceItem->frames.erase(sourceItem->frames.begin() + *it);
const int dropIndex = (int)i;
int desired = std::clamp(dropIndex + 1, 0, (int)targetItem->frames.size());
if (sourceItem == targetItem)
{
if (dropIndex < indices.front())
desired = dropIndex;
else if (dropIndex > indices.back())
desired = dropIndex + 1;
else
desired = indices.front();
int removedBefore = 0;
for (int i : indices)
if (i < desired) ++removedBefore;
desired -= removedBefore;
}
desired = std::clamp(desired, 0, (int)targetItem->frames.size());
insertPosResult = desired;
insertedCount = (int)movedFrames.size();
targetItem->frames.insert(targetItem->frames.begin() + insertPosResult,
std::make_move_iterator(movedFrames.begin()),
std::make_move_iterator(movedFrames.end()));
});
if (insertedCount > 0)
{
frames.selection.clear();
for (int offset = 0; offset < insertedCount; ++offset)
frames.selection.insert(insertPosResult + offset);
reference = {reference.animationIndex, type, id, insertPosResult};
document.frameTime = time_from_index(targetItem, reference.frameIndex);
if (type == anm2::LAYER)
{
document.spritesheet.reference = anm2.content.layers[id].spritesheetID;
}
}
}
}
ImGui::EndDragDropTarget();
frameMoveDrag = {
.type = type,
.itemID = id,
.animationIndex = reference.animationIndex,
.frameIndex = (int)i,
.duration = dragDuration,
.indices = frameSelectionLocked,
.isActive = true,
};
}
}
@@ -1441,7 +1592,20 @@ namespace anm2ed::imgui
ImGui::PopID();
}
if (type != anm2::TRIGGER) frames.selection.finish();
if (isFrameMovePreview)
{
drawList->AddRectFilled(frameMovePreviewMin, frameMovePreviewMax,
ImGui::GetColorU32(ImGuiCol_DragDropTargetBg), FRAME_ROUNDING);
drawList->AddRect(frameMovePreviewMin, frameMovePreviewMax, ImGui::GetColorU32(ImGuiCol_DragDropTarget),
FRAME_ROUNDING, 0, ImGui::GetStyle().DragDropTargetBorderSize);
}
if (isFrameMoveHoveredFrame)
drawList->AddRect(frameMoveHoveredFrameMin, frameMoveHoveredFrameMax,
ImGui::GetColorU32(ImGuiCol_DragDropTarget), FRAME_ROUNDING, 0,
ImGui::GetStyle().DragDropTargetBorderSize * 1.5f);
if (isFrameSelectionStarted) frames.selection.finish();
if (isFrameSelectionLocked)
{
@@ -1496,6 +1660,7 @@ namespace anm2ed::imgui
draggedFrameStart = -1;
draggedFrameStartDuration = -1;
isDraggedFrameSnapshot = false;
frameSelectionLocked.clear();
if (type == anm2::TRIGGER) item->frames_sort_by_at_frame();
}
}
@@ -1736,6 +1901,11 @@ namespace anm2ed::imgui
isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows |
ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
frames_child();
if (frameMoveDrag.isActive && ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
if (isFrameMoveDropTarget) frames_move_to(frameMoveDropType, frameMoveDropItemID, frameMoveDropIndex);
frame_move_drag_clear();
}
items_child();
}
ImGui::PopStyleVar();
@@ -1858,23 +2028,29 @@ namespace anm2ed::imgui
if (type == anm2::LAYER)
{
auto& layer = anm2.content.layers[id];
auto label =
std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, layer.name, layer.spritesheetID));
if (ImGui::Selectable(label.c_str(), isSelected))
if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end())
{
addItemID = id;
addItemSpritesheetID = layer.spritesheetID;
auto& layer = it->second;
auto label = std::vformat(localize.get(FORMAT_LAYER),
std::make_format_args(id, layer.name, layer.spritesheetID));
if (ImGui::Selectable(label.c_str(), isSelected))
{
addItemID = id;
addItemSpritesheetID = layer.spritesheetID;
}
}
}
else if (type == anm2::NULL_)
{
auto& null = anm2.content.nulls[id];
auto label = std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name));
if (ImGui::Selectable(label.c_str(), isSelected))
if (auto it = anm2.content.nulls.find(id); it != anm2.content.nulls.end())
{
addItemID = id;
addItemIsShowRect = null.isShowRect;
auto& null = it->second;
auto label = std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name));
if (ImGui::Selectable(label.c_str(), isSelected))
{
addItemID = id;
addItemIsShowRect = null.isShowRect;
}
}
}
@@ -2007,23 +2183,28 @@ namespace anm2ed::imgui
auto isPreviousFrame = shortcut(manager.chords[SHORTCUT_PREVIOUS_FRAME], shortcut::GLOBAL);
auto isNextFrame = shortcut(manager.chords[SHORTCUT_NEXT_FRAME], shortcut::GLOBAL);
auto isPreviousItem = shortcut(manager.chords[SHORTCUT_PREVIOUS_ITEM], shortcut::GLOBAL);
auto isNextItem = shortcut(manager.chords[SHORTCUT_NEXT_ITEM], shortcut::GLOBAL);
if (isPreviousFrame)
if (auto item = document.item_get(); !item->frames.empty())
if (auto item = document.item_get(); item && !item->frames.empty())
reference.frameIndex = glm::clamp(--reference.frameIndex, 0, (int)item->frames.size() - 1);
if (isNextFrame)
if (auto item = document.item_get(); !item->frames.empty())
if (auto item = document.item_get(); item && !item->frames.empty())
reference.frameIndex = glm::clamp(++reference.frameIndex, 0, (int)item->frames.size() - 1);
if (isPreviousFrame || isNextFrame)
{
if (auto item = document.item_get(); !item->frames.empty())
if (auto item = document.item_get(); item && !item->frames.empty())
{
frames_selection_set_reference();
document.frameTime = item->frame_time_from_index_get(reference.frameIndex);
}
}
if (isPreviousItem) reference_set_adjacent_item(-1);
if (isNextItem) reference_set_adjacent_item(1);
}
if (isTextPushed) ImGui::PopStyleColor();
+6 -3
View File
@@ -10,12 +10,15 @@
namespace anm2ed::imgui
{
struct FrameDragDrop
struct FrameMoveDrag
{
anm2::Type type{anm2::NONE};
int itemID{-1};
int animationIndex{-1};
std::vector<int> selection{};
int frameIndex{-1};
int duration{1};
std::vector<int> indices{};
bool isActive{};
};
class Timeline
@@ -38,7 +41,7 @@ namespace anm2ed::imgui
bool isDraggedFrameSnapshot{};
bool frameFocusRequested{};
int frameFocusIndex{-1};
FrameDragDrop frameDragDrop{};
FrameMoveDrag frameMoveDrag{};
std::vector<int> frameSelectionSnapshot{};
std::vector<int> frameSelectionLocked{};
bool isFrameSelectionLocked{};
@@ -1,5 +1,6 @@
#include "change_all_frame_properties.hpp"
#include <algorithm>
#include <string>
#include <vector>
@@ -48,7 +49,7 @@ namespace anm2ed::imgui::wizard
auto& duration = settings.changeDuration;
auto& tint = settings.changeTint;
auto& colorOffset = settings.changeColorOffset;
auto& regionId = settings.changeRegionId;
auto& regionId = document.changeAllFramePropertiesRegionId;
auto& isVisible = settings.changeIsVisible;
auto& interpolation = settings.changeInterpolation;
auto& isFlipX = settings.changeIsFlipX;
@@ -189,6 +190,11 @@ namespace anm2ed::imgui::wizard
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImGui::GetStyle().ItemInnerSpacing);
ImGui::BeginDisabled(itemType != anm2::LAYER);
float2_value("##Is Crop X", "##Is Crop Y", "##Crop X", localize.get(BASIC_CROP), isCropX, isCropY, crop);
float2_value("##Is Size X", "##Is Size Y", "##Size X", localize.get(BASIC_SIZE), isSizeX, isSizeY, size);
ImGui::EndDisabled();
float2_value("##Is Position X", "##Is Position Y", "##Position X", localize.get(BASIC_POSITION), isPositionX,
isPositionY, position);
@@ -196,11 +202,6 @@ namespace anm2ed::imgui::wizard
float2_value("##Is Pivot X", "##Is Pivot Y", "##Pivot X", localize.get(BASIC_PIVOT), isPivotX, isPivotY, pivot);
ImGui::EndDisabled();
ImGui::BeginDisabled(itemType != anm2::LAYER);
float2_value("##Is Crop X", "##Is Crop Y", "##Crop X", localize.get(BASIC_CROP), isCropX, isCropY, crop);
float2_value("##Is Size X", "##Is Size Y", "##Size X", localize.get(BASIC_SIZE), isSizeX, isSizeY, size);
ImGui::EndDisabled();
float2_value("##Is Scale X", "##Is Scale Y", "##Scale X", localize.get(BASIC_SCALE), isScaleX, isScaleY, scale);
float_value("##Is Rotation", localize.get(BASIC_ROTATION), isRotation, rotation);
@@ -233,12 +234,17 @@ namespace anm2ed::imgui::wizard
const Storage* regionStorage = nullptr;
if (itemType == anm2::LAYER && document.reference.itemID != -1)
{
auto spritesheetID = document.anm2.content.layers.at(document.reference.itemID).spritesheetID;
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
if (regionIt != document.regionBySpritesheet.end()) regionStorage = &regionIt->second;
if (auto layerIt = document.anm2.content.layers.find(document.reference.itemID);
layerIt != document.anm2.content.layers.end())
{
auto spritesheetID = layerIt->second.spritesheetID;
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
if (regionIt != document.regionBySpritesheet.end()) regionStorage = &regionIt->second;
}
}
auto regionIds = regionStorage && !regionStorage->ids.empty() ? regionStorage->ids : fallbackIds;
auto regionLabels = regionStorage && !regionStorage->labels.empty() ? regionStorage->labels : fallbackLabels;
if (itemType != anm2::LAYER || std::find(regionIds.begin(), regionIds.end(), regionId) == regionIds.end()) regionId = -1;
PROPERTIES_WIDGET(combo_id_mapped(localize.get(BASIC_REGION), &regionId, regionIds, regionLabels), "##Is Region",
isRegion);
ImGui::EndDisabled();
@@ -287,10 +293,13 @@ namespace anm2ed::imgui::wizard
if (isFlipXSet) frameChange.isFlipX = std::make_optional(isFlipX);
if (isFlipYSet) frameChange.isFlipY = std::make_optional(isFlipY);
DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES,
document.item_get()->frames_change(frameChange, itemType, changeType, frames));
if (auto item = document.item_get())
{
DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES,
item->frames_change(frameChange, itemType, changeType, frames));
isChanged = true;
isChanged = true;
}
};
ImGui::Separator();
+6
View File
@@ -72,9 +72,15 @@ namespace anm2ed::imgui::wizard
ImGui::RadioButton(localize.get(LABEL_ANM2ED_LIMITED), &temporary.fileCompatibility, anm2::ANM2ED_LIMITED);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED_LIMITED));
ImGui::Checkbox(localize.get(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_ON_SAVE),
&temporary.fileIsSpecialInterpolatedFramesOnSaveReminder);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SPECIAL_INTERPOLATED_FRAMES_REMINDER_ON_SAVE));
ImGui::BeginDisabled(temporary.fileIsSpecialInterpolatedFramesOnSaveReminder);
ImGui::Checkbox(localize.get(LABEL_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE),
&temporary.fileBakeSpecialInterpolatedFramesOnSave);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE));
ImGui::EndDisabled();
ImGui::SeparatorText(localize.get(LABEL_OPTIONS));
ImGui::Checkbox(localize.get(LABEL_OVERWRITE_WARNING), &temporary.fileIsWarnOverwrite);
@@ -57,21 +57,27 @@ namespace anm2ed::imgui::wizard
if (document.reference.itemType == anm2::LAYER)
{
auto& texture =
document.anm2.content.spritesheets[document.anm2.content.layers[document.reference.itemID].spritesheetID]
.texture;
auto layerIt = document.anm2.content.layers.find(document.reference.itemID);
if (layerIt != document.anm2.content.layers.end())
{
auto spritesheetIt = document.anm2.content.spritesheets.find(layerIt->second.spritesheetID);
if (spritesheetIt != document.anm2.content.spritesheets.end())
{
auto& texture = spritesheetIt->second.texture;
auto index = std::clamp((int)(time * (count - 1)), 0, (count - 1));
auto row = index / columns;
auto column = index % columns;
auto crop = startPosition + ivec2(size.x * column, size.y * row);
auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size);
auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size);
auto index = std::clamp((int)(time * (count - 1)), 0, (count - 1));
auto row = index / columns;
auto column = index % columns;
auto crop = startPosition + ivec2(size.x * column, size.y * row);
auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size);
auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size);
mat4 transform = transform_get(zoom) * math::quad_model_get(size, {}, pivot);
mat4 transform = transform_get(zoom) * math::quad_model_get(size, {}, pivot);
texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {},
math::uv_vertices_get(uvMin, uvMax).data());
texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {},
math::uv_vertices_get(uvMin, uvMax).data());
}
}
}
unbind();
@@ -112,4 +118,4 @@ namespace anm2ed::imgui::wizard
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) isEnd = true;
}
}
}
+6 -2
View File
@@ -199,8 +199,10 @@ namespace anm2ed
{
if (id == -1)
editLayer = anm2::Layer();
else if (auto it = document->anm2.content.layers.find(id); it != document->anm2.content.layers.end())
editLayer = it->second;
else
editLayer = document->anm2.content.layers.at(id);
return;
document->layer.reference = id;
@@ -224,8 +226,10 @@ namespace anm2ed
{
if (id == -1)
editNull = anm2::Null();
else if (auto it = document->anm2.content.nulls.find(id); it != document->anm2.content.nulls.end())
editNull = it->second;
else
editNull = document->anm2.content.nulls.at(id);
return;
document->null.reference = id;
+12 -2
View File
@@ -244,7 +244,14 @@ namespace anm2ed
X(LABEL_CLEAR_LIST, "Clear List", "Limpiar Lista", "Стереть список", "清除列表", "기록 삭제") \
X(LABEL_CLOSE, "Close", "Cerrar", "Закрыть", "关闭", "닫기") \
X(LABEL_COMPATIBILITY, "Compatibility", "Compatibilidad", "Совместимость", "兼容性", "호환성") \
X(LABEL_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, "Bake Special Interpolated Frames On Saving", "Hornear frames con interpolación especial al guardar", "Запекать кадры со специальной интерполяцией при сохранении", "保存时烘焙特殊插值帧", "저장 시 특수 보간 프레임 베이크") \
X(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_ON_SAVE, "Remind me when saving a document with frames with special interpolation modes", "Recordatorio de frames con interpolación especial al guardar", "Напоминание о кадрах со специальной интерполяцией при сохранении", "保存时特殊插值帧提醒", "저장 시 특수 보간 프레임 알림") \
X(LABEL_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, "Automatically bake frames with special interpolation modes", "Hornear frames con interpolación especial al guardar", "Запекать кадры со специальной интерполяцией при сохранении", "保存时烘焙特殊插值帧", "저장 시 특수 보간 프레임 베이크") \
X(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_POPUP, "Special Interpolated Frames Reminder", "Recordatorio de frames con interpolación especial", "Напоминание о кадрах со специальной интерполяцией", "特殊插值帧提醒", "특수 보간 프레임 알림") \
X(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_PROMPT, "Your document includes frames with special interpolation modes. The Binding of Isaac: Rebirth does not support these special interpolation modes for frames and animations will not be rendered correctly. However, these frames can be baked to maintain the animation. Would you like to automatically bake these frames now?", "Su documento incluye frames con modos de interpolación especial. The Binding of Isaac: Rebirth no admite estos modos de interpolación especial para frames y las animaciones no se renderizarán correctamente. Sin embargo, estos frames pueden hornearse para mantener la animación. ¿Desea hornear automáticamente estos frames ahora?", "Ваш документ содержит кадры со специальными режимами интерполяции. The Binding of Isaac: Rebirth не поддерживает эти специальные режимы интерполяции для кадров, и анимации не будут отображаться корректно. Однако эти кадры можно запечь, чтобы сохранить анимацию. Хотите автоматически запечь эти кадры сейчас?", "您的文档包含使用特殊插值模式的帧。The Binding of Isaac: Rebirth 不支持这些帧的特殊插值模式,动画将无法正确渲染。不过,这些帧可以通过烘焙来保持动画效果。是否要立即自动烘焙这些帧?", "문서에 특수 보간 모드를 사용하는 프레임이 포함되어 있습니다. The Binding of Isaac: Rebirth는 이러한 프레임의 특수 보간 모드를 지원하지 않으며 애니메이션이 올바르게 렌더링되지 않습니다. 하지만 이러한 프레임은 애니메이션을 유지하도록 베이크할 수 있습니다. 지금 이 프레임들을 자동으로 베이크하시겠습니까?") \
X(LABEL_DONT_NOTIFY_ME_AGAIN, "Don't notify me again", "No volver a notificarme", "Больше не уведомлять", "不再提醒我", "다시 알리지 않기") \
X(LABEL_AUTOMATICALLY_BAKE_THESE_FRAMES_ON_SAVE, "Automatically bake these frames on save", "Hornear automáticamente estos frames al guardar", "Автоматически запекать эти кадры при сохранении", "保存时自动烘焙这些帧", "저장 시 이 프레임 자동 베이크") \
X(LABEL_SAVE_BAKE_FRAMES, "Save (Bake Frames)", "Guardar (Hornear frames)", "Сохранить (Запечь кадры)", "保存(烘焙帧)", "저장 (프레임 베이크)") \
X(LABEL_SAVE_DONT_BAKE_FRAMES, "Save (Don't Bake Frames)", "Guardar (No hornear frames)", "Сохранить (Не запекать кадры)", "保存(不烘焙帧)", "저장 (프레임을 베이크하지 않음)") \
X(LABEL_CUSTOM_RANGE, "Custom Range", "Rango Personalizado", "Пользовательский диапазон", "自定义范围", "길이 맞춤설정") \
X(LABEL_DELETE, "Delete", "Borrar", "Удалить", "删除", "삭제") \
X(LABEL_DELETE_ANIMATIONS_AFTER, "Delete Animations After", "Borrar Animaciones Despues", "Удалить анимации после", "删除之后的动画", "기존 애니메이션 삭제") \
@@ -413,6 +420,7 @@ namespace anm2ed
X(SHORTCUT_STRING_NEW, "New", "Nuevo", "Новый", "", "새 파일") \
X(SHORTCUT_STRING_NEXT_ANIMATION, "Next Animation", "Siguiente Animacion", "Следующая анимация", "下一动画", "다음 애니메이션") \
X(SHORTCUT_STRING_NEXT_FRAME, "Next Frame", "Siguiente Frame", "Следующий кадр", "下一帧", "다음 프레임") \
X(SHORTCUT_STRING_NEXT_ITEM, "Next Item", "Siguiente Item", "Следующий предмет", "下一物品", "다음 항목") \
X(SHORTCUT_STRING_ONIONSKIN, "Onionskin", "Papel Cebolla", "Оньонскин", "洋葱皮预览", "전후 비교") \
X(SHORTCUT_STRING_OPEN, "Open", "Abrir", "Открыть", "打开", "열기") \
X(SHORTCUT_STRING_PAN, "Pan", "Panoramico", "Панорамирование", "平移", "시점 이동") \
@@ -422,6 +430,7 @@ namespace anm2ed
X(SHORTCUT_STRING_PLAY_PAUSE, "Play/Pause", "Reproducir/Pausar", "Возпроизвести/Пауза", "播放/暂停", "재생/일시정지") \
X(SHORTCUT_STRING_PREVIOUS_ANIMATION, "Previous Animation", "Animacion Anterior", "Предыдущая анимация", "上一动画", "이전 애니메이션") \
X(SHORTCUT_STRING_PREVIOUS_FRAME, "Previous Frame", "Frame Anterior", "Предыдущий кадр", "前一帧", "이전 프레임") \
X(SHORTCUT_STRING_PREVIOUS_ITEM, "Previous Item", "Item Anterior", "Предыдущий предмет", "上一物品", "이전 항목") \
X(SHORTCUT_STRING_REDO, "Redo", "Rehacer", "Повторить", "重做", "다시 실행") \
X(SHORTCUT_STRING_RENAME, "Rename", "Renombrar", "Переименовать", "重命名", "이름 변경") \
X(SHORTCUT_STRING_REMOVE, "Remove", "Remover", "Удалить", "去除", "제거") \
@@ -526,6 +535,7 @@ namespace anm2ed
X(TOOLTIP_COMPATIBILITY_ISAAC, "Sets the output file format to that of The Binding of Isaac: Rebirth's.\nThis removes the following:\n- Sounds\n- Regions\n- Special interpolation modes\nNOTE: This will not serialize this data and it won't be able to be recovered.", "Establece el formato del archivo de salida al de The Binding of Isaac: Rebirth.\nEsto elimina lo siguiente:\n- Sonidos\n- Regiones\n- Modos especiales de interpolación\nNOTA: Estos datos no se serializaran y no podran recuperarse.", "Устанавливает формат выходного файла как у The Binding of Isaac: Rebirth.\nЭто удаляет следующее:\n- Звуки\n- Регионы\n- Специальные режимы интерполяции\nПРИМЕЧАНИЕ: Эти данные не будут сериализованы, и их нельзя будет восстановить.", "将输出文件格式设置为 The Binding of Isaac: Rebirth 的格式。\n这会移除以下内容:\n- 声音\n- 区域\n- 特殊插值模式\n注意:这些数据不会被序列化,且无法恢复。", "출력 파일 형식을 The Binding of Isaac: Rebirth의 형식으로 설정합니다.\n다음 항목이 제거됩니다:\n- 사운드\n- 영역\n- 특수 보간 모드\n참고: 이 데이터는 직렬화되지 않으며 복구할 수 없습니다.") \
X(TOOLTIP_COMPATIBILITY_ANM2ED, "Sets the output file format to that of this editor.\nAll features will be serialized, including:\n- Sounds\n- Regions\n- Special interpolation modes", "Establece el formato del archivo de salida al de este editor.\nTodas las funciones se serializaran, incluyendo:\n- Sonidos\n- Regiones\n- Modos especiales de interpolación", "Устанавливает формат выходного файла как у этого редактора.\nВсе возможности будут сериализованы, включая:\n- Звуки\n- Регионы\n- Специальные режимы интерполяции", "将输出文件格式设置为本编辑器的格式。\n所有功能都会被序列化,包括:\n- 声音\n- 区域\n- 特殊插值模式", "출력 파일 형식을 이 편집기의 형식으로 설정합니다.\n다음 기능을 포함한 모든 기능이 직렬화됩니다:\n- 사운드\n- 영역\n- 특수 보간 모드") \
X(TOOLTIP_COMPATIBILITY_ANM2ED_LIMITED, "Sets the output file format to that of this editor.\nThis will additionally remove redundant Region-specific information in frames.", "Establece el formato del archivo de salida al de este editor.\nEsto ademas eliminara informacion redundante especifica de Region en los frames.", "Устанавливает формат выходного файла как у этого редактора.\nДополнительно это удалит избыточную информацию, связанную с регионами, в кадрах.", "将输出文件格式设置为本编辑器的格式。\n此外,这还会移除帧中冗余的区域专用信息。", "출력 파일 형식을 이 편집기의 형식으로 설정합니다.\n추가로 프레임 내 중복된 영역 관련 정보를 제거합니다.") \
X(TOOLTIP_SPECIAL_INTERPOLATED_FRAMES_REMINDER_ON_SAVE, "When manually saving a document with special interpolation modes, display a reminder before saving and let you choose whether to bake those frames.", "Al guardar manualmente un documento con modos de interpolación especial, mostrar un recordatorio antes de guardar y permitir elegir si se hornean esos frames.", "При ручном сохранении документа со специальными режимами интерполяции показывать напоминание перед сохранением и позволять выбрать, нужно ли запекать эти кадры.", "手动保存包含特殊插值模式的文档时,在保存前显示提醒,并允许您选择是否烘焙这些帧。", "특수 보간 모드가 있는 문서를 수동 저장할 때 저장 전에 알림을 표시하고 해당 프레임을 베이크할지 선택할 수 있습니다.") \
X(TOOLTIP_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, "When saving, frames that do not use the None or Linear interpolation modes will automatically be baked in-place when saving the file.\nThe Binding of Isaac: Rebirth does not support extended interpolation features.", "Al guardar, los frames que no usen los modos de interpolación None o Linear se hornearán automáticamente en su lugar al guardar el archivo.\nThe Binding of Isaac: Rebirth no soporta funciones extendidas de interpolación.", "При сохранении кадры, которые не используют режимы интерполяции None или Linear, будут автоматически запекаться на месте при сохранении файла.\nThe Binding of Isaac: Rebirth не поддерживает расширенные возможности интерполяции.", "保存文件时,不使用 None 或 Linear 插值模式的帧将自动在保存时原地烘焙。\nThe Binding of Isaac: Rebirth 不支持扩展插值功能。", "저장할 때 None 또는 Linear 보간 모드를 사용하지 않는 프레임은 파일 저장 시 자동으로 제자리 베이크됩니다.\nThe Binding of Isaac: Rebirth는 확장 보간 기능을 지원하지 않습니다.") \
X(TOOLTIP_COLOR_OFFSET, "Change the color added onto the frame.", "Cambia el color añadido al Frame.", "Изменить цвет, который добавлен на кадр.", "更改覆盖在帧上的颜色.", "프레임에 더해지는 색을 변경합니다.") \
X(TOOLTIP_REGION, "Set the spritesheet region the frame will use.", "Establece la región del spritesheet que usará el frame.", "Установить регион спрайт-листа, который будет использовать кадр.", "设置帧将使用的图集区域.", "프레임이 사용할 스프라이트 시트 영역을 설정합니다.") \
@@ -637,7 +647,7 @@ namespace anm2ed
X(TOOLTIP_STACK_SIZE, "Set the maximum snapshot stack size of a document (i.e., how many undo/redos are preserved at a time).", "Ajusta el tamaño maximo del stack de snapshot de un documento (i. e., cuantos deshacer/rehacer se preservan a lo largo del tiempo.", "Установить максимальный размер стека снимков документа (т. е. количество отмен/повторов, сохраняемых одновременно).", "设置文件的快照栈的最大存储空间. (也就是最大可以存储多少撤销与重做)", "파일의 최대 스냅숏 스택 크기(즉 한 번에 보존되는 실행 취소/다시 실행 수)를 설정합니다.") \
X(TOOLTIP_START, "Set the starting time of the animation.", "Ajusta el tiempo de inicio de la animacion.", "Установить начальное время анимации.", "设置动画的起始时间.", "애니메이션의 시작 시간을 설정합니다.") \
X(TOOLTIP_SUBTRACT_VALUES, "Subtract the specified values from each frame.\n(Boolean/mapped values will simply be set.)", "Subtrae los valores especificos de cada Frame.\n(Los valores booleanos/mapeados simplemente se estableceran.)", "Вычтить указанные значения из каждого кадра.\n(Булевы/сопоставленные значения будут просто установлены.)", "将每一帧的指定值进行相减操作. (布尔/映射值将直接被设置.)", "각 프레임의 속성에 지정한 값을 뺍니다.\n(불리언/매핑 값은 그대로 설정됩니다.)") \
X(TOOLTIP_TIMELINE_SHORTCUTS, "- Press {0} to decrement time.\n- Press {1} to increment time.\n- Press {2} to shorten the selected frame, by one frame.\n- Press {3} to extend the selected frame, by one frame.\n- Press {4} to go to the previous frame.\n- Press {5} to go to the next frame.\n- Click and hold on a frame while holding CTRL to change its duration.\n- Click and hold on a trigger to change its At Frame.\n- Hold Alt while clicking a non-trigger frame to toggle interpolation.", "- Presiona{0} para reducir el tiempo.\n- Presiona {1} para incrementar el tiempo.\n- Presiona {2} para acortar el Frame selecionado, por uno.\n- Presiona {3} para extender el Frame seleccionado, por uno.\n- Presiona {4} para ir al Frame anterior.\n- Presiona {5} para ir al siguiente Frame.\n- Haz click y mantiene en un Frame mientras apretas CTRL para cambiar su duracion.\n- Haz click y mantiene en un trigger para cambiar su \"En Frame\".\n- Manten Alt mientras haces click en un frame sin trigger para alternar la interpolacion.", "- Нажмите {0}, чтобы уменьшить время.\n- Нажмите {1}, чтобы увеличить время.\n- Нажмите {2}, чтобы укоротить выбранный кадр одной мерной единицей.\n- Нажмите {3}, чтобы продлить выбранный кадр на одну мерную единицу.\n- Нажмите {4}, чтобы перейти к предыдущему кадру.\n- Нажмите {5}, чтобы перейти к следующему кадру.\n- Удерживайте нажатой кнопку мыши по кадру, удерживая CTRL, чтобы изменить его длительность.\n- Нажмите и удерживайте кнопку мыши по триггеру, чтобы изменить параметр «На кадре».\n- Удерживайте Alt и нажмите по кадру, который не является триггером, чтобы переключить интерполяцию.", "- 按下 {0} 减少时间.\n- 按下 {1} 增加时间.\n- 按下 {2} 将所选帧缩短一帧.\n- 按下 {3} 将所选帧延长一帧.\n- 按下 {4} 跳到上一帧.\n- 按下 {5} 跳到下一帧.\n- 在按住 CTRL 的同时点击并按住某一帧以更改其持续时间.\n- 点击并按住触发器即可更改其触发帧.\n- 按住 Alt 并点击任何无触发器的帧以切换线性插值的使用.", "- {0} 키: 플레이헤드를 뒤로 보냅니다.\n- {1} 키: 플레이헤드를 앞으로 보냅니다.\n- {2} 키: 선택한 프레임을 한 프레임 단축합니다.\n- {3} 키: 선택한 프레임을 한 프레임 연장합니다.\n- {4} 키: 이전 프레임을 선택합니다.\n- {5} 키: 다음 프레임을 선택합니다.\n- CTRL 키를 누른 채 프레임을 클릭하고 드래그하면 프레임의 유지 시간을 변경할 수 있습니다.\n- 트리거를 클릭하고 드래그하면 트리거의 시작 프레임을 변경할 수 있습니다.\n- Alt 키를 누른 채 트리거를 제외한 프레임을 클릭하면 매끄럽게 연결 설정을 켤 수 있습니다.") \
X(TOOLTIP_TIMELINE_SHORTCUTS, "- Press {0}/{1} to move the playhead backward/forward.\n- Press {2}/{3} to shorten/extend the selected frame by one frame.\n- Press {4}/{5} to select the previous/next frame.\n- Press {6}/{7} to select the previous/next item.\n- Click and hold on a frame while holding CTRL to change its duration.\n- Click and hold on a trigger to change its At Frame.\n- Hold Alt while clicking a non-trigger frame to toggle interpolation.", "- Presiona {0}/{1} para mover el cabezal hacia atras/adelante.\n- Presiona {2}/{3} para acortar/extender el Frame seleccionado por uno.\n- Presiona {4}/{5} para seleccionar el Frame anterior/siguiente.\n- Presiona {6}/{7} para seleccionar el Item anterior/siguiente.\n- Haz click y mantiene en un Frame mientras apretas CTRL para cambiar su duracion.\n- Haz click y mantiene en un trigger para cambiar su \"En Frame\".\n- Manten Alt mientras haces click en un frame sin trigger para alternar la interpolacion.", "- Нажмите {0}/{1}, чтобы переместить воспроизводящую головку назад/вперёд.\n- Нажмите {2}/{3}, чтобы укоротить/продлить выбранный кадр на одну мерную единицу.\n- Нажмите {4}/{5}, чтобы выбрать предыдущий/следующий кадр.\n- Нажмите {6}/{7}, чтобы выбрать предыдущий/следующий предмет.\n- Удерживайте нажатой кнопку мыши по кадру, удерживая CTRL, чтобы изменить его длительность.\n- Нажмите и удерживайте кнопку мыши по триггеру, чтобы изменить параметр «На кадре».\n- Удерживайте Alt и нажмите по кадру, который не является триггером, чтобы переключить интерполяцию.", "- 按下 {0}/{1} 可向后/向前移动播放指针.\n- 按下 {2}/{3} 将所选帧缩短/延长一帧.\n- 按下 {4}/{5} 可选择上一帧/下一帧.\n- 按下 {6}/{7} 可选择上一物品/下一物品.\n- 在按住 CTRL 的同时点击并按住某一帧以更改其持续时间.\n- 点击并按住触发器即可更改其触发帧.\n- 按住 Alt 并点击任何无触发器的帧以切换线性插值的使用.", "- {0}/{1} 키: 플레이헤드를 뒤로/앞으로 보냅니다.\n- {2}/{3} 키: 선택한 프레임을 한 프레임 단축/연장합니다.\n- {4}/{5} 키: 이전/다음 프레임을 선택합니다.\n- {6}/{7} 키: 이전/다음 항목을 선택합니다.\n- CTRL 키를 누른 채 프레임을 클릭하고 드래그하면 프레임의 유지 시간을 변경할 수 있습니다.\n- 트리거를 클릭하고 드래그하면 트리거의 시작 프레임을 변경할 수 있습니다.\n- Alt 키를 누른 채 트리거를 제외한 프레임을 클릭하면 매끄럽게 연결 설정을 켤 수 있습니다.") \
X(TOOLTIP_TINT, "Change the tint of the frame.", "Cambia el matiz del Frame", "Изменить оттенок кадра.", "更改此帧的色调.", "프레임의 색조를 변경합니다.") \
X(TOOLTIP_TOOL_COLOR, "Selects the color to be used for drawing.\n(Spritesheet Editor only.)", "Selecciona el color que se usara para dibujar.\n(Solo en el Editor de Spritesheet.)", "Выбирает цвет, который будет использоваться для рисования.\n(Только в редакторе спрайт-листов.)", "选择用于绘画的颜色.\n(仅应用于图集编辑器.)", "그리기용으로 사용할 색을 선택합니다.\n(스프라이트 시트 편집기 전용)") \
X(TOOLTIP_TOOL_COLOR_PICKER, "Selects a color from the canvas.\n(Spritesheet Editor only.)", "Selecciona un color del lienzo.\n(Solo en el Editor de Spritesheet.)", "Выбирает цвет из холста.\n(Только в редакторе спрайт-листов.)", "从画板上选择一个颜色.\n(仅应用于图集编辑器.)", "캔버스에서 색을 선택합니다.\n(스프라이트시트 편집기 전용)") \
+5 -1
View File
@@ -61,6 +61,7 @@ namespace anm2ed
\
X(FILE_IS_AUTOSAVE, fileIsAutosave, STRING_UNDEFINED, BOOL, true) \
X(FILE_IS_WARN_OVERWRITE, fileIsWarnOverwrite, STRING_UNDEFINED, BOOL, true) \
X(FILE_IS_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE_REMINDER, fileIsSpecialInterpolatedFramesOnSaveReminder, STRING_UNDEFINED, BOOL, true) \
X(FILE_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, fileBakeSpecialInterpolatedFramesOnSave, STRING_UNDEFINED, BOOL, true) \
X(FILE_SNAPSHOT_STACK_SIZE, fileSnapshotStackSize, STRING_UNDEFINED, INT, 50) \
X(FILE_COMPATIBILITY, fileCompatibility, STRING_UNDEFINED, INT, anm2::ANM2ED) \
@@ -239,7 +240,10 @@ namespace anm2ed
X(SHORTCUT_INSERT_FRAME, shortcutInsertFrame, SHORTCUT_STRING_INSERT_FRAME, STRING, "F6") \
X(SHORTCUT_PREVIOUS_FRAME, shortcutPreviousFrame, SHORTCUT_STRING_PREVIOUS_FRAME, STRING, "F7") \
X(SHORTCUT_NEXT_FRAME, shortcutNextFrame, SHORTCUT_STRING_NEXT_FRAME, STRING, "F8") \
/* Animations */ \
/* Item */ \
X(SHORTCUT_PREVIOUS_ITEM, shortcutPreviousItem, SHORTCUT_STRING_PREVIOUS_ITEM, STRING, "Shift+F7") \
X(SHORTCUT_NEXT_ITEM, shortcutNextItem, SHORTCUT_STRING_NEXT_ITEM, STRING, "Shift+F8") \
/* Animation */ \
X(SHORTCUT_PREVIOUS_ANIMATION, shortcutPreviousAnimation, SHORTCUT_STRING_PREVIOUS_ANIMATION, STRING, "F9") \
X(SHORTCUT_NEXT_ANIMATION, shortcutNextAnimation, SHORTCUT_STRING_NEXT_ANIMATION, STRING, "F10") \
/* Toggles */ \
+9 -2
View File
@@ -1,6 +1,7 @@
#include "socket.hpp"
#include <cerrno>
#include <climits>
namespace anm2ed
{
@@ -203,7 +204,12 @@ namespace anm2ed
while (totalSent < size)
{
auto sent = ::send(handle, bytes + totalSent, static_cast<int>(size - totalSent), 0);
auto chunkSize = static_cast<int>(std::min<size_t>(size - totalSent, static_cast<size_t>(INT_MAX)));
#ifdef MSG_NOSIGNAL
auto sent = ::send(handle, bytes + totalSent, chunkSize, MSG_NOSIGNAL);
#else
auto sent = ::send(handle, bytes + totalSent, chunkSize, 0);
#endif
if (sent <= 0)
{
lastError = socket_last_error();
@@ -229,7 +235,8 @@ namespace anm2ed
while (totalReceived < size)
{
auto received = ::recv(handle, bytes + totalReceived, static_cast<int>(size - totalReceived), 0);
auto chunkSize = static_cast<int>(std::min<size_t>(size - totalReceived, static_cast<size_t>(INT_MAX)));
auto received = ::recv(handle, bytes + totalReceived, chunkSize, 0);
if (received <= 0)
{
lastError = socket_last_error();
+81 -6
View File
@@ -15,16 +15,93 @@ namespace anm2ed::util::xml
return std::string(printer.CStr());
}
bool query_string_attribute(const XMLElement* element, const char* attribute, std::string& out)
{
if (!element || !attribute) return false;
const char* value = nullptr;
if (element->QueryStringAttribute(attribute, &value) != XML_SUCCESS || !value) return false;
out = value;
return true;
}
bool query_path_attribute(const XMLElement* element, const char* attribute, std::filesystem::path& out)
{
std::string temp{};
if (!query_string_attribute(element, attribute, temp)) return false;
out = path::from_utf8(temp);
return true;
}
bool query_int_attribute(const XMLElement* element, const char* attribute, int& out)
{
return element && attribute && element->QueryIntAttribute(attribute, &out) == XML_SUCCESS;
}
bool query_float_attribute(const XMLElement* element, const char* attribute, float& out)
{
return element && attribute && element->QueryFloatAttribute(attribute, &out) == XML_SUCCESS;
}
bool query_bool_attribute(const XMLElement* element, const char* attribute, bool& out)
{
return element && attribute && element->QueryBoolAttribute(attribute, &out) == XML_SUCCESS;
}
bool query_color_attribute(const XMLElement* element, const char* attribute, float& out)
{
int value{};
if (!query_int_attribute(element, attribute, value)) return false;
out = math::uint8_to_float(value);
return true;
}
void set_string_attribute(XMLElement* element, const char* attribute, const std::string& value)
{
if (element && attribute && !value.empty()) element->SetAttribute(attribute, value.c_str());
}
void set_path_attribute(XMLElement* element, const char* attribute, const std::filesystem::path& value)
{
if (!element || !attribute || value.empty()) return;
auto utf8 = path::to_utf8(value);
element->SetAttribute(attribute, utf8.c_str());
}
void set_int_attribute(XMLElement* element, const char* attribute, int value)
{
if (element && attribute) element->SetAttribute(attribute, value);
}
void set_float_attribute(XMLElement* element, const char* attribute, float value)
{
if (element && attribute) element->SetAttribute(attribute, value);
}
void set_bool_attribute(XMLElement* element, const char* attribute, bool value)
{
if (element && attribute) element->SetAttribute(attribute, value);
}
void set_color_attribute(XMLElement* element, const char* attribute, float value)
{
if (element && attribute) element->SetAttribute(attribute, math::float_to_uint8(value));
}
XMLError query_string_attribute(XMLElement* element, const char* attribute, std::string* out)
{
const char* temp = nullptr;
auto result = element->QueryStringAttribute(attribute, &temp);
if (result == XML_SUCCESS && temp) *out = temp;
if (!element || !attribute || !out) return XML_NO_ATTRIBUTE;
const char* value = nullptr;
auto result = element->QueryStringAttribute(attribute, &value);
if (result == XML_SUCCESS && value) *out = value;
return result;
}
XMLError query_path_attribute(XMLElement* element, const char* attribute, std::filesystem::path* out)
{
if (!out) return XML_NO_ATTRIBUTE;
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
if (result == XML_SUCCESS) *out = path::from_utf8(temp);
@@ -33,8 +110,6 @@ namespace anm2ed::util::xml
void query_color_attribute(XMLElement* element, const char* attribute, float& out)
{
int value{};
element->QueryIntAttribute(attribute, &value);
out = math::uint8_to_float(value);
query_color_attribute(static_cast<const XMLElement*>(element), attribute, out);
}
}
+15
View File
@@ -8,6 +8,21 @@
namespace anm2ed::util::xml
{
std::string document_to_string(tinyxml2::XMLDocument&);
bool query_string_attribute(const tinyxml2::XMLElement*, const char*, std::string&);
bool query_path_attribute(const tinyxml2::XMLElement*, const char*, std::filesystem::path&);
bool query_int_attribute(const tinyxml2::XMLElement*, const char*, int&);
bool query_float_attribute(const tinyxml2::XMLElement*, const char*, float&);
bool query_bool_attribute(const tinyxml2::XMLElement*, const char*, bool&);
bool query_color_attribute(const tinyxml2::XMLElement*, const char*, float&);
void set_string_attribute(tinyxml2::XMLElement*, const char*, const std::string&);
void set_path_attribute(tinyxml2::XMLElement*, const char*, const std::filesystem::path&);
void set_int_attribute(tinyxml2::XMLElement*, const char*, int);
void set_float_attribute(tinyxml2::XMLElement*, const char*, float);
void set_bool_attribute(tinyxml2::XMLElement*, const char*, bool);
void set_color_attribute(tinyxml2::XMLElement*, const char*, float);
tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement*, const char*, std::string*);
tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement*, const char*, std::filesystem::path*);
void query_color_attribute(tinyxml2::XMLElement*, const char*, float&);
+1 -1
View File
@@ -53,6 +53,6 @@ Alternatively, if you have subscribed to the mod, you can find the latest releas
[h3]Happy animating![/h3]
[img]https://files.catbox.moe/4auc1c.gif[/img]
</description>
<version>2.16</version>
<version>2.18</version>
<visibility>Public</visibility>
</metadata>