fuck git and fuck vs code

This commit is contained in:
2025-11-11 11:25:46 -05:00
parent d07b4dc2eb
commit 07096c487b
62 changed files with 1635 additions and 1301 deletions

View File

@@ -44,6 +44,7 @@ set(SDLMIXER_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL) add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL)
add_subdirectory(external/lunasvg) add_subdirectory(external/lunasvg)
add_subdirectory(external/libxm/src EXCLUDE_FROM_ALL)
set(GLAD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp) set(GLAD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp)
@@ -82,7 +83,6 @@ add_executable(${PROJECT_NAME}
${PROJECT_SRC} ${PROJECT_SRC}
) )
if (WIN32) if (WIN32)
enable_language(RC) enable_language(RC)
target_sources(${PROJECT_NAME} PRIVATE Icon.rc) target_sources(${PROJECT_NAME} PRIVATE Icon.rc)
@@ -114,6 +114,13 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE
IMGUI_ENABLE_DOCKING IMGUI_ENABLE_DOCKING
) )
if (MSVC)
target_compile_definitions(${PROJECT_NAME} PRIVATE restrict=__restrict)
else ()
target_compile_definitions(${PROJECT_NAME} PRIVATE restrict=__restrict__)
endif ()
target_include_directories(${PROJECT_NAME} PRIVATE target_include_directories(${PROJECT_NAME} PRIVATE
external external
external/imgui external/imgui
@@ -130,7 +137,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE
src/util src/util
) )
target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mixer lunasvg) target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mixer lunasvg xm)
message(STATUS "System: ${CMAKE_SYSTEM_NAME}") message(STATUS "System: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Project: ${PROJECT_NAME}") message(STATUS "Project: ${PROJECT_NAME}")

View File

@@ -134,6 +134,8 @@ namespace anm2ed::anm2
return length; return length;
} }
void Animation::fit_length() { frameNum = length(); }
vec4 Animation::rect(bool isRootTransform) vec4 Animation::rect(bool isRootTransform)
{ {
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};

View File

@@ -8,7 +8,7 @@
namespace anm2ed::anm2 namespace anm2ed::anm2
{ {
constexpr auto FRAME_NUM_MIN = 1; constexpr auto FRAME_NUM_MIN = 1;
constexpr auto FRAME_NUM_MAX = FRAME_DELAY_MAX; constexpr auto FRAME_NUM_MAX = FRAME_DURATION_MAX;
class Animation class Animation
{ {
@@ -30,6 +30,7 @@ namespace anm2ed::anm2
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
std::string to_string(); std::string to_string();
int length(); int length();
void fit_length();
glm::vec4 rect(bool); glm::vec4 rect(bool);
}; };

View File

@@ -12,10 +12,7 @@ using namespace glm;
namespace anm2ed::anm2 namespace anm2ed::anm2
{ {
Anm2::Anm2() Anm2::Anm2() { info.createdOn = time::get("%d-%B-%Y %I:%M:%S"); }
{
info.createdOn = time::get("%d-%B-%Y %I:%M:%S");
}
Anm2::Anm2(const std::string& path, std::string* errorString) Anm2::Anm2(const std::string& path, std::string* errorString)
{ {
@@ -37,25 +34,6 @@ namespace anm2ed::anm2
animations = Animations((XMLElement*)animationsElement); animations = Animations((XMLElement*)animationsElement);
} }
bool Anm2::serialize(const std::string& path, std::string* errorString)
{
XMLDocument document;
auto* element = document.NewElement("AnimatedActor");
document.InsertFirstChild(element);
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
XMLElement* Anm2::to_element(XMLDocument& document) XMLElement* Anm2::to_element(XMLDocument& document)
{ {
auto element = document.NewElement("AnimatedActor"); auto element = document.NewElement("AnimatedActor");
@@ -68,6 +46,19 @@ namespace anm2ed::anm2
return element; return element;
} }
bool Anm2::serialize(const std::string& path, std::string* errorString)
{
XMLDocument document;
document.InsertFirstChild(to_element(document));
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
std::string Anm2::to_string() std::string Anm2::to_string()
{ {
XMLDocument document{}; XMLDocument document{};
@@ -75,15 +66,12 @@ namespace anm2ed::anm2
return xml::document_to_string(document); return xml::document_to_string(document);
} }
uint64_t Anm2::hash() uint64_t Anm2::hash() { return std::hash<std::string>{}(to_string()); }
{
return std::hash<std::string>{}(to_string());
}
Frame* Anm2::frame_get(Reference reference) Frame* Anm2::frame_get(int animationIndex, Type itemType, int frameIndex, int itemID)
{ {
if (auto item = item_get(reference); item) if (auto item = item_get(animationIndex, itemType, itemID); item)
if (vector::in_bounds(item->frames, reference.frameIndex)) return &item->frames[reference.frameIndex]; if (vector::in_bounds(item->frames, frameIndex)) return &item->frames[frameIndex];
return nullptr; return nullptr;
} }
} }

View File

@@ -19,7 +19,6 @@ namespace anm2ed::anm2
Type itemType{NONE}; Type itemType{NONE};
int itemID{-1}; int itemID{-1};
int frameIndex{-1}; int frameIndex{-1};
int frameTime{-1};
auto operator<=>(const Reference&) const = default; auto operator<=>(const Reference&) const = default;
}; };
@@ -40,22 +39,23 @@ namespace anm2ed::anm2
Spritesheet* spritesheet_get(int); Spritesheet* spritesheet_get(int);
bool spritesheet_add(const std::string&, const std::string&, int&); bool spritesheet_add(const std::string&, const std::string&, int&);
void spritesheet_remove(int);
std::vector<std::string> spritesheet_labels_get(); std::vector<std::string> spritesheet_labels_get();
std::set<int> spritesheets_unused(); std::set<int> spritesheets_unused();
bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type type, std::string*); bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type type, std::string*);
void layer_add(int&); void layer_add(int&);
std::set<int> layers_unused(Reference = {}); std::set<int> layers_unused();
std::set<int> layers_unused(Animation&);
bool layers_deserialize(const std::string&, types::merge::Type, std::string*); bool layers_deserialize(const std::string&, types::merge::Type, std::string*);
void null_add(int&); void null_add(int&);
std::set<int> nulls_unused(Reference = {}); std::set<int> nulls_unused();
std::set<int> nulls_unused(Animation&);
bool nulls_deserialize(const std::string&, types::merge::Type, std::string*); bool nulls_deserialize(const std::string&, types::merge::Type, std::string*);
void event_add(int&); void event_add(int&);
std::vector<std::string> event_labels_get(); std::vector<std::string> event_labels_get();
std::set<int> events_unused(Reference = {}); std::set<int> events_unused();
bool events_deserialize(const std::string&, types::merge::Type, std::string*); bool events_deserialize(const std::string&, types::merge::Type, std::string*);
bool sound_add(const std::string& directory, const std::string& path, int& id); bool sound_add(const std::string& directory, const std::string& path, int& id);
@@ -63,16 +63,16 @@ namespace anm2ed::anm2
std::set<int> sounds_unused(); std::set<int> sounds_unused();
bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string*); bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string*);
Animation* animation_get(Reference); Animation* animation_get(int);
std::vector<std::string> animation_labels_get(); std::vector<std::string> animation_labels_get();
int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true); int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr); bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
Item* item_get(Reference); Item* item_get(int, Type, int = -1);
Reference layer_animation_add(Reference = {}, std::string = {}, int = 0, Reference layer_animation_add(Reference = {}, std::string = {}, int = 0,
types::locale::Type = types::locale::GLOBAL); types::locale::Type = types::locale::GLOBAL);
Reference null_animation_add(Reference = {}, std::string = {}, types::locale::Type = types::locale::GLOBAL); Reference null_animation_add(Reference = {}, std::string = {}, types::locale::Type = types::locale::GLOBAL);
Frame* frame_get(Reference); Frame* frame_get(int, Type, int, int = -1);
}; };
} }

View File

@@ -8,10 +8,7 @@ using namespace tinyxml2;
namespace anm2ed::anm2 namespace anm2ed::anm2
{ {
Animation* Anm2::animation_get(Reference reference) Animation* Anm2::animation_get(int animationIndex) { return vector::find(animations.items, animationIndex); }
{
return vector::find(animations.items, reference.animationIndex);
}
std::vector<std::string> Anm2::animation_labels_get() std::vector<std::string> Anm2::animation_labels_get()
{ {

View File

@@ -25,14 +25,10 @@ namespace anm2ed::anm2
return labels; return labels;
} }
std::set<int> Anm2::events_unused(Reference reference) std::set<int> Anm2::events_unused()
{ {
std::set<int> used{}; std::set<int> used{};
if (auto animation = animation_get(reference); animation)
for (auto& frame : animation->triggers.frames)
used.insert(frame.eventID);
else
for (auto& animation : animations.items) for (auto& animation : animations.items)
for (auto& frame : animation.triggers.frames) for (auto& frame : animation.triggers.frames)
used.insert(frame.eventID); used.insert(frame.eventID);

View File

@@ -9,18 +9,18 @@ using namespace anm2ed::util;
namespace anm2ed::anm2 namespace anm2ed::anm2
{ {
Item* Anm2::item_get(Reference reference) Item* Anm2::item_get(int animationIndex, Type type, int id)
{ {
if (Animation* animation = animation_get(reference)) if (Animation* animation = animation_get(animationIndex))
{ {
switch (reference.itemType) switch (type)
{ {
case ROOT: case ROOT:
return &animation->rootAnimation; return &animation->rootAnimation;
case LAYER: case LAYER:
return unordered_map::find(animation->layerAnimations, reference.itemID); return unordered_map::find(animation->layerAnimations, id);
case NULL_: case NULL_:
return map::find(animation->nullAnimations, reference.itemID); return map::find(animation->nullAnimations, id);
case TRIGGER: case TRIGGER:
return &animation->triggers; return &animation->triggers;
default: default:
@@ -51,7 +51,7 @@ namespace anm2ed::anm2
} }
else if (locale == locale::LOCAL) else if (locale == locale::LOCAL)
{ {
if (auto animation = animation_get(reference)) if (auto animation = animation_get(reference.animationIndex))
if (!animation->layerAnimations.contains(id)) add(animation, id); if (!animation->layerAnimations.contains(id)) add(animation, id);
} }
@@ -74,7 +74,7 @@ namespace anm2ed::anm2
} }
else if (locale == locale::LOCAL) else if (locale == locale::LOCAL)
{ {
if (auto animation = animation_get(reference)) if (auto animation = animation_get(reference.animationIndex))
if (!animation->nullAnimations.contains(id)) add(animation, id); if (!animation->nullAnimations.contains(id)) add(animation, id);
} }

View File

@@ -16,15 +16,11 @@ namespace anm2ed::anm2
content.layers[id] = Layer(); content.layers[id] = Layer();
} }
std::set<int> Anm2::layers_unused(Reference reference) std::set<int> Anm2::layers_unused()
{ {
std::set<int> used{}; std::set<int> used{};
std::set<int> unused{}; std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->layerAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items) for (auto& animation : animations.items)
for (auto& id : animation.layerAnimations | std::views::keys) for (auto& id : animation.layerAnimations | std::views::keys)
used.insert(id); used.insert(id);
@@ -35,6 +31,16 @@ namespace anm2ed::anm2
return unused; return unused;
} }
std::set<int> Anm2::layers_unused(Animation& animation)
{
std::set<int> unused{};
for (auto& id : content.layers | std::views::keys)
if (!animation.layerAnimations.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString) bool Anm2::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{ {
XMLDocument document{}; XMLDocument document{};

View File

@@ -16,15 +16,11 @@ namespace anm2ed::anm2
content.nulls[id] = Null(); content.nulls[id] = Null();
} }
std::set<int> Anm2::nulls_unused(Reference reference) std::set<int> Anm2::nulls_unused()
{ {
std::set<int> used{}; std::set<int> used{};
std::set<int> unused{}; std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->nullAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items) for (auto& animation : animations.items)
for (auto& id : animation.nullAnimations | std::views::keys) for (auto& id : animation.nullAnimations | std::views::keys)
used.insert(id); used.insert(id);
@@ -35,6 +31,16 @@ namespace anm2ed::anm2
return unused; return unused;
} }
std::set<int> Anm2::nulls_unused(Animation& animation)
{
std::set<int> unused{};
for (auto& id : content.nulls | std::views::keys)
if (!animation.nullAnimations.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString) bool Anm2::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{ {
XMLDocument document{}; XMLDocument document{};

View File

@@ -16,11 +16,6 @@ namespace anm2ed::anm2
return map::find(content.spritesheets, id); return map::find(content.spritesheets, id);
} }
void Anm2::spritesheet_remove(int id)
{
content.spritesheets.erase(id);
}
bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id) bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id)
{ {
Spritesheet spritesheet(directory, path); Spritesheet spritesheet(directory, path);

View File

@@ -24,6 +24,10 @@ namespace anm2ed::anm2
if (auto eventsElement = element->FirstChildElement("Events")) if (auto eventsElement = element->FirstChildElement("Events"))
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event")) for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
events[id] = Event(child, id); events[id] = Event(child, id);
if (auto eventsElement = element->FirstChildElement("Sounds"))
for (auto child = eventsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
sounds[id] = Sound(child, id);
} }
void Content::serialize(XMLDocument& document, XMLElement* parent) void Content::serialize(XMLDocument& document, XMLElement* parent)
@@ -50,6 +54,11 @@ namespace anm2ed::anm2
event.serialize(document, eventsElement, id); event.serialize(document, eventsElement, id);
element->InsertEndChild(eventsElement); element->InsertEndChild(eventsElement);
auto soundsElement = document.NewElement("Sounds");
for (auto& [id, sound] : sounds)
sound.serialize(document, soundsElement, id);
element->InsertEndChild(soundsElement);
parent->InsertEndChild(element); parent->InsertEndChild(element);
} }

View File

@@ -2,6 +2,7 @@
#include <map> #include <map>
#include "anm2_type.h"
#include "event.h" #include "event.h"
#include "layer.h" #include "layer.h"
#include "null.h" #include "null.h"

View File

@@ -25,7 +25,7 @@ namespace anm2ed::anm2
} }
element->QueryFloatAttribute("XScale", &scale.x); element->QueryFloatAttribute("XScale", &scale.x);
element->QueryFloatAttribute("YScale", &scale.y); element->QueryFloatAttribute("YScale", &scale.y);
element->QueryIntAttribute("Delay", &delay); element->QueryIntAttribute("Delay", &duration);
element->QueryBoolAttribute("Visible", &isVisible); element->QueryBoolAttribute("Visible", &isVisible);
xml::query_color_attribute(element, "RedTint", tint.r); xml::query_color_attribute(element, "RedTint", tint.r);
xml::query_color_attribute(element, "GreenTint", tint.g); xml::query_color_attribute(element, "GreenTint", tint.g);
@@ -40,6 +40,7 @@ namespace anm2ed::anm2
else else
{ {
element->QueryIntAttribute("EventId", &eventID); element->QueryIntAttribute("EventId", &eventID);
element->QueryIntAttribute("SoundId", &soundID);
element->QueryIntAttribute("AtFrame", &atFrame); element->QueryIntAttribute("AtFrame", &atFrame);
} }
} }
@@ -54,7 +55,7 @@ namespace anm2ed::anm2
case NULL_: case NULL_:
element->SetAttribute("XPosition", position.x); element->SetAttribute("XPosition", position.x);
element->SetAttribute("YPosition", position.y); element->SetAttribute("YPosition", position.y);
element->SetAttribute("Delay", delay); element->SetAttribute("Delay", duration);
element->SetAttribute("Visible", isVisible); element->SetAttribute("Visible", isVisible);
element->SetAttribute("XScale", scale.x); element->SetAttribute("XScale", scale.x);
element->SetAttribute("YScale", scale.y); element->SetAttribute("YScale", scale.y);
@@ -79,7 +80,7 @@ namespace anm2ed::anm2
element->SetAttribute("Height", size.y); element->SetAttribute("Height", size.y);
element->SetAttribute("XScale", scale.x); element->SetAttribute("XScale", scale.x);
element->SetAttribute("YScale", scale.y); element->SetAttribute("YScale", scale.y);
element->SetAttribute("Delay", delay); element->SetAttribute("Delay", duration);
element->SetAttribute("Visible", isVisible); element->SetAttribute("Visible", isVisible);
element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); element->SetAttribute("RedTint", math::float_to_uint8(tint.r));
element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); element->SetAttribute("GreenTint", math::float_to_uint8(tint.g));
@@ -93,6 +94,7 @@ namespace anm2ed::anm2
break; break;
case TRIGGER: case TRIGGER:
element->SetAttribute("EventId", eventID); element->SetAttribute("EventId", eventID);
element->SetAttribute("SoundId", soundID);
element->SetAttribute("AtFrame", atFrame); element->SetAttribute("AtFrame", atFrame);
break; break;
default: default:
@@ -114,9 +116,9 @@ namespace anm2ed::anm2
return xml::document_to_string(document); return xml::document_to_string(document);
} }
void Frame::shorten() { delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); } void Frame::shorten() { duration = glm::clamp(--duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); }
void Frame::extend() { delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); } void Frame::extend() { duration = glm::clamp(++duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); }
bool Frame::is_visible(Type type) bool Frame::is_visible(Type type)
{ {

View File

@@ -9,14 +9,14 @@
namespace anm2ed::anm2 namespace anm2ed::anm2
{ {
constexpr auto FRAME_DELAY_MIN = 1; constexpr auto FRAME_DURATION_MIN = 1;
constexpr auto FRAME_DELAY_MAX = 100000; constexpr auto FRAME_DURATION_MAX = 100000;
#define MEMBERS \ #define MEMBERS \
X(isVisible, bool, true) \ X(isVisible, bool, true) \
X(isInterpolated, bool, false) \ X(isInterpolated, bool, false) \
X(rotation, float, 0.0f) \ X(rotation, float, 0.0f) \
X(delay, int, FRAME_DELAY_MIN) \ X(duration, int, FRAME_DURATION_MIN) \
X(atFrame, int, -1) \ X(atFrame, int, -1) \
X(eventID, int, -1) \ X(eventID, int, -1) \
X(soundID, int, -1) \ X(soundID, int, -1) \
@@ -53,5 +53,4 @@ namespace anm2ed::anm2
}; };
#undef MEMBERS #undef MEMBERS
} }

View File

@@ -1,4 +1,5 @@
#include "item.h" #include "item.h"
#include <algorithm>
#include <ranges> #include <ranges>
#include "vector_.h" #include "vector_.h"
@@ -30,6 +31,8 @@ namespace anm2ed::anm2
if (type == NULL_) element->SetAttribute("NullId", id); if (type == NULL_) element->SetAttribute("NullId", id);
if (type == LAYER || type == NULL_) element->SetAttribute("Visible", isVisible); if (type == LAYER || type == NULL_) element->SetAttribute("Visible", isVisible);
if (type == TRIGGER) frames_sort_by_at_frame();
for (auto& frame : frames) for (auto& frame : frames)
frame.serialize(document, element, type); frame.serialize(document, element, type);
@@ -57,11 +60,16 @@ namespace anm2ed::anm2
length = frame.atFrame > length ? frame.atFrame : length; length = frame.atFrame > length ? frame.atFrame : length;
else else
for (auto& frame : frames) for (auto& frame : frames)
length += frame.delay; length += frame.duration;
return length; return length;
} }
void Item::frames_sort_by_at_frame()
{
std::sort(frames.begin(), frames.end(), [](const Frame& a, const Frame& b) { return a.atFrame < b.atFrame; });
}
Frame Item::frame_generate(float time, Type type) Frame Item::frame_generate(float time, Type type)
{ {
Frame frame{}; Frame frame{};
@@ -70,8 +78,8 @@ namespace anm2ed::anm2
if (frames.empty()) return frame; if (frames.empty()) return frame;
Frame* frameNext = nullptr; Frame* frameNext = nullptr;
int delayCurrent = 0; int durationCurrent = 0;
int delayNext = 0; int durationNext = 0;
for (auto [i, iFrame] : std::views::enumerate(frames)) for (auto [i, iFrame] : std::views::enumerate(frames))
{ {
@@ -87,9 +95,9 @@ namespace anm2ed::anm2
{ {
frame = iFrame; frame = iFrame;
delayNext += frame.delay; durationNext += frame.duration;
if (time >= delayCurrent && time < delayNext) if (time >= durationCurrent && time < durationNext)
{ {
if (i + 1 < (int)frames.size()) if (i + 1 < (int)frames.size())
frameNext = &frames[i + 1]; frameNext = &frames[i + 1];
@@ -98,13 +106,13 @@ namespace anm2ed::anm2
break; break;
} }
delayCurrent += frame.delay; durationCurrent += frame.duration;
} }
} }
if (type != TRIGGER && frame.isInterpolated && frameNext && frame.delay > 1) if (type != TRIGGER && frame.isInterpolated && frameNext && frame.duration > 1)
{ {
auto interpolation = (time - delayCurrent) / (delayNext - delayCurrent); auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation); frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation);
frame.position = glm::mix(frame.position, frameNext->position, interpolation); frame.position = glm::mix(frame.position, frameNext->position, interpolation);
@@ -133,7 +141,7 @@ namespace anm2ed::anm2
{ {
case ADJUST: case ADJUST:
if (change.rotation) frame.rotation = *change.rotation; if (change.rotation) frame.rotation = *change.rotation;
if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, *change.delay); if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, *change.duration);
if (change.crop) frame.crop = *change.crop; if (change.crop) frame.crop = *change.crop;
if (change.pivot) frame.pivot = *change.pivot; if (change.pivot) frame.pivot = *change.pivot;
if (change.position) frame.position = *change.position; if (change.position) frame.position = *change.position;
@@ -145,7 +153,7 @@ namespace anm2ed::anm2
case ADD: case ADD:
if (change.rotation) frame.rotation += *change.rotation; if (change.rotation) frame.rotation += *change.rotation;
if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay + *change.delay); if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration + *change.duration);
if (change.crop) frame.crop += *change.crop; if (change.crop) frame.crop += *change.crop;
if (change.pivot) frame.pivot += *change.pivot; if (change.pivot) frame.pivot += *change.pivot;
if (change.position) frame.position += *change.position; if (change.position) frame.position += *change.position;
@@ -157,7 +165,7 @@ namespace anm2ed::anm2
case SUBTRACT: case SUBTRACT:
if (change.rotation) frame.rotation -= *change.rotation; if (change.rotation) frame.rotation -= *change.rotation;
if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay - *change.delay); if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration - *change.duration);
if (change.crop) frame.crop -= *change.crop; if (change.crop) frame.crop -= *change.crop;
if (change.pivot) frame.pivot -= *change.pivot; if (change.pivot) frame.pivot -= *change.pivot;
if (change.position) frame.position -= *change.position; if (change.position) frame.position -= *change.position;
@@ -177,14 +185,12 @@ namespace anm2ed::anm2
if (document.Parse(string.c_str()) == XML_SUCCESS) if (document.Parse(string.c_str()) == XML_SUCCESS)
{ {
if (!document.FirstChildElement("Frame"))
{
if (errorString) *errorString = "No valid frame(s).";
return false;
}
int count{}; int count{};
for (auto element = document.FirstChildElement("Frame"); element; element = element->NextSiblingElement("Frame")) if (document.FirstChildElement("Frame") && type != anm2::TRIGGER)
{
start = std::clamp(start, 0, (int)frames.size());
for (auto element = document.FirstChildElement("Frame"); element;
element = element->NextSiblingElement("Frame"))
{ {
auto index = start + count; auto index = start + count;
frames.insert(frames.begin() + start + count, Frame(element, type)); frames.insert(frames.begin() + start + count, Frame(element, type));
@@ -194,6 +200,36 @@ namespace anm2ed::anm2
return true; return true;
} }
else if (document.FirstChildElement("Trigger") && type == anm2::TRIGGER)
{
auto has_conflict = [&](int value)
{
for (auto& trigger : frames)
if (trigger.atFrame == value) return true;
return false;
};
for (auto element = document.FirstChildElement("Trigger"); element;
element = element->NextSiblingElement("Trigger"))
{
Frame trigger(element, type);
trigger.atFrame = start + count;
while (has_conflict(trigger.atFrame))
trigger.atFrame++;
frames.push_back(trigger);
indices.insert(trigger.atFrame);
count++;
}
frames_sort_by_at_frame();
return true;
}
else
{
if (errorString) *errorString = type == anm2::TRIGGER ? "No valid trigger(s)." : "No valid frame(s).";
return false;
}
}
else if (errorString) else if (errorString)
*errorString = document.ErrorStr(); *errorString = document.ErrorStr();
@@ -205,18 +241,18 @@ namespace anm2ed::anm2
if (!vector::in_bounds(frames, index)) return; if (!vector::in_bounds(frames, index)) return;
Frame& frame = frames[index]; Frame& frame = frames[index];
if (frame.delay == FRAME_DELAY_MIN) return; if (frame.duration == FRAME_DURATION_MIN) return;
Frame frameNext = vector::in_bounds(frames, index + 1) ? frames[index + 1] : frame; Frame frameNext = vector::in_bounds(frames, index + 1) ? frames[index + 1] : frame;
int delay{}; int duration{};
int i = index; int i = index;
while (delay < frame.delay) while (duration < frame.duration)
{ {
Frame baked = frame; Frame baked = frame;
float interpolation = (float)delay / frame.delay; float interpolation = (float)duration / frame.duration;
baked.delay = std::min(interval, frame.delay - delay); baked.duration = std::min(interval, frame.duration - duration);
baked.isInterpolated = (i == index) ? frame.isInterpolated : false; baked.isInterpolated = (i == index) ? frame.isInterpolated : false;
baked.rotation = glm::mix(frame.rotation, frameNext.rotation, interpolation); baked.rotation = glm::mix(frame.rotation, frameNext.rotation, interpolation);
baked.position = glm::mix(frame.position, frameNext.position, interpolation); baked.position = glm::mix(frame.position, frameNext.position, interpolation);
@@ -232,16 +268,17 @@ namespace anm2ed::anm2
frames.insert(frames.begin() + i, baked); frames.insert(frames.begin() + i, baked);
i++; i++;
delay += baked.delay; duration += baked.duration;
} }
} }
void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, int delay) void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count,
int duration)
{ {
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
Frame frame{}; Frame frame{};
frame.delay = delay; frame.duration = duration;
frame.pivot = pivot; frame.pivot = pivot;
frame.size = size; frame.size = size;
frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns)); frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns));
@@ -249,4 +286,11 @@ namespace anm2ed::anm2
frames.emplace_back(frame); frames.emplace_back(frame);
} }
} }
int Item::frame_index_from_at_frame_get(int atFrame)
{
for (auto [i, frame] : std::views::enumerate(frames))
if (frame.atFrame == atFrame) return i;
return -1;
}
} }

View File

@@ -24,5 +24,7 @@ namespace anm2ed::anm2
bool frames_deserialize(const std::string&, Type, int, std::set<int>&, std::string*); bool frames_deserialize(const std::string&, Type, int, std::set<int>&, std::string*);
void frames_bake(int, int, bool, bool); void frames_bake(int, int, bool, bool);
void frames_generate_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); void frames_generate_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int);
void frames_sort_by_at_frame();
int frame_index_from_at_frame_get(int);
}; };
} }

View File

@@ -9,10 +9,7 @@ using namespace tinyxml2;
namespace anm2ed::anm2 namespace anm2ed::anm2
{ {
Sound::Sound(const Sound& other) : path(other.path) Sound::Sound(const Sound& other) : path(other.path) { audio = path.empty() ? Audio() : Audio(path.c_str()); }
{
audio = path.empty() ? Audio() : Audio(path.c_str());
}
Sound& Sound::operator=(const Sound& other) Sound& Sound::operator=(const Sound& other)
{ {
@@ -49,6 +46,11 @@ namespace anm2ed::anm2
return element; return element;
} }
void Sound::serialize(XMLDocument& document, XMLElement* parent, int id)
{
parent->InsertEndChild(to_element(document, id));
}
std::string Sound::to_string(int id) std::string Sound::to_string(int id)
{ {
XMLDocument document{}; XMLDocument document{};
@@ -56,18 +58,9 @@ namespace anm2ed::anm2
return xml::document_to_string(document); return xml::document_to_string(document);
} }
void Sound::reload(const std::string& directory) void Sound::reload(const std::string& directory) { *this = Sound(directory, this->path); }
{
*this = Sound(directory, this->path);
}
bool Sound::is_valid() bool Sound::is_valid() { return audio.is_valid(); }
{
return audio.is_valid();
}
void Sound::play() void Sound::play() { audio.play(); }
{
audio.play();
}
} }

View File

@@ -25,6 +25,7 @@ namespace anm2ed::anm2
Sound(const std::string&, const std::string&); Sound(const std::string&, const std::string&);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
std::string to_string(int); std::string to_string(int);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
void reload(const std::string&); void reload(const std::string&);
bool is_valid(); bool is_valid();
void play(); void play();

View File

@@ -56,14 +56,8 @@ namespace anm2ed::anm2
return texture.write_png(this->path); return texture.write_png(this->path);
} }
void Spritesheet::reload(const std::string& directory) void Spritesheet::reload(const std::string& directory) { *this = Spritesheet(directory, this->path); }
{
*this = Spritesheet(directory, this->path);
}
bool Spritesheet::is_valid() bool Spritesheet::is_valid() { return texture.is_valid(); }
{
return texture.is_valid();
}
} }

View File

@@ -36,9 +36,9 @@ namespace anm2ed
: path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current), : path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current),
anm2(current.anm2), reference(current.reference), playback(current.playback), animation(current.animation), anm2(current.anm2), reference(current.reference), playback(current.playback), animation(current.animation),
merge(current.merge), event(current.event), layer(current.layer), null(current.null), sound(current.sound), merge(current.merge), event(current.event), layer(current.layer), null(current.null), sound(current.sound),
spritesheet(current.spritesheet), message(current.message), previewZoom(other.previewZoom), spritesheet(current.spritesheet), frames(current.frames), message(current.message),
previewPan(other.previewPan), editorPan(other.editorPan), editorZoom(other.editorZoom), previewZoom(other.previewZoom), previewPan(other.previewPan), editorPan(other.editorPan),
overlayIndex(other.overlayIndex), hoveredFrame(other.hoveredFrame), hash(other.hash), saveHash(other.saveHash), editorZoom(other.editorZoom), overlayIndex(other.overlayIndex), saveHash(other.saveHash),
autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isOpen(other.isOpen), autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isOpen(other.isOpen),
isForceDirty(other.isForceDirty), isAnimationPreviewSet(other.isAnimationPreviewSet), isForceDirty(other.isForceDirty), isAnimationPreviewSet(other.isAnimationPreviewSet),
isSpritesheetEditorSet(other.isSpritesheetEditorSet) isSpritesheetEditorSet(other.isSpritesheetEditorSet)
@@ -56,7 +56,6 @@ namespace anm2ed
editorPan = other.editorPan; editorPan = other.editorPan;
editorZoom = other.editorZoom; editorZoom = other.editorZoom;
overlayIndex = other.overlayIndex; overlayIndex = other.overlayIndex;
hoveredFrame = other.hoveredFrame;
hash = other.hash; hash = other.hash;
saveHash = other.saveHash; saveHash = other.saveHash;
autosaveHash = other.autosaveHash; autosaveHash = other.autosaveHash;
@@ -118,10 +117,7 @@ namespace anm2ed
return false; return false;
} }
void Document::hash_set() void Document::hash_set() { hash = anm2.hash(); }
{
hash = anm2.hash();
}
void Document::clean() void Document::clean()
{ {
@@ -194,50 +190,23 @@ namespace anm2ed
} }
} }
bool Document::is_dirty() bool Document::is_dirty() { return hash != saveHash; }
{ bool Document::is_autosave_dirty() { return hash != autosaveHash; }
return hash != saveHash; std::filesystem::path Document::directory_get() { return path.parent_path(); }
} std::filesystem::path Document::filename_get() { return path.filename(); }
bool Document::is_valid() { return !path.empty(); }
bool Document::is_autosave_dirty()
{
return hash != autosaveHash;
}
std::filesystem::path Document::directory_get()
{
return path.parent_path();
}
std::filesystem::path Document::filename_get()
{
return path.filename();
}
bool Document::is_valid()
{
return !path.empty();
}
anm2::Frame* Document::frame_get() anm2::Frame* Document::frame_get()
{ {
return anm2.frame_get(reference); return anm2.frame_get(reference.animationIndex, reference.itemType, reference.frameIndex, reference.itemID);
} }
anm2::Item* Document::item_get() anm2::Item* Document::item_get()
{ {
return anm2.item_get(reference); return anm2.item_get(reference.animationIndex, reference.itemType, reference.itemID);
}
anm2::Animation* Document::animation_get()
{
return anm2.animation_get(reference);
}
anm2::Spritesheet* Document::spritesheet_get()
{
return anm2.spritesheet_get(spritesheet.reference);
} }
anm2::Animation* Document::animation_get() { return anm2.animation_get(reference.animationIndex); }
anm2::Spritesheet* Document::spritesheet_get() { return anm2.spritesheet_get(spritesheet.reference); }
void Document::spritesheet_add(const std::string& path) void Document::spritesheet_add(const std::string& path)
{ {
@@ -277,14 +246,6 @@ namespace anm2ed
change(Document::ALL); change(Document::ALL);
} }
bool Document::is_able_to_undo() bool Document::is_able_to_undo() { return !snapshots.undoStack.is_empty(); }
{ bool Document::is_able_to_redo() { return !snapshots.redoStack.is_empty(); }
return !snapshots.undoStack.is_empty();
}
bool Document::is_able_to_redo()
{
return !snapshots.redoStack.is_empty();
}
} }

View File

@@ -14,6 +14,7 @@ namespace anm2ed
public: public:
enum ChangeType enum ChangeType
{ {
INFO,
LAYERS, LAYERS,
NULLS, NULLS,
SPRITESHEETS, SPRITESHEETS,
@@ -33,6 +34,7 @@ namespace anm2ed
anm2::Anm2& anm2 = current.anm2; anm2::Anm2& anm2 = current.anm2;
anm2::Reference& reference = current.reference; anm2::Reference& reference = current.reference;
float& frameTime = current.frameTime;
Playback& playback = current.playback; Playback& playback = current.playback;
Storage& animation = current.animation; Storage& animation = current.animation;
Storage& merge = current.merge; Storage& merge = current.merge;
@@ -41,6 +43,7 @@ namespace anm2ed
Storage& null = current.null; Storage& null = current.null;
Storage& sound = current.sound; Storage& sound = current.sound;
Storage& spritesheet = current.spritesheet; Storage& spritesheet = current.spritesheet;
Storage& frames = current.frames;
std::string& message = current.message; std::string& message = current.message;
float previewZoom{200}; float previewZoom{200};
@@ -49,8 +52,6 @@ namespace anm2ed
float editorZoom{200}; float editorZoom{200};
int overlayIndex{-1}; int overlayIndex{-1};
anm2::Reference hoveredFrame{};
uint64_t hash{}; uint64_t hash{};
uint64_t saveHash{}; uint64_t saveHash{};
uint64_t autosaveHash{}; uint64_t autosaveHash{};

View File

@@ -34,7 +34,7 @@ namespace anm2ed::imgui
if (settings.windowIsFrameProperties) frameProperties.update(manager, settings); if (settings.windowIsFrameProperties) frameProperties.update(manager, settings);
if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard); if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard);
if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard); if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard);
if (settings.windowIsOnionskin) onionskin.update(settings); if (settings.windowIsOnionskin) onionskin.update(manager, settings);
if (settings.windowIsSounds) sounds.update(manager, settings, resources, dialog, clipboard); if (settings.windowIsSounds) sounds.update(manager, settings, resources, dialog, clipboard);
if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources); if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources);
if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard); if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard);

View File

@@ -38,9 +38,9 @@ namespace anm2ed::imgui
if (ImGui::BeginTabBar("Documents Bar", ImGuiTabBarFlags_Reorderable)) if (ImGui::BeginTabBar("Documents Bar", ImGuiTabBarFlags_Reorderable))
{ {
auto documentsCount = (int)manager.documents.size(); auto documentsCount = (int)manager.documents.size();
bool closeShortcut = imgui::shortcut(settings.shortcutClose, shortcut::GLOBAL) && !closePopup.is_open(); bool isCloseShortcut = shortcut(manager.chords[SHORTCUT_CLOSE], shortcut::GLOBAL) && !closePopup.is_open();
int closeShortcutIndex = int closeShortcutIndex =
closeShortcut && manager.selected >= 0 && manager.selected < documentsCount ? manager.selected : -1; isCloseShortcut && manager.selected >= 0 && manager.selected < documentsCount ? manager.selected : -1;
std::vector<int> closeIndices{}; std::vector<int> closeIndices{};
closeIndices.reserve(documentsCount); closeIndices.reserve(documentsCount);
@@ -85,7 +85,7 @@ namespace anm2ed::imgui
ImGui::PushFont(resources.fonts[font].get(), font::SIZE); ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags)) if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags))
{ {
manager.set(i); if (manager.selected != i) manager.set(i);
if (isRequested) manager.pendingSelected = -1; if (isRequested) manager.pendingSelected = -1;

View File

@@ -248,18 +248,18 @@ namespace anm2ed::imgui
return false; return false;
} }
bool shortcut(std::string string, shortcut::Type type) bool shortcut(ImGuiKeyChord chord, shortcut::Type type)
{ {
if (ImGui::GetTopMostPopupModal() != nullptr) return false; if (ImGui::GetTopMostPopupModal() != nullptr) return false;
int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal
: ImGuiInputFlags_RouteFocused; : ImGuiInputFlags_RouteFocused;
if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET) if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET)
{ {
ImGui::SetNextItemShortcut(string_to_chord(string), flags); ImGui::SetNextItemShortcut(chord, flags);
return false; return false;
} }
return ImGui::Shortcut(string_to_chord(string), flags); return ImGui::Shortcut(chord, flags);
} }
MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; } MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; }

View File

@@ -174,7 +174,7 @@ namespace anm2ed::imgui
ImVec2 icon_size_get(); ImVec2 icon_size_get();
bool chord_held(ImGuiKeyChord); bool chord_held(ImGuiKeyChord);
bool chord_repeating(ImGuiKeyChord, float = ImGui::GetIO().KeyRepeatDelay, float = ImGui::GetIO().KeyRepeatRate); bool chord_repeating(ImGuiKeyChord, float = ImGui::GetIO().KeyRepeatDelay, float = ImGui::GetIO().KeyRepeatRate);
bool shortcut(std::string, types::shortcut::Type = types::shortcut::FOCUSED_SET); bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET);
class MultiSelectStorage : public std::set<int> class MultiSelectStorage : public std::set<int>
{ {

View File

@@ -1,19 +1,19 @@
#include "taskbar.h" #include "taskbar.h"
#include <algorithm>
#include <array> #include <array>
#include <cstdint> #include <cfloat>
#include <cstdlib> #include <cmath>
#include <filesystem> #include <filesystem>
#include <fstream>
#include <iterator>
#include <ranges> #include <ranges>
#include <vector>
#include <imgui/imgui.h> #include <imgui/imgui.h>
#include <xm.h>
#include "math_.h" #include "math_.h"
#include "render.h" #include "render.h"
#include "shader.h" #include "shader.h"
#include "toast.h"
#include "types.h" #include "types.h"
#include "icon.h" #include "icon.h"
@@ -26,115 +26,11 @@ using namespace glm;
namespace anm2ed::imgui namespace anm2ed::imgui
{ {
#ifdef __unix__
namespace
{
constexpr std::array<int, 7> ICON_SIZES{16, 24, 32, 48, 64, 128, 256};
bool ensure_parent_directory_exists(const std::filesystem::path& path)
{
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec)
{
toasts.warning(std::format("Could not create directory for {} ({})", path.string(), ec.message()));
return false;
}
return true;
}
bool write_binary_blob(const std::filesystem::path& path, const std::uint8_t* data, size_t size)
{
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ios::binary | std::ios::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not open {} for writing", path.string()));
return false;
}
file.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
return true;
}
bool run_command_checked(const std::string& command, const std::string& description)
{
auto result = std::system(command.c_str());
if (result != 0)
{
toasts.warning(std::format("{} failed (exit code {})", description, result));
return false;
}
return true;
}
bool install_icon_set(const std::string& context, const std::string& iconName, const std::filesystem::path& path)
{
bool success = true;
for (auto size : ICON_SIZES)
{
auto command = std::format("xdg-icon-resource install --noupdate --novendor --context {} --size {} \"{}\" {}",
context, size, path.string(), iconName);
success &= run_command_checked(command, std::format("Install {} icon ({}px)", iconName, size));
}
return success;
}
bool uninstall_icon_set(const std::string& context, const std::string& iconName)
{
bool success = true;
for (auto size : ICON_SIZES)
{
auto command =
std::format("xdg-icon-resource uninstall --noupdate --context {} --size {} {}", context, size, iconName);
success &= run_command_checked(command, std::format("Remove {} icon ({}px)", iconName, size));
}
return success;
}
bool remove_file_if_exists(const std::filesystem::path& path)
{
std::error_code ec;
if (!std::filesystem::exists(path, ec)) return true;
std::filesystem::remove(path, ec);
if (ec)
{
toasts.warning(std::format("Could not remove {} ({})", path.string(), ec.message()));
return false;
}
return true;
}
}
constexpr auto MIME_TYPE = R"(<?xml version="1.0" encoding="utf-8"?>
<mime-type xmlns="http://www.freedesktop.org/standards/shared-mime-info" type="application/x-anm2+xml">
<!--Created automatically by update-mime-database. DO NOT EDIT!-->
<comment>Anm2 Animation</comment>
<glob pattern="*.anm2"/>
</mime-type>
)";
constexpr auto DESKTOP_ENTRY_FORMAT = R"([Desktop Entry]
Type=Application
Name=Anm2Ed
Icon=anm2ed
Comment=Animation editor for .anm2 files
Exec={}
Terminal=false
Categories=Graphics;Development;
MimeType=application/x-anm2+xml;
)";
#endif
Taskbar::Taskbar() : generate(vec2()) {} Taskbar::Taskbar() : generate(vec2()) {}
void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting)
{ {
auto document = manager.get(); auto document = manager.get();
auto reference = document ? &document->reference : nullptr;
auto animation = document ? document->animation_get() : nullptr; auto animation = document ? document->animation_get() : nullptr;
auto item = document ? document->item_get() : nullptr; auto item = document ? document->item_get() : nullptr;
@@ -194,10 +90,10 @@ MimeType=application/x-anm2+xml;
if (ImGui::BeginMenu("Wizard")) if (ImGui::BeginMenu("Wizard"))
{ {
ImGui::BeginDisabled(!item || document->reference.itemType != anm2::LAYER); if (ImGui::MenuItem("Generate Animation From Grid", nullptr, false,
if (ImGui::MenuItem("Generate Animation From Grid")) generatePopup.open(); item && document->reference.itemType == anm2::LAYER))
if (ImGui::MenuItem("Change All Frame Properties")) changePopup.open(); generatePopup.open();
ImGui::EndDisabled();
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("Render Animation", nullptr, false, animation)) renderPopup.open(); if (ImGui::MenuItem("Render Animation", nullptr, false, animation)) renderPopup.open();
ImGui::EndMenu(); ImGui::EndMenu();
@@ -208,8 +104,9 @@ MimeType=application/x-anm2+xml;
ImGui::MenuItem("Always Loop", nullptr, &settings.playbackIsLoop); ImGui::MenuItem("Always Loop", nullptr, &settings.playbackIsLoop);
ImGui::SetItemTooltip("%s", "Animations will always loop during playback, even if looping isn't set."); ImGui::SetItemTooltip("%s", "Animations will always loop during playback, even if looping isn't set.");
ImGui::MenuItem("Clamp Playhead", nullptr, &settings.playbackIsClampPlayhead); ImGui::MenuItem("Clamp", nullptr, &settings.playbackIsClamp);
ImGui::SetItemTooltip("%s", "The playhead will always clamp to the animation's length."); ImGui::SetItemTooltip("%s", "Operations will always be clamped to within the animation's bounds.\nFor example, "
"dragging the playhead, or triggers.");
ImGui::EndMenu(); ImGui::EndMenu();
} }
@@ -230,146 +127,12 @@ MimeType=application/x-anm2+xml;
configurePopup.open(); configurePopup.open();
} }
ImGui::Separator();
if (ImGui::MenuItem("Associate .anm2 Files with Editor", nullptr, false,
!isAnm2Association || !isAbleToAssociateAnm2))
{
#ifdef _WIN32
#elif __unix__
auto cache_icons = []()
{
auto programIconPath = std::filesystem::path(filesystem::path_icon_get());
auto fileIconPath = std::filesystem::path(filesystem::path_icon_file_get());
auto iconBytes = std::size(resource::icon::PROGRAM);
bool isSuccess = write_binary_blob(programIconPath, resource::icon::PROGRAM, iconBytes) &&
write_binary_blob(fileIconPath, resource::icon::PROGRAM, iconBytes);
if (isSuccess)
{
isSuccess = install_icon_set("apps", "anm2ed", programIconPath) &&
install_icon_set("mimetypes", "application-x-anm2+xml", fileIconPath) &&
run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache");
}
remove_file_if_exists(programIconPath);
remove_file_if_exists(fileIconPath);
if (isSuccess) toasts.info("Cached program and file icons.");
return isSuccess;
};
auto register_mime = []()
{
auto path = std::filesystem::path(filesystem::path_mime_get());
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ofstream::out | std::ofstream::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not write .anm2 MIME type: {}", path.string()));
return false;
}
file << MIME_TYPE;
file.close();
toasts.info(std::format("Wrote .anm2 MIME type to: {}", path.string()));
auto mimeRoot = path.parent_path().parent_path();
auto command = std::format("update-mime-database \"{}\"", mimeRoot.string());
return run_command_checked(command, "Update MIME database");
};
auto register_desktop_entry = []()
{
auto path = std::filesystem::path(filesystem::path_application_get());
if (!ensure_parent_directory_exists(path)) return false;
std::ofstream file(path, std::ofstream::out | std::ofstream::trunc);
if (!file.is_open())
{
toasts.warning(std::format("Could not write desktop entry: {}", path.string()));
return false;
}
auto desktopEntry = std::format(DESKTOP_ENTRY_FORMAT, filesystem::path_executable_get());
file << desktopEntry;
file.close();
toasts.info(std::format("Wrote desktop entry to: {}", path.string()));
auto desktopDir = path.parent_path();
auto desktopUpdate =
std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string());
auto desktopFileName = path.filename().string();
auto setDefault = std::format("xdg-mime default {} application/x-anm2+xml",
desktopFileName.empty() ? path.string() : desktopFileName);
auto databaseUpdated = run_command_checked(desktopUpdate, "Update desktop database");
auto defaultRegistered = run_command_checked(setDefault, "Set default handler for .anm2");
return databaseUpdated && defaultRegistered;
};
auto iconsCached = cache_icons();
auto mimeRegistered = register_mime();
auto desktopRegistered = register_desktop_entry();
isAnm2Association = iconsCached && mimeRegistered && desktopRegistered;
if (isAnm2Association)
toasts.info("Associated .anm2 files with the editor.");
else
toasts.warning("Association incomplete. Please review the warnings above.");
#endif
}
ImGui::SetItemTooltip(
"Associate .anm2 files with the application (i.e., clicking on them in a file explorer will "
"open the application).");
if (ImGui::MenuItem("Remove .anm2 File Association", nullptr, false,
isAnm2Association || !isAbleToAssociateAnm2))
{
#ifdef _WIN32
#elif __unix__
{
auto iconsRemoved =
uninstall_icon_set("apps", "anm2ed") && uninstall_icon_set("mimetypes", "application-x-anm2+xml") &&
run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache");
if (iconsRemoved)
toasts.info("Removed cached icons.");
else
toasts.warning("Could not remove all cached icons.");
}
{
auto path = std::filesystem::path(filesystem::path_mime_get());
auto removed = remove_file_if_exists(path);
if (removed) toasts.info(std::format("Removed .anm2 MIME type: {}", path.string()));
auto mimeRoot = path.parent_path().parent_path();
run_command_checked(std::format("update-mime-database \"{}\"", mimeRoot.string()), "Update MIME database");
}
{
auto path = std::filesystem::path(filesystem::path_application_get());
if (remove_file_if_exists(path)) toasts.info(std::format("Removed desktop entry: {}", path.string()));
auto desktopDir = path.parent_path();
run_command_checked(
std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string()),
"Update desktop database");
}
#endif
isAnm2Association = false;
}
ImGui::SetItemTooltip("Unassociate .anm2 files with the application.");
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("Help")) if (ImGui::BeginMenu("Help"))
{ {
if (ImGui::MenuItem("About")) aboutPopup.open(); if (ImGui::MenuItem("About")) aboutPopup.open();
ImGui::EndMenu(); ImGui::EndMenu();
} }
@@ -476,134 +239,6 @@ MimeType=application/x-anm2+xml;
ImGui::EndPopup(); ImGui::EndPopup();
} }
changePopup.trigger();
if (ImGui::BeginPopupModal(changePopup.label, &changePopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto& isCrop = settings.changeIsCrop;
auto& isSize = settings.changeIsSize;
auto& isPosition = settings.changeIsPosition;
auto& isPivot = settings.changeIsPivot;
auto& isScale = settings.changeIsScale;
auto& isRotation = settings.changeIsRotation;
auto& isDelay = settings.changeIsDelay;
auto& isTint = settings.changeIsTint;
auto& isColorOffset = settings.changeIsColorOffset;
auto& isVisibleSet = settings.changeIsVisibleSet;
auto& isInterpolatedSet = settings.changeIsInterpolatedSet;
auto& crop = settings.changeCrop;
auto& size = settings.changeSize;
auto& position = settings.changePosition;
auto& pivot = settings.changePivot;
auto& scale = settings.changeScale;
auto& rotation = settings.changeRotation;
auto& delay = settings.changeDelay;
auto& tint = settings.changeTint;
auto& colorOffset = settings.changeColorOffset;
auto& isVisible = settings.changeIsVisible;
auto& isInterpolated = settings.changeIsInterpolated;
auto& isFromSelectedFrame = settings.changeIsFromSelectedFrame;
auto& numberFrames = settings.changeNumberFrames;
auto propertiesSize = child_size_get(10);
if (ImGui::BeginChild("##Properties", propertiesSize, ImGuiChildFlags_Borders))
{
#define PROPERTIES_WIDGET(body) \
ImGui::Checkbox(checkboxLabel, &isEnabled); \
ImGui::SameLine(); \
ImGui::BeginDisabled(!isEnabled); \
body; \
ImGui::EndDisabled();
auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value)
{ PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value)); };
auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value)
{ PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value))); };
auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value)
{ PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value))); };
auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value)
{ PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), math::vec2_format_get(value))); };
auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value)
{ PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, math::float_format_get(value))); };
auto int_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value)
{ PROPERTIES_WIDGET(ImGui::InputInt(valueLabel, &value, STEP, STEP_FAST)); };
#undef PROPERTIES_WIDGET
float2_value("##Is Crop", "Crop", isCrop, crop);
float2_value("##Is Size", "Size", isSize, size);
float2_value("##Is Position", "Position", isPosition, position);
float2_value("##Is Pivot", "Pivot", isPivot, pivot);
float2_value("##Is Scale", "Scale", isScale, scale);
float_value("##Is Rotation", "Rotation", isRotation, rotation);
int_value("##Is Delay", "Delay", isDelay, delay);
color4_value("##Is Tint", "Tint", isTint, tint);
color3_value("##Is Color Offset", "Color Offset", isColorOffset, colorOffset);
bool_value("##Is Visible", "Visible", isVisibleSet, isVisible);
ImGui::SameLine();
bool_value("##Is Interpolated", "Interpolated", isInterpolatedSet, isInterpolated);
}
ImGui::EndChild();
auto settingsSize = child_size_get(2);
if (ImGui::BeginChild("##Settings", settingsSize, ImGuiChildFlags_Borders))
{
ImGui::Checkbox("From Selected Frame", &isFromSelectedFrame);
ImGui::SetItemTooltip("The frames after the currently referenced frame will be changed with these values.\nIf"
" off, will use all frames.");
ImGui::BeginDisabled(!isFromSelectedFrame);
input_int_range("Number of Frames", numberFrames, anm2::FRAME_NUM_MIN,
item->frames.size() - reference->frameIndex);
ImGui::SetItemTooltip("Set the number of frames that will be changed.");
ImGui::EndDisabled();
}
ImGui::EndChild();
auto widgetSize = widget_size_with_row_get(4);
auto frame_change = [&](anm2::ChangeType type)
{
anm2::FrameChange frameChange;
if (isCrop) frameChange.crop = std::make_optional(crop);
if (isSize) frameChange.size = std::make_optional(size);
if (isPosition) frameChange.position = std::make_optional(position);
if (isPivot) frameChange.pivot = std::make_optional(pivot);
if (isScale) frameChange.scale = std::make_optional(scale);
if (isRotation) frameChange.rotation = std::make_optional(rotation);
if (isDelay) frameChange.delay = std::make_optional(delay);
if (isTint) frameChange.tint = std::make_optional(tint);
if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset);
if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible);
if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated);
DOCUMENT_EDIT_PTR(document, "Change Frame Properties", Document::FRAMES,
item->frames_change(frameChange, type,
isFromSelectedFrame && document->frame_get() ? reference->frameIndex : 0,
isFromSelectedFrame ? numberFrames : -1));
changePopup.close();
};
if (ImGui::Button("Add", widgetSize)) frame_change(anm2::ADD);
ImGui::SameLine();
if (ImGui::Button("Subtract", widgetSize)) frame_change(anm2::SUBTRACT);
ImGui::SameLine();
if (ImGui::Button("Adjust", widgetSize)) frame_change(anm2::ADJUST);
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) changePopup.close();
ImGui::EndPopup();
}
configurePopup.trigger(); configurePopup.trigger();
if (ImGui::BeginPopupModal(configurePopup.label, &configurePopup.isOpen, ImGuiWindowFlags_NoResize)) if (ImGui::BeginPopupModal(configurePopup.label, &configurePopup.isOpen, ImGuiWindowFlags_NoResize))
@@ -612,20 +247,44 @@ MimeType=application/x-anm2+xml;
if (ImGui::BeginTabBar("##Configure Tabs")) if (ImGui::BeginTabBar("##Configure Tabs"))
{ {
if (ImGui::BeginTabItem("General")) if (ImGui::BeginTabItem("Display"))
{ {
if (ImGui::BeginChild("##Tab Child", childSize, true)) if (ImGui::BeginChild("##Tab Child", childSize, true))
{ {
ImGui::SeparatorText("File"); input_float_range("UI Scale", editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f");
ImGui::SetItemTooltip("Change the scale of the UI.");
ImGui::Checkbox("Autosaving", &editSettings.fileIsAutosave); ImGui::Checkbox("Vsync", &editSettings.isVsync);
ImGui::SetItemTooltip("Toggle vertical sync; synchronizes program update rate with monitor refresh rate.");
}
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("File"))
{
if (ImGui::BeginChild("##Tab Child", childSize, true))
{
ImGui::SeparatorText("Autosave");
ImGui::Checkbox("Enabled", &editSettings.fileIsAutosave);
ImGui::SetItemTooltip("Enables autosaving of documents."); ImGui::SetItemTooltip("Enables autosaving of documents.");
ImGui::BeginDisabled(!editSettings.fileIsAutosave); ImGui::BeginDisabled(!editSettings.fileIsAutosave);
input_int_range("Autosave Time (minutes)", editSettings.fileAutosaveTime, 0, 10); input_int_range("Time (minutes)", editSettings.fileAutosaveTime, 0, 10);
ImGui::SetItemTooltip("If changed, will autosave documents using this interval."); ImGui::SetItemTooltip("If changed, will autosave documents using this interval.");
ImGui::EndDisabled(); ImGui::EndDisabled();
}
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Input"))
{
if (ImGui::BeginChild("##Tab Child", childSize, true))
{
ImGui::SeparatorText("Keyboard"); ImGui::SeparatorText("Keyboard");
input_float_range("Repeat Delay (seconds)", editSettings.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f, 0.05f, input_float_range("Repeat Delay (seconds)", editSettings.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f, 0.05f,
@@ -636,17 +295,9 @@ MimeType=application/x-anm2+xml;
"%.3f"); "%.3f");
ImGui::SetItemTooltip("Set how often, after repeating begins, key inputs will be fired."); ImGui::SetItemTooltip("Set how often, after repeating begins, key inputs will be fired.");
ImGui::SeparatorText("UI"); ImGui::SeparatorText("Zoom");
input_float_range("UI Scale", editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f"); input_float_range("Step", editSettings.viewZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, "%.0f");
ImGui::SetItemTooltip("Change the scale of the UI.");
ImGui::Checkbox("Vsync", &editSettings.isVsync);
ImGui::SetItemTooltip("Toggle vertical sync; synchronizes program update rate with monitor refresh rate.");
ImGui::SeparatorText("View");
input_float_range("Zoom Step", editSettings.viewZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, "%.0f");
ImGui::SetItemTooltip("When zooming in/out with mouse or shortcut, this value will be used."); ImGui::SetItemTooltip("When zooming in/out with mouse or shortcut, this value will be used.");
} }
ImGui::EndChild(); ImGui::EndChild();
@@ -724,6 +375,7 @@ MimeType=application/x-anm2+xml;
if (ImGui::Button("Save", widgetSize)) if (ImGui::Button("Save", widgetSize))
{ {
settings = editSettings; settings = editSettings;
manager.chords_set(settings);
configurePopup.close(); configurePopup.close();
} }
ImGui::SetItemTooltip("Use the configured settings."); ImGui::SetItemTooltip("Use the configured settings.");
@@ -745,9 +397,6 @@ MimeType=application/x-anm2+xml;
if (ImGui::BeginPopupModal(renderPopup.label, &renderPopup.isOpen, ImGuiWindowFlags_NoResize)) if (ImGui::BeginPopupModal(renderPopup.label, &renderPopup.isOpen, ImGuiWindowFlags_NoResize))
{ {
auto animation = document ? document->animation_get() : nullptr;
if (!animation) renderPopup.close();
auto& playback = document->playback; auto& playback = document->playback;
auto& ffmpegPath = settings.renderFFmpegPath; auto& ffmpegPath = settings.renderFFmpegPath;
auto& path = settings.renderPath; auto& path = settings.renderPath;
@@ -802,39 +451,47 @@ MimeType=application/x-anm2+xml;
if (ImGui::Combo("Type", &type, render::STRINGS, render::COUNT)) replace_extension(); if (ImGui::Combo("Type", &type, render::STRINGS, render::COUNT)) replace_extension();
ImGui::SetItemTooltip("Set the type of the output."); ImGui::SetItemTooltip("Set the type of the output.");
ImGui::BeginDisabled(type != render::PNGS); if (type == render::PNGS)
{
ImGui::Separator();
input_text_string("Format", &format); input_text_string("Format", &format);
ImGui::SetItemTooltip( ImGui::SetItemTooltip(
"For outputted images, each image will use this format.\n{} represents the index of each image."); "For outputted images, each image will use this format.\n{} represents the index of each image.");
ImGui::EndDisabled(); }
ImGui::BeginDisabled(!isRange); ImGui::Separator();
input_int_range("Start", start, 0, animation->frameNum - 1);
ImGui::SetItemTooltip("Set the starting time of the animation.");
input_int_range("End", end, start + 1, animation->frameNum);
ImGui::SetItemTooltip("Set the ending time of the animation.");
ImGui::EndDisabled();
ImGui::BeginDisabled(!isRaw);
input_float_range("Scale", scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx");
ImGui::SetItemTooltip("Set the output scale of the animation.");
ImGui::EndDisabled();
if (ImGui::Checkbox("Custom Range", &isRange)) if (ImGui::Checkbox("Custom Range", &isRange))
if (!isRange) range_to_length(); if (!isRange) range_to_length();
ImGui::SetItemTooltip("Toggle using a custom range for the animation."); ImGui::SetItemTooltip("Toggle using a custom range for the animation.");
ImGui::SameLine(); if (isRange)
{
input_int_range("Start", start, 0, animation->frameNum - 1);
ImGui::SetItemTooltip("Set the starting time of the animation.");
input_int_range("End", end, start + 1, animation->frameNum);
ImGui::SetItemTooltip("Set the ending time of the animation.");
}
ImGui::Separator();
ImGui::Checkbox("Raw", &isRaw); ImGui::Checkbox("Raw", &isRaw);
ImGui::SetItemTooltip("Record only the raw animation; i.e., only its layers, to its bounds."); ImGui::SetItemTooltip("Record only the raw animation; i.e., only its layers, to its bounds.");
ImGui::SameLine(); if (isRaw)
{
input_float_range("Scale", scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx");
ImGui::SetItemTooltip("Set the output scale of the animation.");
}
ImGui::Separator();
ImGui::Checkbox("Sound", &settings.timelineIsSound); ImGui::Checkbox("Sound", &settings.timelineIsSound);
ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window.\nThe " ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window.\nThe "
"output animation will use the played sounds."); "output animation will use the played sounds.");
ImGui::Separator();
if (ImGui::Button("Render", widgetSize)) if (ImGui::Button("Render", widgetSize))
{ {
manager.isRecordingStart = true; manager.isRecordingStart = true;
@@ -857,14 +514,205 @@ MimeType=application/x-anm2+xml;
if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize)) if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize))
{ {
if (ImGui::Button("Close")) aboutPopup.close(); struct Credit
{
const char* string{};
font::Type font{font::REGULAR};
};
struct ScrollingCredit
{
int index{};
float offset{};
};
struct CreditsState
{
std::vector<ScrollingCredit> active{};
float spawnTimer{1.0f};
int nextIndex{};
};
static constexpr auto ANM2ED_LABEL = "Anm2Ed";
static constexpr auto VERSION_LABEL = "Version 2.0";
static constexpr auto CREDIT_DELAY = 1.0f;
static constexpr auto CREDIT_SCROLL_SPEED = 25.0f;
static constexpr Credit CREDITS[] = {
{"Anm2Ed", font::BOLD},
{"License: GPLv3"},
{""},
{"Designer", font::BOLD},
{"Shweet"},
{""},
{"Additional Help", font::BOLD},
{"im-tem"},
{""},
{"Based on the work of:", font::BOLD},
{"Adrian Gavrilita"},
{"Simon Parzer"},
{"Matt Kapuszczak"},
{""},
{"XM Music", font::BOLD},
{"Drozerix"},
{"\"Keygen Wraith\""},
{"https://modarchive.org/module.php?207854"},
{"License: CC0"},
{""},
{"Libraries", font::BOLD},
{"Dear ImGui"},
{"https://github.com/ocornut/imgui"},
{"License: MIT"},
{""},
{"SDL"},
{"https://github.com/libsdl-org/SDL"},
{"License: zlib"},
{""},
{"SDL_mixer"},
{"https://github.com/libsdl-org/SDL_mixer"},
{"License: zlib"},
{""},
{"tinyxml2"},
{"https://github.com/leethomason/tinyxml2"},
{"License: zlib"},
{""},
{"glm"},
{"https://github.com/g-truc/glm"},
{"License: MIT"},
{""},
{"lunasvg"},
{"https://github.com/sammycage/lunasvg"},
{"License: MIT"},
{""},
{"libxm"},
{"https://github.com/Artefact2/libxm"},
{"License: WTFPL"},
{""},
{"Icons", font::BOLD},
{"Remix Icons"},
{"remixicon.com"},
{"License: Apache"},
{""},
{"Font", font::BOLD},
{"Noto Sans"},
{"https://fonts.google.com/noto/specimen/Noto+Sans"},
{"License: OFL"},
{""},
{"Special Thanks", font::BOLD},
{"Edmund McMillen"},
{"Florian Himsl"},
{"Tyrone Rodriguez"},
{"The-Vinh Truong (_kilburn)"},
{"Everyone who waited patiently for this to be finished"},
{"Everyone else who has worked on The Binding of Isaac!"},
{""},
{""},
{""},
{""},
{""},
{"enjoy the jams :)"},
{""},
{""},
{""},
{""},
{""},
};
static constexpr auto CREDIT_COUNT = (int)(sizeof(CREDITS) / sizeof(Credit));
static CreditsState creditsState{};
auto credits_reset = [&]()
{
resources.music.play(true);
creditsState = {};
creditsState.spawnTimer = CREDIT_DELAY;
};
if (aboutPopup.isJustOpened) credits_reset();
auto size = ImGui::GetContentRegionAvail();
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE);
ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(ANM2ED_LABEL).x) / 2);
ImGui::Text(ANM2ED_LABEL);
ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(VERSION_LABEL).x) / 2);
ImGui::Text(VERSION_LABEL);
ImGui::PopFont();
auto creditRegionPos = ImGui::GetCursorScreenPos();
auto creditRegionSize = ImGui::GetContentRegionAvail();
if (creditRegionSize.y > 0.0f && creditRegionSize.x > 0.0f)
{
auto drawList = ImGui::GetWindowDrawList();
auto clipMax = ImVec2(creditRegionPos.x + creditRegionSize.x, creditRegionPos.y + creditRegionSize.y);
drawList->PushClipRect(creditRegionPos, clipMax, true);
auto delta = ImGui::GetIO().DeltaTime;
creditsState.spawnTimer -= delta;
auto maxVisible = std::max(1, (int)std::floor(creditRegionSize.y / (float)font::SIZE));
while (creditsState.active.size() < (size_t)maxVisible && creditsState.spawnTimer <= 0.0f)
{
creditsState.active.push_back({creditsState.nextIndex, 0.0f});
creditsState.nextIndex = (creditsState.nextIndex + 1) % CREDIT_COUNT;
creditsState.spawnTimer += CREDIT_DELAY;
}
auto baseY = clipMax.y - (float)font::SIZE;
auto baseColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
auto fadeSpan = (float)font::SIZE * 2.0f;
for (auto it = creditsState.active.begin(); it != creditsState.active.end();)
{
it->offset += CREDIT_SCROLL_SPEED * delta;
auto yPos = baseY - it->offset;
if (yPos + font::SIZE < creditRegionPos.y)
{
it = creditsState.active.erase(it);
continue;
}
const auto& credit = CREDITS[it->index];
auto fontPtr = resources.fonts[credit.font].get();
auto textSize = fontPtr->CalcTextSizeA((float)font::SIZE, FLT_MAX, 0.0f, credit.string);
auto xPos = creditRegionPos.x + (creditRegionSize.x - textSize.x) * 0.5f;
auto alpha = 1.0f;
auto topDist = yPos - creditRegionPos.y;
if (topDist < fadeSpan) alpha *= std::clamp(topDist / fadeSpan, 0.0f, 1.0f);
auto bottomDist = (creditRegionPos.y + creditRegionSize.y) - (yPos + font::SIZE);
if (bottomDist < fadeSpan) alpha *= std::clamp(bottomDist / fadeSpan, 0.0f, 1.0f);
if (alpha <= 0.0f)
{
++it;
continue;
}
auto color = baseColor;
color.w *= alpha;
drawList->AddText(fontPtr, (float)font::SIZE, ImVec2(xPos, yPos), ImGui::GetColorU32(color), credit.string);
++it;
}
drawList->PopClipRect();
}
ImGui::EndPopup(); ImGui::EndPopup();
} }
if (shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_NEW); if (resources.music.is_playing() && !aboutPopup.isOpen) resources.music.stop();
if (shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN);
if (shortcut(settings.shortcutSave, shortcut::GLOBAL)) document->save(); aboutPopup.end();
if (shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE);
if (shortcut(settings.shortcutExit, shortcut::GLOBAL)) isQuitting = true; 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)) manager.save();
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;
} }
} }

View File

@@ -2,7 +2,6 @@
#include "canvas.h" #include "canvas.h"
#include "dialog.h" #include "dialog.h"
#include "filesystem_.h"
#include "imgui_.h" #include "imgui_.h"
#include "manager.h" #include "manager.h"
#include "resources.h" #include "resources.h"
@@ -15,21 +14,12 @@ namespace anm2ed::imgui
Canvas generate; Canvas generate;
float generateTime{}; float generateTime{};
PopupHelper generatePopup{PopupHelper("Generate Animation from Grid")}; PopupHelper generatePopup{PopupHelper("Generate Animation from Grid")};
PopupHelper changePopup{PopupHelper("Change All Frame Properties", imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper renderPopup{PopupHelper("Render Animation", imgui::POPUP_SMALL_NO_HEIGHT)}; PopupHelper renderPopup{PopupHelper("Render Animation", imgui::POPUP_SMALL_NO_HEIGHT)};
PopupHelper configurePopup{PopupHelper("Configure")}; PopupHelper configurePopup{PopupHelper("Configure")};
PopupHelper aboutPopup{PopupHelper("About")}; PopupHelper aboutPopup{PopupHelper("About")};
Settings editSettings{}; Settings editSettings{};
int selectedShortcut{-1}; int selectedShortcut{-1};
int creditsIndex{};
#if defined(_WIN32) || defined(__unix__)
bool isAbleToAssociateAnm2 = true;
#else
bool isAbleToAssociateAnm2 = false;
#endif
bool isAnm2Association = std::filesystem::exists(util::filesystem::path_application_get());
bool isQuittingMode{}; bool isQuittingMode{};
public: public:

View File

@@ -24,14 +24,13 @@ namespace anm2ed::imgui
constexpr auto NULL_RECT_SIZE = vec2(100); constexpr auto NULL_RECT_SIZE = vec2(100);
constexpr auto TRIGGER_TEXT_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); constexpr auto TRIGGER_TEXT_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.5f);
AnimationPreview::AnimationPreview() : Canvas(vec2()) AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
{
}
void AnimationPreview::tick(Manager& manager, Document& document, Settings& settings) void AnimationPreview::tick(Manager& manager, Document& document, Settings& settings)
{ {
auto& anm2 = document.anm2; auto& anm2 = document.anm2;
auto& playback = document.playback; auto& playback = document.playback;
auto& frameTime = document.frameTime;
auto& zoom = document.previewZoom; auto& zoom = document.previewZoom;
auto& pan = document.previewPan; auto& pan = document.previewPan;
auto& isRootTransform = settings.previewIsRootTransform; auto& isRootTransform = settings.previewIsRootTransform;
@@ -53,7 +52,7 @@ namespace anm2ed::imgui
} }
} }
document.reference.frameTime = playback.time; frameTime = playback.time;
} }
if (manager.isRecording) if (manager.isRecording)
@@ -124,6 +123,7 @@ namespace anm2ed::imgui
settings.previewIsGrid = false; settings.previewIsGrid = false;
settings.previewIsAxes = false; settings.previewIsAxes = false;
settings.timelineIsOnlyShowLayers = true; settings.timelineIsOnlyShowLayers = true;
settings.onionskinIsEnabled = false;
savedZoom = zoom; savedZoom = zoom;
savedPan = pan; savedPan = pan;
@@ -211,13 +211,13 @@ namespace anm2ed::imgui
auto widgetSize = widget_size_with_row_get(2); auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutCenterView); shortcut(manager.chords[SHORTCUT_CENTER_VIEW]);
if (ImGui::Button("Center View", widgetSize)) pan = vec2(); if (ImGui::Button("Center View", widgetSize)) pan = vec2();
set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView);
ImGui::SameLine(); ImGui::SameLine();
shortcut(settings.shortcutFit); shortcut(manager.chords[SHORTCUT_FIT]);
if (ImGui::Button("Fit", widgetSize)) if (ImGui::Button("Fit", widgetSize))
if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform)); if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform));
set_item_tooltip_shortcut("Set the view to match the extent of the animation.", settings.shortcutFit); set_item_tooltip_shortcut("Set the view to match the extent of the animation.", settings.shortcutFit);
@@ -397,7 +397,7 @@ namespace anm2ed::imgui
onionskin_render(time, settings.onionskinAfterCount, 1, settings.onionskinAfterColor); onionskin_render(time, settings.onionskinAfterCount, 1, settings.onionskinAfterColor);
}; };
auto frameTime = reference.frameTime > -1 && !playback.isPlaying ? reference.frameTime : playback.time; auto frameTime = document.frameTime > -1 && !playback.isPlaying ? document.frameTime : playback.time;
if (animation) if (animation)
{ {
@@ -408,7 +408,7 @@ namespace anm2ed::imgui
render(animation, frameTime); render(animation, frameTime);
if (auto overlayAnimation = anm2.animation_get({overlayIndex})) if (auto overlayAnimation = anm2.animation_get(overlayIndex))
render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency)); render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency));
if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime); if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime);
@@ -469,8 +469,8 @@ namespace anm2ed::imgui
auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown; auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown;
auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased;
auto isZoomIn = chord_repeating(string_to_chord(settings.shortcutZoomIn)); auto isZoomIn = chord_repeating(manager.chords[SHORTCUT_ZOOM_IN]);
auto isZoomOut = chord_repeating(string_to_chord(settings.shortcutZoomOut)); auto isZoomOut = chord_repeating(manager.chords[SHORTCUT_ZOOM_OUT]);
auto isBegin = isMouseClicked || isKeyJustPressed; auto isBegin = isMouseClicked || isKeyJustPressed;
auto isDuring = isMouseDown || isKeyDown; auto isDuring = isMouseDown || isKeyDown;

View File

@@ -48,7 +48,10 @@ namespace anm2ed::imgui
ImGui::SetNextItemSelectionUserData((int)i); ImGui::SetNextItemSelectionUserData((int)i);
if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i), if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i),
animation.name, selection.contains((int)i))) animation.name, selection.contains((int)i)))
{
reference = {(int)i}; reference = {(int)i};
document.frames.clear();
}
if (ImGui::IsItemHovered()) hovered = (int)i; if (ImGui::IsItemHovered()) hovered = (int)i;
ImGui::PopFont(); ImGui::PopFont();
@@ -155,9 +158,9 @@ namespace anm2ed::imgui
DOCUMENT_EDIT(document, "Paste Animation(s)", Document::ANIMATIONS, deserialize()); DOCUMENT_EDIT(document, "Paste Animation(s)", Document::ANIMATIONS, deserialize());
}; };
if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut();
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{ {
@@ -171,7 +174,7 @@ namespace anm2ed::imgui
auto widgetSize = widget_size_with_row_get(5); auto widgetSize = widget_size_with_row_get(5);
shortcut(settings.shortcutAdd); shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Add", widgetSize)) if (ImGui::Button("Add", widgetSize))
{ {
auto add = [&]() auto add = [&]()
@@ -204,7 +207,7 @@ namespace anm2ed::imgui
ImGui::BeginDisabled(selection.empty()); ImGui::BeginDisabled(selection.empty());
{ {
shortcut(settings.shortcutDuplicate); shortcut(manager.chords[SHORTCUT_DUPLICATE]);
if (ImGui::Button("Duplicate", widgetSize)) if (ImGui::Button("Duplicate", widgetSize))
{ {
auto duplicate = [&]() auto duplicate = [&]()
@@ -225,7 +228,7 @@ namespace anm2ed::imgui
ImGui::SameLine(); ImGui::SameLine();
if (shortcut(settings.shortcutMerge, shortcut::FOCUSED) && !selection.empty()) if (shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED) && !selection.empty())
{ {
auto merge_quick = [&]() auto merge_quick = [&]()
{ {
@@ -270,7 +273,7 @@ namespace anm2ed::imgui
ImGui::SameLine(); ImGui::SameLine();
shortcut(settings.shortcutRemove); shortcut(manager.chords[SHORTCUT_REMOVE]);
if (ImGui::Button("Remove", widgetSize)) if (ImGui::Button("Remove", widgetSize))
{ {
auto remove = [&]() auto remove = [&]()
@@ -285,11 +288,11 @@ namespace anm2ed::imgui
DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, remove()); DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, remove());
} }
set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate); set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutRemove);
ImGui::SameLine(); ImGui::SameLine();
shortcut(settings.shortcutDefault); shortcut(manager.chords[SHORTCUT_DEFAULT]);
ImGui::BeginDisabled(selection.size() != 1); ImGui::BeginDisabled(selection.size() != 1);
if (ImGui::Button("Default", widgetSize)) if (ImGui::Button("Default", widgetSize))
{ {
@@ -369,6 +372,8 @@ namespace anm2ed::imgui
auto widgetSize = widget_size_with_row_get(2); auto widgetSize = widget_size_with_row_get(2);
ImGui::BeginDisabled(mergeSelection.empty());
{
if (ImGui::Button("Merge", widgetSize)) if (ImGui::Button("Merge", widgetSize))
{ {
auto merge = [&]() auto merge = [&]()
@@ -384,6 +389,8 @@ namespace anm2ed::imgui
DOCUMENT_EDIT(document, "Merge Animations", Document::ANIMATIONS, merge()); DOCUMENT_EDIT(document, "Merge Animations", Document::ANIMATIONS, merge());
merge_close(); merge_close();
} }
}
ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Close", widgetSize)) merge_close(); if (ImGui::Button("Close", widgetSize)) merge_close();

View File

@@ -73,8 +73,8 @@ namespace anm2ed::imgui
toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); toasts.error(std::format("Failed to deserialize event(s): {}", errorString));
}; };
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{ {
@@ -96,7 +96,7 @@ namespace anm2ed::imgui
auto widgetSize = widget_size_with_row_get(2); auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutAdd); shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Add", widgetSize)) if (ImGui::Button("Add", widgetSize))
{ {
auto add = [&]() auto add = [&]()
@@ -112,7 +112,7 @@ namespace anm2ed::imgui
set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd); set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd);
ImGui::SameLine(); ImGui::SameLine();
shortcut(settings.shortcutRemove); shortcut(manager.chords[SHORTCUT_REMOVE]);
ImGui::BeginDisabled(unused.empty()); ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize)) if (ImGui::Button("Remove Unused", widgetSize))
{ {

View File

@@ -16,7 +16,11 @@ namespace anm2ed::imgui
if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties)) if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties))
{ {
auto& document = *manager.get(); auto& document = *manager.get();
auto& frames = document.frames.selection;
auto& type = document.reference.itemType; auto& type = document.reference.itemType;
if (frames.size() <= 1)
{
auto frame = document.frame_get(); auto frame = document.frame_get();
auto useFrame = frame ? *frame : anm2::Frame(); auto useFrame = frame ? *frame : anm2::Frame();
@@ -24,20 +28,24 @@ namespace anm2ed::imgui
{ {
if (type == anm2::TRIGGER) if (type == anm2::TRIGGER)
{ {
if (combo_negative_one_indexed("Event", frame ? &useFrame.eventID : &dummy_value<int>(), if (combo_negative_one_indexed("Event", frame ? &useFrame.eventID : &dummy_value_negative<int>(),
document.event.labels)) document.event.labels))
DOCUMENT_EDIT(document, "Trigger Event", Document::FRAMES, frame->eventID = useFrame.eventID); DOCUMENT_EDIT(document, "Trigger Event", Document::FRAMES, frame->eventID = useFrame.eventID);
ImGui::SetItemTooltip("Change the event this trigger uses."); ImGui::SetItemTooltip("Change the event this trigger uses.");
if (combo_negative_one_indexed("Sound", frame ? &useFrame.soundID : &dummy_value<int>(), if (combo_negative_one_indexed("Sound", frame ? &useFrame.soundID : &dummy_value_negative<int>(),
document.sound.labels)) document.sound.labels))
DOCUMENT_EDIT(document, "Trigger Sound", Document::FRAMES, frame->soundID = useFrame.soundID); DOCUMENT_EDIT(document, "Trigger Sound", Document::FRAMES, frame->soundID = useFrame.soundID);
ImGui::SetItemTooltip("Change the sound this trigger uses."); ImGui::SetItemTooltip("Change the sound this trigger uses.");
if (ImGui::InputInt("At Frame", frame ? &useFrame.atFrame : &dummy_value<int>(), imgui::STEP, if (ImGui::InputInt("At Frame", frame ? &useFrame.atFrame : &dummy_value<int>(), STEP, STEP_FAST,
imgui::STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0))
DOCUMENT_EDIT(document, "Trigger At Frame", Document::FRAMES, frame->atFrame = useFrame.atFrame); DOCUMENT_EDIT(document, "Trigger At Frame", Document::FRAMES, frame->atFrame = useFrame.atFrame);
ImGui::SetItemTooltip("Change the frame the trigger will be activated at."); ImGui::SetItemTooltip("Change the frame the trigger will be activated at.");
if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value<bool>()))
DOCUMENT_EDIT(document, "Trigger Visibility", Document::FRAMES, frame->isVisible = useFrame.isVisible);
ImGui::SetItemTooltip("Toggle the trigger's visibility.");
} }
else else
{ {
@@ -74,14 +82,15 @@ namespace anm2ed::imgui
DOCUMENT_EDIT(document, "Frame Scale", Document::FRAMES, frame->scale = useFrame.scale); DOCUMENT_EDIT(document, "Frame Scale", Document::FRAMES, frame->scale = useFrame.scale);
ImGui::SetItemTooltip("Change the scale of the frame, in percent."); ImGui::SetItemTooltip("Change the scale of the frame, in percent.");
if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value<float>(), imgui::STEP, if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value<float>(), STEP, STEP_FAST,
imgui::STEP_FAST, frame ? float_format_get(useFrame.rotation) : "")) frame ? float_format_get(useFrame.rotation) : ""))
DOCUMENT_EDIT(document, "Frame Rotation", Document::FRAMES, frame->rotation = useFrame.rotation); DOCUMENT_EDIT(document, "Frame Rotation", Document::FRAMES, frame->rotation = useFrame.rotation);
ImGui::SetItemTooltip("Change the rotation of the frame."); ImGui::SetItemTooltip("Change the rotation of the frame.");
if (ImGui::InputInt("Duration", frame ? &useFrame.delay : &dummy_value<int>(), imgui::STEP, imgui::STEP_FAST, if (input_int_range("Duration", frame ? useFrame.duration : dummy_value<int>(),
frame ? anm2::FRAME_DURATION_MIN : 0, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST,
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0))
DOCUMENT_EDIT(document, "Frame Duration", Document::FRAMES, frame->delay = useFrame.delay); DOCUMENT_EDIT(document, "Frame Duration", Document::FRAMES, frame->duration = useFrame.duration);
ImGui::SetItemTooltip("Change how long the frame lasts."); ImGui::SetItemTooltip("Change how long the frame lasts.");
if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value<float>())) if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value<float>()))
@@ -89,7 +98,8 @@ namespace anm2ed::imgui
ImGui::SetItemTooltip("Change the tint of the frame."); ImGui::SetItemTooltip("Change the tint of the frame.");
if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value<float>())) if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value<float>()))
DOCUMENT_EDIT(document, "Frame Color Offset", Document::FRAMES, frame->colorOffset = useFrame.colorOffset); DOCUMENT_EDIT(document, "Frame Color Offset", Document::FRAMES,
frame->colorOffset = useFrame.colorOffset);
ImGui::SetItemTooltip("Change the color added onto the frame."); ImGui::SetItemTooltip("Change the color added onto the frame.");
if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value<bool>())) if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value<bool>()))
@@ -104,7 +114,7 @@ namespace anm2ed::imgui
ImGui::SetItemTooltip( ImGui::SetItemTooltip(
"Toggle the frame interpolating; i.e., blending its values into the next frame based on the time."); "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time.");
auto widgetSize = imgui::widget_size_with_row_get(2); auto widgetSize = widget_size_with_row_get(2);
if (ImGui::Button("Flip X", widgetSize)) if (ImGui::Button("Flip X", widgetSize))
DOCUMENT_EDIT(document, "Frame Flip X", Document::FRAMES, frame->scale.x = -frame->scale.x); DOCUMENT_EDIT(document, "Frame Flip X", Document::FRAMES, frame->scale.x = -frame->scale.x);
@@ -119,6 +129,105 @@ namespace anm2ed::imgui
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
else
{
auto& isCrop = settings.changeIsCrop;
auto& isSize = settings.changeIsSize;
auto& isPosition = settings.changeIsPosition;
auto& isPivot = settings.changeIsPivot;
auto& isScale = settings.changeIsScale;
auto& isRotation = settings.changeIsRotation;
auto& isDelay = settings.changeIsDelay;
auto& isTint = settings.changeIsTint;
auto& isColorOffset = settings.changeIsColorOffset;
auto& isVisibleSet = settings.changeIsVisibleSet;
auto& isInterpolatedSet = settings.changeIsInterpolatedSet;
auto& crop = settings.changeCrop;
auto& size = settings.changeSize;
auto& position = settings.changePosition;
auto& pivot = settings.changePivot;
auto& scale = settings.changeScale;
auto& rotation = settings.changeRotation;
auto& duration = settings.changeDelay;
auto& tint = settings.changeTint;
auto& colorOffset = settings.changeColorOffset;
auto& isVisible = settings.changeIsVisible;
auto& isInterpolated = settings.changeIsInterpolated;
#define PROPERTIES_WIDGET(body) \
ImGui::Checkbox(checkboxLabel, &isEnabled); \
ImGui::SameLine(); \
ImGui::BeginDisabled(!isEnabled); \
body; \
ImGui::EndDisabled();
auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value)
{ PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value)); };
auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value)
{ PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value))); };
auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value)
{ PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value))); };
auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value)
{ PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), vec2_format_get(value))); };
auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value)
{ PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, float_format_get(value))); };
auto duration_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value)
{
PROPERTIES_WIDGET(
input_int_range(valueLabel, value, anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST));
};
#undef PROPERTIES_WIDGET
float2_value("##Is Crop", "Crop", isCrop, crop);
float2_value("##Is Size", "Size", isSize, size);
float2_value("##Is Position", "Position", isPosition, position);
float2_value("##Is Pivot", "Pivot", isPivot, pivot);
float2_value("##Is Scale", "Scale", isScale, scale);
float_value("##Is Rotation", "Rotation", isRotation, rotation);
duration_value("##Is Delay", "Delay", isDelay, duration);
color4_value("##Is Tint", "Tint", isTint, tint);
color3_value("##Is Color Offset", "Color Offset", isColorOffset, colorOffset);
bool_value("##Is Visible", "Visible", isVisibleSet, isVisible);
ImGui::SameLine();
bool_value("##Is Interpolated", "Interpolated", isInterpolatedSet, isInterpolated);
auto frame_change = [&](anm2::ChangeType type)
{
anm2::FrameChange frameChange;
if (isCrop) frameChange.crop = std::make_optional(crop);
if (isSize) frameChange.size = std::make_optional(size);
if (isPosition) frameChange.position = std::make_optional(position);
if (isPivot) frameChange.pivot = std::make_optional(pivot);
if (isScale) frameChange.scale = std::make_optional(scale);
if (isRotation) frameChange.rotation = std::make_optional(rotation);
if (isDelay) frameChange.duration = std::make_optional(duration);
if (isTint) frameChange.tint = std::make_optional(tint);
if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset);
if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible);
if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated);
DOCUMENT_EDIT(document, "Change Frame Properties", Document::FRAMES,
document.item_get()->frames_change(frameChange, type, *frames.begin(), (int)frames.size()));
};
auto widgetSize = widget_size_with_row_get(3);
if (ImGui::Button("Adjust", widgetSize)) frame_change(anm2::ADJUST);
ImGui::SetItemTooltip("Set the value of each specified value onto the frame's equivalent.");
ImGui::SameLine();
if (ImGui::Button("Add", widgetSize)) frame_change(anm2::ADD);
ImGui::SetItemTooltip("Add the specified values onto each frame.\n(Boolean values will simply be set.)");
ImGui::SameLine();
if (ImGui::Button("Subtract", widgetSize)) frame_change(anm2::SUBTRACT);
ImGui::SetItemTooltip("Subtract the specified values from each frame.\n(Boolean values will simply be set.)");
}
}
ImGui::End(); ImGui::End();
} }
} }

View File

@@ -84,8 +84,8 @@ namespace anm2ed::imgui
toasts.error(std::format("Failed to deserialize layer(s): {}", errorString)); toasts.error(std::format("Failed to deserialize layer(s): {}", errorString));
}; };
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{ {
@@ -107,12 +107,12 @@ namespace anm2ed::imgui
auto widgetSize = widget_size_with_row_get(2); auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutAdd); shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Add", widgetSize)) manager.layer_properties_open(); if (ImGui::Button("Add", widgetSize)) manager.layer_properties_open();
set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd); set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd);
ImGui::SameLine(); ImGui::SameLine();
shortcut(settings.shortcutRemove); shortcut(manager.chords[SHORTCUT_REMOVE]);
ImGui::BeginDisabled(unused.empty()); ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize)) if (ImGui::Button("Remove Unused", widgetSize))
{ {

View File

@@ -84,8 +84,8 @@ namespace anm2ed::imgui
toasts.error(std::format("Failed to deserialize null(s): {}", errorString)); toasts.error(std::format("Failed to deserialize null(s): {}", errorString));
}; };
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{ {
@@ -107,12 +107,12 @@ namespace anm2ed::imgui
auto widgetSize = widget_size_with_row_get(2); auto widgetSize = widget_size_with_row_get(2);
shortcut(settings.shortcutAdd); shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Add", widgetSize)) manager.null_properties_open(); if (ImGui::Button("Add", widgetSize)) manager.null_properties_open();
set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd); set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd);
ImGui::SameLine(); ImGui::SameLine();
shortcut(settings.shortcutRemove); shortcut(manager.chords[SHORTCUT_REMOVE]);
ImGui::BeginDisabled(unused.empty()); ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize)) if (ImGui::Button("Remove Unused", widgetSize))
{ {

View File

@@ -11,7 +11,7 @@ namespace anm2ed::imgui
{ {
constexpr auto FRAMES_MAX = 100; constexpr auto FRAMES_MAX = 100;
void Onionskin::update(Settings& settings) void Onionskin::update(Manager& manager, Settings& settings)
{ {
auto& isEnabled = settings.onionskinIsEnabled; auto& isEnabled = settings.onionskinIsEnabled;
auto& beforeCount = settings.onionskinBeforeCount; auto& beforeCount = settings.onionskinBeforeCount;
@@ -49,7 +49,7 @@ namespace anm2ed::imgui
} }
ImGui::End(); ImGui::End();
if (shortcut(settings.shortcutOnionskin, shortcut::GLOBAL)) isEnabled = !isEnabled; if (shortcut(manager.chords[SHORTCUT_ONIONSKIN], shortcut::GLOBAL)) isEnabled = !isEnabled;
} }
} }

View File

@@ -1,12 +1,12 @@
#pragma once #pragma once
#include "settings.h" #include "manager.h"
namespace anm2ed::imgui namespace anm2ed::imgui
{ {
class Onionskin class Onionskin
{ {
public: public:
void update(Settings&); void update(Manager&, Settings&);
}; };
} }

View File

@@ -79,8 +79,8 @@ namespace anm2ed::imgui
toasts.error(std::format("Failed to deserialize sound(s): {}", errorString)); toasts.error(std::format("Failed to deserialize sound(s): {}", errorString));
}; };
if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{ {
@@ -102,12 +102,12 @@ namespace anm2ed::imgui
auto widgetSize = imgui::widget_size_with_row_get(2); auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd); imgui::shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Add", widgetSize)) dialog.file_open(dialog::SOUND_OPEN); if (ImGui::Button("Add", widgetSize)) dialog.file_open(dialog::SOUND_OPEN);
imgui::set_item_tooltip_shortcut("Add a sound.", settings.shortcutAdd); imgui::set_item_tooltip_shortcut("Add a sound.", settings.shortcutAdd);
ImGui::SameLine(); ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove); imgui::shortcut(manager.chords[SHORTCUT_REMOVE]);
ImGui::BeginDisabled(unused.empty()); ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize)) if (ImGui::Button("Remove Unused", widgetSize))
{ {

View File

@@ -17,9 +17,7 @@ namespace anm2ed::imgui
{ {
constexpr auto PIVOT_COLOR = color::PINK; constexpr auto PIVOT_COLOR = color::PINK;
SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) {}
{
}
void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources) void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources)
{ {
@@ -80,13 +78,13 @@ namespace anm2ed::imgui
auto widgetSize = ImVec2(imgui::row_widget_width_get(2), 0); auto widgetSize = ImVec2(imgui::row_widget_width_get(2), 0);
imgui::shortcut(settings.shortcutCenterView); imgui::shortcut(manager.chords[SHORTCUT_CENTER_VIEW]);
if (ImGui::Button("Center View", widgetSize)) center_view(); if (ImGui::Button("Center View", widgetSize)) center_view();
imgui::set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); imgui::set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView);
ImGui::SameLine(); ImGui::SameLine();
imgui::shortcut(settings.shortcutFit); imgui::shortcut(manager.chords[SHORTCUT_FIT]);
if (ImGui::Button("Fit", widgetSize)) if (ImGui::Button("Fit", widgetSize))
if (spritesheet) set_to_rect(zoom, pan, {0, 0, spritesheet->texture.size.x, spritesheet->texture.size.y}); if (spritesheet) set_to_rect(zoom, pan, {0, 0, spritesheet->texture.size.x, spritesheet->texture.size.y});
imgui::set_item_tooltip_shortcut("Set the view to match the extent of the spritesheet.", settings.shortcutFit); imgui::set_item_tooltip_shortcut("Set the view to match the extent of the spritesheet.", settings.shortcutFit);

View File

@@ -52,8 +52,8 @@ namespace anm2ed::imgui
toasts.error(std::format("Failed to deserialize spritesheet(s): {}", errorString)); toasts.error(std::format("Failed to deserialize spritesheet(s): {}", errorString));
}; };
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
@@ -181,9 +181,9 @@ namespace anm2ed::imgui
} }
ImGui::EndChild(); ImGui::EndChild();
auto rowOneWidgetSize = widget_size_with_row_get(4); auto rowOneWidgetSize = widget_size_with_row_get(3);
shortcut(settings.shortcutAdd); shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Add", rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_OPEN); if (ImGui::Button("Add", rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_OPEN);
set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd); set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd);
@@ -238,12 +238,12 @@ namespace anm2ed::imgui
dialog.reset(); dialog.reset();
} }
ImGui::SameLine(); auto rowTwoWidgetSize = widget_size_with_row_get(2);
ImGui::BeginDisabled(unused.empty()); ImGui::BeginDisabled(unused.empty());
{ {
shortcut(settings.shortcutRemove); shortcut(manager.chords[SHORTCUT_REMOVE]);
if (ImGui::Button("Remove Unused", rowOneWidgetSize)) if (ImGui::Button("Remove Unused", rowTwoWidgetSize))
{ {
auto remove_unused = [&]() auto remove_unused = [&]()
{ {
@@ -263,26 +263,6 @@ namespace anm2ed::imgui
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
auto rowTwoWidgetSize = widget_size_with_row_get(3);
shortcut(settings.shortcutSelectAll);
ImGui::BeginDisabled(selection.size() == anm2.content.spritesheets.size());
{
if (ImGui::Button("Select All", rowTwoWidgetSize))
for (auto& id : anm2.content.spritesheets | std::views::keys)
selection.insert(id);
}
ImGui::EndDisabled();
set_item_tooltip_shortcut("Select all spritesheets.", settings.shortcutSelectAll);
ImGui::SameLine();
shortcut(settings.shortcutSelectNone);
ImGui::BeginDisabled(selection.empty());
if (ImGui::Button("Select None", rowTwoWidgetSize)) selection.clear();
set_item_tooltip_shortcut("Unselect all spritesheets.", settings.shortcutSelectNone);
ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginDisabled(selection.empty()); ImGui::BeginDisabled(selection.empty());

View File

@@ -1,13 +1,17 @@
#include "timeline.h" #include "timeline.h"
#include <algorithm>
#include <ranges> #include <ranges>
#include <imgui_internal.h> #include <imgui_internal.h>
#include "toast.h" #include "toast.h"
#include "vector_.h"
using namespace anm2ed::resource; using namespace anm2ed::resource;
using namespace anm2ed::types; using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace glm; using namespace glm;
namespace anm2ed::imgui namespace anm2ed::imgui
@@ -15,13 +19,18 @@ namespace anm2ed::imgui
constexpr auto COLOR_HIDDEN_MULTIPLIER = vec4(0.5f, 0.5f, 0.5f, 1.000f); constexpr auto COLOR_HIDDEN_MULTIPLIER = vec4(0.5f, 0.5f, 0.5f, 1.000f);
constexpr auto FRAME_TIMELINE_COLOR = ImVec4(0.106f, 0.184f, 0.278f, 1.000f); constexpr auto FRAME_TIMELINE_COLOR = ImVec4(0.106f, 0.184f, 0.278f, 1.000f);
constexpr auto FRAME_BORDER_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.15f); constexpr auto FRAME_BORDER_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
constexpr auto FRAME_BORDER_COLOR_REFERENCED = ImVec4(1.0f, 1.0f, 1.0f, 0.50f);
constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.05f); constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.05f);
constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f; constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f;
constexpr auto FRAME_BORDER_THICKNESS = 2.5f;
constexpr auto FRAME_BORDER_THICKNESS_REFERENCED = 5.0f;
constexpr auto TEXT_MULTIPLE_COLOR = to_imvec4(color::WHITE); constexpr auto TEXT_MULTIPLE_COLOR = to_imvec4(color::WHITE);
constexpr auto PLAYHEAD_LINE_COLOR = to_imvec4(color::WHITE); constexpr auto PLAYHEAD_LINE_COLOR = to_imvec4(color::WHITE);
constexpr auto FRAME_MULTIPLE = 5; constexpr auto FRAME_MULTIPLE = 5;
constexpr auto FRAME_DRAG_PAYLOAD_ID = "Frame Drag Drop";
constexpr auto HELP_FORMAT = R"(- Press {} to decrement time. constexpr auto HELP_FORMAT = R"(- Press {} to decrement time.
- Press {} to increment time. - Press {} to increment time.
@@ -32,50 +41,69 @@ namespace anm2ed::imgui
void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{ {
auto& document = *manager.get(); auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& playback = document.playback; auto& playback = document.playback;
auto& reference = document.reference; auto& reference = document.reference;
auto& frames = document.frames;
auto animation = document.animation_get(); auto animation = document.animation_get();
style = ImGui::GetStyle(); style = ImGui::GetStyle();
auto frames_delete = [&]()
{
if (auto item = animation->item_get(reference.itemType, reference.itemID); item)
{
for (auto& i : frames.selection | std::views::reverse)
item->frames.erase(item->frames.begin() + i);
reference.frameIndex = -1;
frames.clear();
}
};
auto context_menu = [&]() auto context_menu = [&]()
{ {
auto& hoveredFrame = document.hoveredFrame;
auto& anm2 = document.anm2;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
auto copy = [&]() auto copy = [&]()
{ {
if (auto frame = anm2.frame_get(hoveredFrame)) clipboard.set(frame->to_string(hoveredFrame.itemType)); if (auto item = animation->item_get(reference.itemType, reference.itemID); item)
{
std::string clipboardString{};
for (auto& i : frames.selection)
{
if (!vector::in_bounds(item->frames, i)) break;
clipboardString += item->frames[i].to_string(reference.itemType);
}
clipboard.set(clipboardString);
}
}; };
auto cut = [&]() auto cut = [&]()
{ {
copy(); copy();
auto frames_delete = [&]()
{
if (auto item = anm2.item_get(reference); item)
{
item->frames.erase(item->frames.begin() + reference.frameIndex);
reference.frameIndex = glm::max(-1, --reference.frameIndex);
}
};
DOCUMENT_EDIT(document, "Cut Frame(s)", Document::FRAMES, frames_delete()); DOCUMENT_EDIT(document, "Cut Frame(s)", Document::FRAMES, frames_delete());
}; };
auto paste = [&]() auto paste = [&]()
{ {
if (auto item = document.item_get()) if (auto item = animation->item_get(reference.itemType, reference.itemID))
{ {
document.snapshot("Paste Frame(s)"); document.snapshot("Paste Frame(s)");
std::set<int> indices{}; std::set<int> indices{};
std::string errorString{}; std::string errorString{};
auto start = reference.frameIndex + 1; auto insertIndex = reference.frameIndex == -1 ? item->frames.size() : reference.frameIndex + 1;
auto start = reference.itemType == anm2::TRIGGER ? hoveredTime : insertIndex;
if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString))
{
frames.selection.clear();
for (auto i : indices)
frames.selection.insert(i);
reference.frameIndex = *indices.begin();
animation->fit_length();
document.change(Document::FRAMES); document.change(Document::FRAMES);
}
else else
toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); toasts.error(std::format("Failed to deserialize frame(s): {}", errorString));
} }
@@ -83,14 +111,14 @@ namespace anm2ed::imgui
toasts.error(std::format("Failed to deserialize frame(s): select an item first!")); toasts.error(std::format("Failed to deserialize frame(s): select an item first!"));
}; };
if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut();
if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{ {
if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, hoveredFrame != anm2::Reference{})) cut(); if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, !frames.selection.empty())) cut();
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, hoveredFrame != anm2::Reference{})) copy(); if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !frames.selection.empty())) copy();
if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste(); if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste();
ImGui::EndPopup(); ImGui::EndPopup();
} }
@@ -98,9 +126,19 @@ namespace anm2ed::imgui
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
}; };
auto item_properties_reset = [&]()
{
addItemName.clear();
addItemSpritesheetID = {};
addItemID = -1;
unusedItems = reference.itemType == anm2::LAYER ? anm2.layers_unused(*animation)
: reference.itemType == anm2::NULL_ ? anm2.nulls_unused(*animation)
: std::set<int>{};
};
auto item_child = [&](anm2::Type type, int id, int& index) auto item_child = [&](anm2::Type type, int id, int& index)
{ {
auto& anm2 = document.anm2; ImGui::PushID(index);
auto item = animation ? animation->item_get(type, id) : nullptr; auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false; auto isVisible = item ? item->isVisible : false;
@@ -123,13 +161,24 @@ namespace anm2ed::imgui
auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x,
ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2));
if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders)) if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse))
{ {
auto isReferenced = reference.itemType == type && reference.itemID == id;
auto cursorPos = ImGui::GetCursorPos();
if (type != anm2::NONE) if (type != anm2::NONE)
{ {
anm2::Reference itemReference = {reference.animationIndex, type, id}; ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing)));
if (ImGui::IsWindowHovered()) ImGui::SetNextItemAllowOverlap();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4());
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4());
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4());
if (ImGui::Selectable("##Item Button", false, ImGuiSelectableFlags_None, itemSize))
reference = {reference.animationIndex, type, id};
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{ {
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{ {
@@ -144,16 +193,43 @@ namespace anm2ed::imgui
break; break;
} }
} }
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference;
} }
if (type == anm2::LAYER)
{
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip))
{
ImGui::SetDragDropPayload("Layer Animation Drag Drop", &id, sizeof(int));
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget())
{
if (auto payload = ImGui::AcceptDragDropPayload("Layer Animation Drag Drop"))
{
auto droppedID = *(int*)payload->Data;
auto layer_order_move = [&]()
{
int source = vector::find_index(animation->layerOrder, droppedID);
int destination = vector::find_index(animation->layerOrder, id);
if (source != -1 && destination != -1) vector::move_index(animation->layerOrder, source, destination);
};
DOCUMENT_EDIT(document, "Move Layer Animation", Document::ITEMS, layer_order_move());
}
ImGui::EndDragDropTarget();
}
}
ImGui::SetCursorPos(cursorPos);
ImGui::Image(resources.icons[icon].id, icon_size_get()); ImGui::Image(resources.icons[icon].id, icon_size_get());
ImGui::SameLine(); ImGui::SameLine();
if (isReferenced) ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(label.c_str()); ImGui::TextUnformatted(label.c_str());
if (isReferenced) ImGui::PopFont();
anm2::Item* itemPtr = animation->item_get(type, id);
bool& itemVisible = itemPtr->isVisible;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4());
@@ -163,18 +239,15 @@ namespace anm2ed::imgui
ImGui::SetCursorPos( ImGui::SetCursorPos(
ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
int visibleIcon = itemVisible ? icon::VISIBLE : icon::INVISIBLE; int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE;
if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get()))
DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, itemVisible = !itemVisible); DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, item->isVisible = !item->isVisible);
ImGui::SetItemTooltip(itemVisible ? "The item is shown. Press to hide." ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show.");
: "The item is hidden. Press to show.");
if (type == anm2::NULL_) if (type == anm2::NULL_)
{ {
auto& null = anm2.content.nulls.at(id); auto& null = anm2.content.nulls.at(id);
auto& isShowRect = null.isShowRect; auto& isShowRect = null.isShowRect;
auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT; auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT;
ImGui::SetCursorPos( ImGui::SetCursorPos(
ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x,
@@ -191,17 +264,15 @@ namespace anm2ed::imgui
else else
{ {
auto cursorPos = ImGui::GetCursorPos(); auto cursorPos = ImGui::GetCursorPos();
auto& isShowUnused = settings.timelineIsShowUnused;
ImGui::SetCursorPos(
ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4());
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2());
ImGui::SetCursorPos(
ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
auto& isShowUnused = settings.timelineIsShowUnused;
auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED;
if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, icon_size_get())) if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, icon_size_get()))
isShowUnused = !isShowUnused; isShowUnused = !isShowUnused;
@@ -210,16 +281,13 @@ namespace anm2ed::imgui
auto& showLayersOnly = settings.timelineIsOnlyShowLayers; auto& showLayersOnly = settings.timelineIsOnlyShowLayers;
auto layersIcon = showLayersOnly ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; auto layersIcon = showLayersOnly ? icon::SHOW_LAYERS : icon::HIDE_LAYERS;
ImGui::SetCursorPos( ImGui::SetCursorPos(
ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get())) if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get()))
showLayersOnly = !showLayersOnly; showLayersOnly = !showLayersOnly;
ImGui::SetItemTooltip(showLayersOnly ? "Only layers are visible. Press to show all items." ImGui::SetItemTooltip(showLayersOnly ? "Only layers are visible. Press to show all items."
: "All items are visible. Press to only show layers."); : "All items are visible. Press to only show layers.");
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
@@ -238,6 +306,8 @@ namespace anm2ed::imgui
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
index++; index++;
ImGui::PopID();
}; };
auto items_child = [&]() auto items_child = [&]()
@@ -313,15 +383,19 @@ namespace anm2ed::imgui
ImGui::BeginDisabled(!animation); ImGui::BeginDisabled(!animation);
{ {
shortcut(settings.shortcutAdd); shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Add", widgetSize)) propertiesPopup.open(); if (ImGui::Button("Add", widgetSize))
{
item_properties_reset();
propertiesPopup.open();
}
set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd); set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd);
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER && ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER &&
reference.itemType != anm2::NULL_); reference.itemType != anm2::NULL_);
{ {
shortcut(settings.shortcutRemove); shortcut(manager.chords[SHORTCUT_REMOVE]);
if (ImGui::Button("Remove", widgetSize)) if (ImGui::Button("Remove", widgetSize))
{ {
auto remove = [&]() auto remove = [&]()
@@ -345,9 +419,6 @@ namespace anm2ed::imgui
auto frame_child = [&](anm2::Type type, int id, int& index, float width) auto frame_child = [&](anm2::Type type, int id, int& index, float width)
{ {
auto& anm2 = document.anm2;
auto& hoveredFrame = document.hoveredFrame;
auto item = animation ? animation->item_get(type, id) : nullptr; auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false; auto isVisible = item ? item->isVisible : false;
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
@@ -358,7 +429,7 @@ namespace anm2ed::imgui
auto colorHovered = to_imvec4(anm2::TYPE_COLOR_HOVERED[type]); auto colorHovered = to_imvec4(anm2::TYPE_COLOR_HOVERED[type]);
auto colorHidden = to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER); auto colorHidden = to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER);
auto colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER); auto colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER);
auto colorHoveredHidden = to_imvec4(to_vec4(colorHidden) * COLOR_HIDDEN_MULTIPLIER); auto colorHoveredHidden = to_imvec4(to_vec4(colorHovered) * COLOR_HIDDEN_MULTIPLIER);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
@@ -373,6 +444,8 @@ namespace anm2ed::imgui
if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders))
{ {
auto drawList = ImGui::GetWindowDrawList();
auto clipMax = drawList->GetClipRectMax();
auto length = animation ? animation->frameNum : anm2.animations.length(); auto length = animation ? animation->frameNum : anm2.animations.length();
auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y); auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y);
auto framesSize = ImVec2(frameSize.x * length, frameSize.y); auto framesSize = ImVec2(frameSize.x * length, frameSize.y);
@@ -380,11 +453,8 @@ namespace anm2ed::imgui
auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto cursorScreenPos = ImGui::GetCursorScreenPos();
auto border = ImGui::GetStyle().FrameBorderSize; auto border = ImGui::GetStyle().FrameBorderSize;
auto borderLineLength = frameSize.y / 5; auto borderLineLength = frameSize.y / 5;
auto scrollX = ImGui::GetScrollX(); auto frameMin = std::max(0, (int)std::floor(scroll.x / frameSize.x) - 1);
auto available = ImGui::GetContentRegionAvail(); auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil((scroll.x + clipMax.x) / frameSize.x) + 1);
auto frameMin = std::max(0, (int)std::floor(scrollX / frameSize.x) - 1);
auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil(scrollX + available.x / frameSize.x) + 1);
auto drawList = ImGui::GetWindowDrawList();
pickerLineDrawList = drawList; pickerLineDrawList = drawList;
if (type == anm2::NONE) if (type == anm2::NONE)
@@ -422,16 +492,18 @@ namespace anm2ed::imgui
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseDown(0)) if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseDown(0))
isDragging = true; isDragging = true;
if (isDragging)
{
auto childPos = ImGui::GetWindowPos(); auto childPos = ImGui::GetWindowPos();
auto mousePos = ImGui::GetIO().MousePos; auto mousePos = ImGui::GetIO().MousePos;
auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y); auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y);
playback.time = floorf(localMousePos.x / frameSize.x); hoveredTime = floorf(localMousePos.x / frameSize.x);
reference.frameTime = playback.time;
if (isDragging)
{
playback.time = hoveredTime;
document.frameTime = playback.time;
} }
playback.clamp(settings.playbackIsClampPlayhead ? length : anm2::FRAME_NUM_MAX); playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX);
if (ImGui::IsMouseReleased(0)) isDragging = false; if (ImGui::IsMouseReleased(0)) isDragging = false;
@@ -440,47 +512,220 @@ namespace anm2ed::imgui
} }
else if (animation) else if (animation)
{ {
anm2::Reference itemReference{reference.animationIndex, type, id};
ImGui::PushStyleColor(ImGuiCol_ButtonActive, isVisible ? colorActive : colorActiveHidden);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isVisible ? colorHovered : colorHoveredHidden);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
float frameTime{}; float frameTime{};
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
frames.clear();
reference = {reference.animationIndex, type, id};
}
for (int i = frameMin; i < frameMax; i++)
{
auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y);
auto frameRectMax = ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y);
drawList->AddRect(frameScreenPos, frameRectMax, ImGui::GetColorU32(FRAME_BORDER_COLOR));
if (i % FRAME_MULTIPLE == 0)
drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(FRAME_MULTIPLE_OVERLAY_COLOR));
}
frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape);
for (auto [i, frame] : std::views::enumerate(item->frames)) for (auto [i, frame] : std::views::enumerate(item->frames))
{ {
ImGui::PushID(i); ImGui::PushID(i);
auto frameReference = auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)i};
anm2::Reference(itemReference.animationIndex, itemReference.itemType, itemReference.itemID, i); auto isFrameVisible = isVisible && frame.isVisible;
auto isSelected = reference == frameReference; auto isReferenced = reference == frameReference;
vec2 frameMin = {frameTime * frameSize.x, cursorPos.y}; auto isSelected =
vec2 frameMax = {frameMin.x + frame.delay * frameSize.x, frameMin.y + frameSize.y}; (frames.selection.contains(i) && reference.itemType == type && reference.itemID == id) || isReferenced;
auto buttonSize = to_imvec2(frameMax - frameMin);
auto buttonPos = ImVec2(cursorPos.x + frameMin.x, cursorPos.y); if (type == anm2::TRIGGER) frameTime = frame.atFrame;
auto buttonSize =
type == anm2::TRIGGER ? frameSize : to_imvec2(vec2(frameSize.x * frame.duration, frameSize.y));
auto buttonPos = ImVec2(cursorPos.x + (frameTime * frameSize.x), cursorPos.y);
ImGui::SetCursorPos(buttonPos); ImGui::SetCursorPos(buttonPos);
ImGui::PushStyleColor(ImGuiCol_Button, isSelected && isVisible ? colorActive ImGui::PushStyleColor(ImGuiCol_Header, isFrameVisible && isSelected ? colorActive
: isSelected && !isVisible ? colorActiveHidden : isSelected ? colorActiveHidden
: isVisible ? color : isFrameVisible ? color
: colorHidden); : colorHidden);
if (ImGui::Button("##Frame Button", buttonSize)) ImGui::PushStyleColor(ImGuiCol_HeaderActive, isFrameVisible ? colorActive : colorActiveHidden);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, isFrameVisible ? colorHovered : colorHoveredHidden);
ImGui::SetNextItemAllowOverlap();
ImGui::SetNextItemSelectionUserData((int)i);
if (ImGui::Selectable("##Frame Button", true, ImGuiSelectableFlags_None, buttonSize))
{ {
if (type == anm2::LAYER) if (type == anm2::LAYER)
{ {
document.spritesheet.reference = anm2.content.layers[id].spritesheetID; document.spritesheet.reference = anm2.content.layers[id].spritesheetID;
document.layer.selection = {id}; document.layer.selection = {id};
} }
reference = frameReference; else if (type == anm2::NULL_)
reference.frameTime = frameTime; document.null.selection = {id};
if (type != anm2::TRIGGER)
{
if (ImGui::IsKeyDown(ImGuiMod_Alt)) if (ImGui::IsKeyDown(ImGuiMod_Alt))
DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES,
frame.isInterpolated = !frame.isInterpolated); frame.isInterpolated = !frame.isInterpolated);
document.frameTime = frameTime;
} }
if (ImGui::IsItemHovered()) hoveredFrame = frameReference;
ImGui::PopStyleColor(); reference = frameReference;
}
if (type == anm2::TRIGGER && ImGui::IsItemHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
!draggedTrigger)
{
draggedTrigger = &frame;
draggedTriggerIndex = (int)i;
draggedTriggerAtFrameStart = hoveredTime;
}
ImGui::PopStyleColor(3);
if (type != anm2::TRIGGER)
{
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip))
{
frameDragDrop = {};
frameDragDrop.type = type;
frameDragDrop.itemID = id;
frameDragDrop.animationIndex = reference.animationIndex;
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 (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) &&
frameSelectionSnapshotReference.animationIndex == reference.animationIndex &&
frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id &&
contains_index(frameSelectionSnapshot, (int)i))
{
frameDragDrop.selection = 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);
}
std::sort(frameDragDrop.selection.begin(), frameDragDrop.selection.end());
frameDragDrop.selection.erase(
std::unique(frameDragDrop.selection.begin(), frameDragDrop.selection.end()),
frameDragDrop.selection.end());
ImGui::SetDragDropPayload(FRAME_DRAG_PAYLOAD_ID, &frameDragDrop, sizeof(FrameDragDrop));
ImGui::EndDragDropSource();
}
if (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, "Move Frame(s)", 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);
int desired = std::clamp((int)i + 1, 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, type, id, insertPosResult};
document.frameTime = time_from_index(targetItem, reference.frameIndex);
if (type == anm2::LAYER)
{
document.spritesheet.reference = anm2.content.layers[id].spritesheetID;
document.layer.selection = {id};
}
else if (type == anm2::NULL_)
document.null.selection = {id};
}
}
}
ImGui::EndDragDropTarget();
}
}
auto rectMin = ImGui::GetItemRectMin();
auto rectMax = ImGui::GetItemRectMax();
auto borderColor = isReferenced ? FRAME_BORDER_COLOR_REFERENCED : FRAME_BORDER_COLOR;
auto borderThickness = isReferenced ? FRAME_BORDER_THICKNESS_REFERENCED : FRAME_BORDER_THICKNESS;
drawList->AddRect(rectMin, rectMax, ImGui::GetColorU32(borderColor), ImGui::GetStyle().FrameRounding, 0,
borderThickness);
auto icon = type == anm2::TRIGGER ? icon::TRIGGER auto icon = type == anm2::TRIGGER ? icon::TRIGGER
: frame.isInterpolated ? icon::INTERPOLATED : frame.isInterpolated ? icon::INTERPOLATED
: icon::UNINTERPOLATED; : icon::UNINTERPOLATED;
@@ -489,15 +734,59 @@ namespace anm2ed::imgui
ImGui::SetCursorPos(iconPos); ImGui::SetCursorPos(iconPos);
ImGui::Image(resources.icons[icon].id, icon_size_get()); ImGui::Image(resources.icons[icon].id, icon_size_get());
frameTime += frame.delay; if (type != anm2::TRIGGER) frameTime += frame.duration;
ImGui::PopID(); ImGui::PopID();
} }
ImGui::PopStyleVar(); frames.selection.finish();
ImGui::PopStyleColor(2); if (isFrameSelectionLocked)
{
frames.selection.clear();
for (int idx : frameSelectionLocked)
frames.selection.insert(idx);
isFrameSelectionLocked = false;
frameSelectionLocked.clear();
}
if (reference.itemType == type && reference.itemID == id)
{
frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end());
frameSelectionSnapshotReference = reference;
}
if (draggedTrigger)
{
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (!isDraggedTriggerSnapshot && hoveredTime != draggedTriggerAtFrameStart)
{
isDraggedTriggerSnapshot = true;
document.snapshot("Trigger At Frame");
}
draggedTrigger->atFrame = glm::clamp(
hoveredTime, 0, settings.playbackIsClamp ? animation->frameNum - 1 : anm2::FRAME_NUM_MAX - 1);
for (auto&& [i, trigger] : std::views::enumerate(animation->triggers.frames))
{
if (i == draggedTriggerIndex) continue;
if (trigger.atFrame == draggedTrigger->atFrame) draggedTrigger->atFrame--;
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
document.change(Document::FRAMES);
draggedTrigger = nullptr;
draggedTriggerIndex = -1;
draggedTriggerAtFrameStart = -1;
isDraggedTriggerSnapshot = false;
item->frames_sort_by_at_frame();
} }
} }
}
}
context_menu();
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -507,8 +796,6 @@ namespace anm2ed::imgui
auto frames_child = [&]() auto frames_child = [&]()
{ {
auto& anm2 = document.anm2;
auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15; auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15;
auto cursorPos = ImGui::GetCursorPos(); auto cursorPos = ImGui::GetCursorPos();
@@ -577,16 +864,16 @@ namespace anm2ed::imgui
for (auto& id : animation->layerOrder) for (auto& id : animation->layerOrder)
{ {
if (auto itemPtr = animation->item_get(anm2::LAYER, id); itemPtr) if (auto item = animation->item_get(anm2::LAYER, id); item)
if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue; if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
frames_child_row(anm2::LAYER, id); frames_child_row(anm2::LAYER, id);
} }
for (auto& id : animation->nullAnimations | std::views::keys) for (auto& id : animation->nullAnimations | std::views::keys)
{ {
if (auto itemPtr = animation->item_get(anm2::NULL_, id); itemPtr) if (auto item = animation->item_get(anm2::NULL_, id); item)
if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue; if (!settings.timelineIsShowUnused && item->frames.empty()) continue;
frames_child_row(anm2::NULL_, id); frames_child_row(anm2::NULL_, id);
} }
@@ -615,8 +902,6 @@ namespace anm2ed::imgui
pickerLineDrawList->PopClipRect(); pickerLineDrawList->PopClipRect();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
context_menu();
} }
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -633,33 +918,55 @@ namespace anm2ed::imgui
auto label = playback.isPlaying ? "Pause" : "Play"; auto label = playback.isPlaying ? "Pause" : "Play";
auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation."; auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation.";
shortcut(settings.shortcutPlayPause); shortcut(manager.chords[SHORTCUT_PLAY_PAUSE]);
if (ImGui::Button(label, widgetSize)) playback.toggle(); if (ImGui::Button(label, widgetSize)) playback.toggle();
set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause);
ImGui::SameLine(); ImGui::SameLine();
auto itemPtr = document.item_get(); auto item = animation->item_get(reference.itemType, reference.itemID);
ImGui::BeginDisabled(!itemPtr); ImGui::BeginDisabled(!item);
{ {
shortcut(settings.shortcutAdd); shortcut(manager.chords[SHORTCUT_ADD]);
if (ImGui::Button("Insert Frame", widgetSize)) if (ImGui::Button("Insert", widgetSize))
{ {
auto insert_frame = [&]() auto insert_frame = [&]()
{
if (reference.itemType == anm2::TRIGGER)
{
for (auto& trigger : animation->triggers.frames)
if (document.frameTime == trigger.atFrame) return;
auto addFrame = anm2::Frame();
addFrame.atFrame = document.frameTime;
item->frames.push_back(addFrame);
item->frames_sort_by_at_frame();
reference.frameIndex = item->frame_index_from_at_frame_get(addFrame.atFrame);
}
else
{ {
auto frame = document.frame_get(); auto frame = document.frame_get();
if (frame) if (frame)
{ {
itemPtr->frames.insert(itemPtr->frames.begin() + reference.frameIndex, *frame); auto addFrame = *frame;
item->frames.insert(item->frames.begin() + reference.frameIndex, addFrame);
reference.frameIndex++; reference.frameIndex++;
} }
else if (!itemPtr->frames.empty()) else if (!item->frames.empty())
{ {
auto lastFrame = itemPtr->frames.back(); auto addFrame = item->frames.back();
itemPtr->frames.emplace_back(lastFrame); item->frames.emplace_back(addFrame);
reference.frameIndex = static_cast<int>(itemPtr->frames.size()) - 1; reference.frameIndex = (int)(item->frames.size()) - 1;
} }
else
{
item->frames.emplace_back(anm2::Frame());
reference.frameIndex = 0;
}
}
frames.selection = {reference.frameIndex};
}; };
DOCUMENT_EDIT(document, "Insert Frame", Document::FRAMES, insert_frame()); DOCUMENT_EDIT(document, "Insert Frame", Document::FRAMES, insert_frame());
@@ -670,17 +977,9 @@ namespace anm2ed::imgui
ImGui::BeginDisabled(!document.frame_get()); ImGui::BeginDisabled(!document.frame_get());
{ {
shortcut(settings.shortcutRemove); shortcut(manager.chords[SHORTCUT_REMOVE]);
if (ImGui::Button("Delete Frame", widgetSize)) if (ImGui::Button("Delete", widgetSize))
{ DOCUMENT_EDIT(document, "Delete Frame(s)", Document::FRAMES, frames_delete());
auto delete_frame = [&]()
{
itemPtr->frames.erase(itemPtr->frames.begin() + reference.frameIndex);
reference.frameIndex = glm::max(-1, --reference.frameIndex);
};
DOCUMENT_EDIT(document, "Delete Frame(s)", Document::FRAMES, delete_frame());
}
set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove); set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove);
ImGui::SameLine(); ImGui::SameLine();
@@ -694,36 +993,44 @@ namespace anm2ed::imgui
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Fit Animation Length", widgetSize)) animation->frameNum = animation->length(); if (ImGui::Button("Fit Animation Length", widgetSize))
DOCUMENT_EDIT(document, "Fit Animation Length", Document::ANIMATIONS,
animation->frameNum = animation->length());
ImGui::SetItemTooltip("The animation length will be set to the effective length of the animation."); ImGui::SetItemTooltip("The animation length will be set to the effective length of the animation.");
ImGui::SameLine(); ImGui::SameLine();
auto frameNum = animation ? animation->frameNum : dummy_value<int>();
ImGui::SetNextItemWidth(widgetSize.x); ImGui::SetNextItemWidth(widgetSize.x);
ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value<int>(), STEP, STEP_FAST, if (input_int_range("Animation Length", frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX, STEP, STEP_FAST,
!animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0))
if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX); DOCUMENT_EDIT(document, "Animation Length", Document::ANIMATIONS, animation->frameNum = frameNum);
ImGui::SetItemTooltip("Set the animation's length."); ImGui::SetItemTooltip("Set the animation's length.");
ImGui::SameLine(); ImGui::SameLine();
auto isLoop = animation ? animation->isLoop : dummy_value<bool>();
ImGui::SetNextItemWidth(widgetSize.x); ImGui::SetNextItemWidth(widgetSize.x);
ImGui::Checkbox("Loop", animation ? &animation->isLoop : &dummy_value<bool>()); if (ImGui::Checkbox("Loop", &isLoop))
DOCUMENT_EDIT(document, "Loop", Document::ANIMATIONS, animation->isLoop = isLoop);
ImGui::SetItemTooltip("Toggle the animation looping."); ImGui::SetItemTooltip("Toggle the animation looping.");
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
auto fps = anm2.info.fps;
ImGui::SetNextItemWidth(widgetSize.x); ImGui::SetNextItemWidth(widgetSize.x);
ImGui::InputInt("FPS", &anm2.info.fps, 1, 5); if (input_int_range("FPS", fps, anm2::FPS_MIN, anm2::FPS_MAX))
anm2.info.fps = clamp(anm2.info.fps, anm2::FPS_MIN, anm2::FPS_MAX); DOCUMENT_EDIT(document, "FPS", Document::ANIMATIONS, anm2.info.fps = fps);
ImGui::SetItemTooltip("Set the FPS of all animations."); ImGui::SetItemTooltip("Set the FPS of all animations.");
ImGui::SameLine(); ImGui::SameLine();
auto createdBy = anm2.info.createdBy;
ImGui::SetNextItemWidth(widgetSize.x); ImGui::SetNextItemWidth(widgetSize.x);
input_text_string("Author", &anm2.info.createdBy); if (input_text_string("Author", &createdBy))
DOCUMENT_EDIT(document, "FPS", Document::ANIMATIONS, anm2.info.createdBy = createdBy);
ImGui::SetItemTooltip("Set the author of the document."); ImGui::SetItemTooltip("Set the author of the document.");
ImGui::SameLine(); ImGui::SameLine();
@@ -739,17 +1046,15 @@ namespace anm2ed::imgui
ImGui::SetCursorPos(cursorPos); ImGui::SetCursorPos(cursorPos);
}; };
auto popups_fn = [&]() ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::Begin("Timeline", &settings.windowIsTimeline))
{ {
auto item_properties_reset = [&]() isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
{ frames_child();
addItemName.clear(); items_child();
addItemSpritesheetID = {}; }
addItemID = -1; ImGui::PopStyleVar();
isUnusedItemsSet = false; ImGui::End();
};
auto& anm2 = document.anm2;
propertiesPopup.trigger(); propertiesPopup.trigger();
@@ -765,15 +1070,6 @@ namespace anm2ed::imgui
auto& locale = settings.timelineAddItemLocale; auto& locale = settings.timelineAddItemLocale;
auto& source = settings.timelineAddItemSource; auto& source = settings.timelineAddItemSource;
if (!isUnusedItemsSet)
{
unusedItems = type == anm2::LAYER ? anm2.layers_unused(reference)
: type == anm2::NULL_ ? anm2.nulls_unused(reference)
: std::set<int>{};
isUnusedItemsSet = true;
}
auto footerSize = footer_size_get(); auto footerSize = footer_size_get();
auto optionsSize = child_size_get(11); auto optionsSize = child_size_get(11);
auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y -
@@ -787,8 +1083,7 @@ namespace anm2ed::imgui
if (ImGui::BeginChild("Type Layer", size)) if (ImGui::BeginChild("Type Layer", size))
{ {
ImGui::RadioButton("Layer", &type, anm2::LAYER); ImGui::RadioButton("Layer", &type, anm2::LAYER);
ImGui::SetItemTooltip( ImGui::SetItemTooltip("Layers are a basic visual element in an animation, used for displaying spritesheets.");
"Layers are a basic visual element in an animation, used for displaying spritesheets.");
} }
ImGui::EndChild(); ImGui::EndChild();
@@ -930,8 +1225,9 @@ namespace anm2ed::imgui
auto frame = document.frame_get(); auto frame = document.frame_get();
input_int_range("Interval", interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); input_int_range("Interval", interval, anm2::FRAME_DURATION_MIN,
ImGui::SetItemTooltip("Set the maximum delay of each frame that will be baked."); frame ? frame->duration : anm2::FRAME_DURATION_MIN);
ImGui::SetItemTooltip("Set the maximum duration of each frame that will be baked.");
ImGui::Checkbox("Round Rotation", &isRoundRotation); ImGui::Checkbox("Round Rotation", &isRoundRotation);
ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number."); ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number.");
@@ -957,39 +1253,26 @@ namespace anm2ed::imgui
ImGui::EndPopup(); ImGui::EndPopup();
} }
};
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle();
if (ImGui::Begin("Timeline", &settings.windowIsTimeline))
{
isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
frames_child();
items_child();
}
ImGui::PopStyleVar();
ImGui::End();
popups_fn();
if (shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle();
if (animation) if (animation)
{ {
if (chord_repeating(string_to_chord(settings.shortcutPreviousFrame))) if (chord_repeating(manager.chords[SHORTCUT_PREVIOUS_FRAME]))
{ {
playback.decrement(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); playback.decrement(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX);
reference.frameTime = playback.time; document.frameTime = playback.time;
} }
if (chord_repeating(string_to_chord(settings.shortcutNextFrame))) if (chord_repeating(manager.chords[SHORTCUT_NEXT_FRAME]))
{ {
playback.increment(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); playback.increment(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX);
reference.frameTime = playback.time; document.frameTime = playback.time;
} }
} }
if (ImGui::IsKeyChordPressed(string_to_chord(settings.shortcutShortenFrame))) document.snapshot("Shorten Frame"); if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_SHORTEN_FRAME])) document.snapshot("Shorten Frame");
if (chord_repeating(string_to_chord(settings.shortcutShortenFrame))) if (chord_repeating(manager.chords[SHORTCUT_SHORTEN_FRAME]))
{ {
if (auto frame = document.frame_get()) if (auto frame = document.frame_get())
{ {
@@ -998,8 +1281,8 @@ namespace anm2ed::imgui
} }
} }
if (ImGui::IsKeyChordPressed(string_to_chord(settings.shortcutExtendFrame))) document.snapshot("Extend Frame"); if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_EXTEND_FRAME])) document.snapshot("Extend Frame");
if (chord_repeating(string_to_chord(settings.shortcutExtendFrame))) if (chord_repeating(manager.chords[SHORTCUT_EXTEND_FRAME]))
{ {
if (auto frame = document.frame_get()) if (auto frame = document.frame_get())
{ {

View File

@@ -1,5 +1,8 @@
#pragma once #pragma once
#include <set>
#include <vector>
#include "clipboard.h" #include "clipboard.h"
#include "manager.h" #include "manager.h"
#include "resources.h" #include "resources.h"
@@ -7,6 +10,14 @@
namespace anm2ed::imgui namespace anm2ed::imgui
{ {
struct FrameDragDrop
{
anm2::Type type{anm2::NONE};
int itemID{-1};
int animationIndex{-1};
std::vector<int> selection{};
};
class Timeline class Timeline
{ {
bool isDragging{}; bool isDragging{};
@@ -18,7 +29,16 @@ namespace anm2ed::imgui
bool addItemIsRect{}; bool addItemIsRect{};
int addItemID{-1}; int addItemID{-1};
int addItemSpritesheetID{-1}; int addItemSpritesheetID{-1};
bool isUnusedItemsSet{}; int hoveredTime{};
anm2::Frame* draggedTrigger{};
int draggedTriggerIndex{-1};
int draggedTriggerAtFrameStart{-1};
bool isDraggedTriggerSnapshot{};
FrameDragDrop frameDragDrop{};
std::vector<int> frameSelectionSnapshot{};
std::vector<int> frameSelectionLocked{};
bool isFrameSelectionLocked{};
anm2::Reference frameSelectionSnapshotReference{};
std::set<int> unusedItems{}; std::set<int> unusedItems{};
glm::vec2 scroll{}; glm::vec2 scroll{};
ImDrawList* pickerLineDrawList{}; ImDrawList* pickerLineDrawList{};

View File

@@ -50,9 +50,7 @@ namespace anm2ed::imgui
if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
auto member = SHORTCUT_MEMBERS[info.shortcut]; if (shortcut(manager.chords[info.shortcut], shortcut::GLOBAL_SET)) tool_use((tool::Type)i);
if (shortcut(settings.*member, shortcut::GLOBAL_SET)) tool_use((tool::Type)i);
if (i == tool::COLOR) if (i == tool::COLOR)
{ {

View File

@@ -61,10 +61,7 @@ namespace anm2ed
} }
} }
std::string Loader::settings_path() std::string Loader::settings_path() { return filesystem::path_preferences_get() + "settings.ini"; }
{
return filesystem::path_preferences_get() + "settings.ini";
}
Loader::Loader(int argc, const char** argv) Loader::Loader(int argc, const char** argv)
{ {

View File

@@ -7,7 +7,7 @@ int main(int argc, const char** argv)
if (loader.isError) return EXIT_FAILURE; if (loader.isError) return EXIT_FAILURE;
anm2ed::State state(loader.window, loader.arguments); anm2ed::State state(loader.window, loader.settings, loader.arguments);
while (!state.isQuit) while (!state.isQuit)
state.loop(loader.window, loader.settings); state.loop(loader.window, loader.settings);

View File

@@ -281,5 +281,11 @@ namespace anm2ed
autosave_files_write(); autosave_files_write();
} }
void Manager::chords_set(Settings& settings)
{
for (int i = 0; i < SHORTCUT_COUNT; i++)
chords[i] = imgui::string_to_chord(settings.*SHORTCUT_MEMBERS[i]);
}
Manager::~Manager() { autosave_files_clear(); } Manager::~Manager() { autosave_files_clear(); }
} }

View File

@@ -3,6 +3,7 @@
#include <vector> #include <vector>
#include "document.h" #include "document.h"
#include "settings.h"
namespace anm2ed namespace anm2ed
{ {
@@ -27,6 +28,8 @@ namespace anm2ed
int recordingEnd{}; int recordingEnd{};
bool isRecordingRange{}; bool isRecordingRange{};
ImGuiKeyChord chords[SHORTCUT_COUNT]{};
anm2::Layer editLayer{}; anm2::Layer editLayer{};
imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL_NO_HEIGHT)};
@@ -64,6 +67,8 @@ namespace anm2ed
void autosave_files_write(); void autosave_files_write();
void autosave_files_clear(); void autosave_files_clear();
void chords_set(Settings&);
std::filesystem::path autosave_directory_get(); std::filesystem::path autosave_directory_get();
}; };
} }

View File

@@ -11,10 +11,7 @@ namespace anm2ed
isPlaying = !isPlaying; isPlaying = !isPlaying;
} }
void Playback::clamp(int length) void Playback::clamp(int length) { time = glm::clamp(time, 0.0f, (float)length - 1.0f); }
{
time = glm::clamp(time, 0.0f, (float)length - 1.0f);
}
void Playback::tick(int fps, int length, bool isLoop) void Playback::tick(int fps, int length, bool isLoop)
{ {

View File

@@ -1,12 +1,26 @@
#include "audio.h" #include "audio.h"
#include <SDL3/SDL_properties.h>
#include <algorithm>
#include <cmath>
#include <memory>
#include <utility> #include <utility>
#include <vector>
#include <xm.h>
namespace anm2ed::resource namespace anm2ed::resource
{ {
namespace
{
constexpr int XM_SAMPLE_RATE = 44100;
constexpr uint16_t XM_CHUNK_FRAMES = 1024;
constexpr int XM_MAX_SECONDS = 600;
constexpr int XM_CHANNELS = 2;
}
MIX_Mixer* Audio::mixer_get() MIX_Mixer* Audio::mixer_get()
{ {
static MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); static auto mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
return mixer; return mixer;
} }
@@ -15,21 +29,130 @@ namespace anm2ed::resource
if (path && *path) internal = MIX_LoadAudio(mixer_get(), path, true); if (path && *path) internal = MIX_LoadAudio(mixer_get(), path, true);
} }
Audio::Audio(const unsigned char* data, size_t size)
{
if (!data || size == 0) return;
auto is_chunk_silent = [](const float* samples, size_t count)
{
constexpr float epsilon = 1e-6f;
for (size_t i = 0; i < count; ++i)
if (std::fabs(samples[i]) > epsilon) return false;
return true;
};
auto prescanStorage = std::make_unique<unsigned char[]>(XM_PRESCAN_DATA_SIZE);
auto prescan = (xm_prescan_data_t*)prescanStorage.get();
if (!xm_prescan_module((const char*)data, (uint32_t)size, prescan)) return;
auto contextSize = xm_size_for_context(prescan);
auto pool = std::make_unique<char[]>(contextSize);
auto context = xm_create_context(pool.get(), prescan, (const char*)data, (uint32_t)size);
if (!context) return;
xm_set_sample_rate(context, (uint16_t)XM_SAMPLE_RATE);
xm_set_max_loop_count(context, 1);
auto pcm = std::vector<float>{};
pcm.reserve(XM_CHUNK_FRAMES * XM_CHANNELS * 8);
auto framesGenerated = (size_t)0;
const auto maxFrames = (size_t)XM_SAMPLE_RATE * XM_MAX_SECONDS;
auto heardAudio = false;
while (framesGenerated < maxFrames)
{
auto framesThisPass = (uint16_t)std::min<size_t>(XM_CHUNK_FRAMES, maxFrames - framesGenerated);
auto offset = pcm.size();
pcm.resize(offset + framesThisPass * XM_CHANNELS);
auto* chunkStart = pcm.data() + offset;
xm_generate_samples(context, chunkStart, framesThisPass);
framesGenerated += framesThisPass;
auto chunkSamples = (size_t)framesThisPass * XM_CHANNELS;
auto chunkSilent = is_chunk_silent(chunkStart, chunkSamples);
if (!chunkSilent)
{
heardAudio = true;
}
else if (heardAudio)
{
pcm.resize(offset);
break;
}
if (xm_get_loop_count(context) > 0) break;
}
if (pcm.empty()) return;
auto spec = SDL_AudioSpec{};
spec.freq = XM_SAMPLE_RATE;
spec.format = SDL_AUDIO_F32;
spec.channels = XM_CHANNELS;
internal = MIX_LoadRawAudio(nullptr, pcm.data(), pcm.size() * sizeof(float), &spec);
}
void Audio::unload() void Audio::unload()
{ {
if (!internal) return; if (track)
{
MIX_DestroyTrack(track);
track = nullptr;
}
if (internal)
{
MIX_DestroyAudio(internal); MIX_DestroyAudio(internal);
internal = nullptr; internal = nullptr;
} }
void Audio::play(MIX_Mixer* mixer)
{
MIX_PlayAudio(mixer ? mixer : mixer_get(), internal);
} }
void Audio::play(bool loop, MIX_Mixer* mixer)
{
if (!internal) return;
auto targetMixer = mixer ? mixer : mixer_get();
if (!targetMixer) return;
if (track && MIX_GetTrackMixer(track) != targetMixer)
{
MIX_DestroyTrack(track);
track = nullptr;
}
if (!track)
{
track = MIX_CreateTrack(targetMixer);
if (!track) return;
}
MIX_SetTrackAudio(track, internal);
SDL_PropertiesID options = 0;
if (loop)
{
options = SDL_CreateProperties();
if (options) SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
}
MIX_PlayTrack(track, options);
if (options) SDL_DestroyProperties(options);
}
void Audio::stop(MIX_Mixer* mixer)
{
if (!track) return;
if (mixer && MIX_GetTrackMixer(track) != mixer) return;
MIX_StopTrack(track, 0);
}
bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); }
Audio::Audio(Audio&& other) noexcept Audio::Audio(Audio&& other) noexcept
{ {
internal = std::exchange(other.internal, nullptr); internal = std::exchange(other.internal, nullptr);
track = std::exchange(other.track, nullptr);
} }
Audio& Audio::operator=(Audio&& other) noexcept Audio& Audio::operator=(Audio&& other) noexcept
@@ -38,17 +161,11 @@ namespace anm2ed::resource
{ {
unload(); unload();
internal = std::exchange(other.internal, nullptr); internal = std::exchange(other.internal, nullptr);
track = std::exchange(other.track, nullptr);
} }
return *this; return *this;
} }
Audio::~Audio() Audio::~Audio() { unload(); }
{ bool Audio::is_valid() { return internal; }
unload();
}
bool Audio::is_valid()
{
return internal;
}
} }

View File

@@ -1,17 +1,20 @@
#pragma once #pragma once
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <cstddef>
namespace anm2ed::resource namespace anm2ed::resource
{ {
class Audio class Audio
{ {
MIX_Audio* internal{nullptr}; MIX_Audio* internal{nullptr};
MIX_Track* track{nullptr};
MIX_Mixer* mixer_get(); MIX_Mixer* mixer_get();
void unload(); void unload();
public: public:
Audio(const char*); Audio(const char*);
Audio(const unsigned char*, size_t);
~Audio(); ~Audio();
Audio() = default; Audio() = default;
Audio(Audio&&) noexcept; Audio(Audio&&) noexcept;
@@ -20,6 +23,8 @@ namespace anm2ed::resource
Audio& operator=(const Audio&) = delete; Audio& operator=(const Audio&) = delete;
bool is_valid(); bool is_valid();
void play(MIX_Mixer* = nullptr); void play(bool loop = false, MIX_Mixer* = nullptr);
void stop(MIX_Mixer* = nullptr);
bool is_playing() const;
}; };
} }

View File

@@ -212,18 +212,4 @@ namespace anm2ed::resource::icon
SVG_LIST SVG_LIST
#undef X #undef X
}; };
constexpr uint8_t PROGRAM[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x40, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x60, 0xb9, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70,
0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00,
0x8b, 0x49, 0x44, 0x41, 0x54, 0x68, 0xde, 0xed, 0xd7, 0x3d, 0x0e, 0x80, 0x20, 0x0c, 0x86, 0x61, 0x7b, 0x53, 0x3c,
0x99, 0xbd, 0x69, 0x1d, 0x8c, 0x83, 0x4d, 0xd0, 0x44, 0x29, 0x7f, 0xbe, 0x5d, 0x08, 0x30, 0xf4, 0x19, 0x80, 0x2f,
0xc8, 0xd2, 0xb8, 0x04, 0xc0, 0xb4, 0x80, 0x64, 0xc7, 0xb8, 0xf9, 0x86, 0x02, 0x60, 0x36, 0x80, 0xd9, 0x75, 0xbe,
0xba, 0x7d, 0x00, 0xf3, 0x00, 0x72, 0x8d, 0x7c, 0x83, 0x73, 0x5d, 0xa5, 0xf0, 0x43, 0x04, 0xa0, 0x1a, 0x20, 0xaa,
0x11, 0x80, 0xf1, 0x01, 0x1a, 0x14, 0x5b, 0x00, 0xfa, 0x03, 0x24, 0xbb, 0x0f, 0x93, 0xa7, 0xb0, 0xf9, 0x1c, 0x46,
0x00, 0x00, 0x00, 0xe8, 0x06, 0xa0, 0xa5, 0x3f, 0x2a, 0x99, 0x07, 0x0d, 0x40, 0xbf, 0x80, 0xa8, 0x02, 0x30, 0x0e,
0x40, 0xdf, 0x1e, 0xb2, 0x56, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x07, 0x90, 0x86, 0xff, 0x05, 0xd4,
0x2e, 0x00, 0x00, 0x00, 0x34, 0x07, 0xec, 0x94, 0x51, 0xac, 0x41, 0x55, 0x6e, 0xe4, 0x26, 0x00, 0x00, 0x00, 0x00,
0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
} }

View File

@@ -1,5 +1,7 @@
#include "resources.h" #include "resources.h"
#include "xm_music.h"
#include <ranges> #include <ranges>
using namespace anm2ed::resource; using namespace anm2ed::resource;
@@ -16,5 +18,7 @@ namespace anm2ed
for (auto [i, shader] : std::views::enumerate(shader::SHADERS)) for (auto [i, shader] : std::views::enumerate(shader::SHADERS))
shaders[i] = Shader(shader.vertex, shader.fragment); shaders[i] = Shader(shader.vertex, shader.fragment);
music = Audio(xm::ABOUT, std::size(xm::ABOUT));
}; };
} }

View File

@@ -2,6 +2,7 @@
#include <imgui/imgui.h> #include <imgui/imgui.h>
#include "audio.h"
#include "font.h" #include "font.h"
#include "icon.h" #include "icon.h"
#include "shader.h" #include "shader.h"
@@ -15,6 +16,7 @@ namespace anm2ed
resource::Font fonts[resource::font::COUNT]{}; resource::Font fonts[resource::font::COUNT]{};
resource::Texture icons[resource::icon::COUNT]{}; resource::Texture icons[resource::icon::COUNT]{};
resource::Shader shaders[resource::shader::COUNT]{}; resource::Shader shaders[resource::shader::COUNT]{};
resource::Audio music{};
Resources(); Resources();
}; };

View File

@@ -4,7 +4,7 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "anm2/anm2.h" #include "anm2/anm2_type.h"
#include "render.h" #include "render.h"
#include "types.h" #include "types.h"
@@ -54,7 +54,7 @@ namespace anm2ed
X(VIEW_ZOOM_STEP, viewZoomStep, "Zoom Step", FLOAT, 50.0f) \ X(VIEW_ZOOM_STEP, viewZoomStep, "Zoom Step", FLOAT, 50.0f) \
\ \
X(PLAYBACK_IS_LOOP, playbackIsLoop, "Loop", BOOL, true) \ X(PLAYBACK_IS_LOOP, playbackIsLoop, "Loop", BOOL, true) \
X(PLAYBACK_IS_CLAMP_PLAYHEAD, playbackIsClampPlayhead, "Clamp Playhead", BOOL, true) \ X(PLAYBACK_IS_CLAMP, playbackIsClamp, "Clamp", BOOL, true) \
\ \
X(CHANGE_IS_CROP, changeIsCrop, "##Is Crop", BOOL, false) \ X(CHANGE_IS_CROP, changeIsCrop, "##Is Crop", BOOL, false) \
X(CHANGE_IS_SIZE, changeIsSize, "##Is Size", BOOL, false) \ X(CHANGE_IS_SIZE, changeIsSize, "##Is Size", BOOL, false) \
@@ -186,9 +186,7 @@ namespace anm2ed
X(SHORTCUT_DUPLICATE, shortcutDuplicate, "Duplicate", STRING, "Ctrl+J") \ X(SHORTCUT_DUPLICATE, shortcutDuplicate, "Duplicate", STRING, "Ctrl+J") \
X(SHORTCUT_DEFAULT, shortcutDefault, "Default", STRING, "Home") \ X(SHORTCUT_DEFAULT, shortcutDefault, "Default", STRING, "Home") \
X(SHORTCUT_MERGE, shortcutMerge, "Merge", STRING, "Ctrl+E") \ X(SHORTCUT_MERGE, shortcutMerge, "Merge", STRING, "Ctrl+E") \
X(SHORTCUT_PASTE, shortcutPaste, "Paste", STRING, "Ctrl+V") \ X(SHORTCUT_PASTE, shortcutPaste, "Paste", STRING, "Ctrl+V")
X(SHORTCUT_SELECT_ALL, shortcutSelectAll, "Select All", STRING, "Ctrl+A") \
X(SHORTCUT_SELECT_NONE, shortcutSelectNone, "Select None", STRING, "Escape")
#define SETTINGS_WINDOWS \ #define SETTINGS_WINDOWS \
/* Symbol / Name / String / Type / Default */ \ /* Symbol / Name / String / Type / Default */ \
@@ -205,6 +203,14 @@ namespace anm2ed
X(WINDOW_TIMELINE, windowIsTimeline, "Timeline", BOOL, true) \ X(WINDOW_TIMELINE, windowIsTimeline, "Timeline", BOOL, true) \
X(WINDOW_TOOLS, windowIsTools, "Tools", BOOL, true) X(WINDOW_TOOLS, windowIsTools, "Tools", BOOL, true)
enum ShortcutType
{
#define X(symbol, name, string, type, ...) symbol,
SETTINGS_SHORTCUTS
#undef X
SHORTCUT_COUNT
};
class Settings class Settings
{ {
public: public:
@@ -218,14 +224,6 @@ namespace anm2ed
void save(const std::string&, const std::string&); void save(const std::string&, const std::string&);
}; };
enum ShortcutType
{
#define X(symbol, name, string, type, ...) symbol,
SETTINGS_SHORTCUTS
#undef X
SHORTCUT_COUNT
};
constexpr const char* SHORTCUT_STRINGS[] = { constexpr const char* SHORTCUT_STRINGS[] = {
#define X(symbol, name, string, type, ...) string, #define X(symbol, name, string, type, ...) string,
SETTINGS_SHORTCUTS SETTINGS_SHORTCUTS

View File

@@ -17,6 +17,7 @@ namespace anm2ed
public: public:
anm2::Anm2 anm2{}; anm2::Anm2 anm2{};
anm2::Reference reference{}; anm2::Reference reference{};
float frameTime{};
Playback playback{}; Playback playback{};
Storage animation{}; Storage animation{};
Storage merge{}; Storage merge{};
@@ -26,7 +27,7 @@ namespace anm2ed
Storage sound{}; Storage sound{};
Storage spritesheet{}; Storage spritesheet{};
Storage items{}; Storage items{};
std::map<int, Storage> frames{}; Storage frames{};
std::string message = snapshots::ACTION; std::string message = snapshots::ACTION;
}; };

View File

@@ -17,12 +17,14 @@ namespace anm2ed
constexpr auto UPDATE_RATE = 120; constexpr auto UPDATE_RATE = 120;
constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE); constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE);
State::State(SDL_Window*& window, std::vector<std::string>& arguments) State::State(SDL_Window*& window, Settings& settings, std::vector<std::string>& arguments)
{ {
dialog = Dialog(window); dialog = Dialog(window);
for (auto argument : arguments) for (auto argument : arguments)
manager.open(argument); manager.open(argument);
manager.chords_set(settings);
} }
void State::tick(Settings& settings) void State::tick(Settings& settings)

View File

@@ -27,7 +27,7 @@ namespace anm2ed
uint64_t previousTick{}; uint64_t previousTick{};
uint64_t previousUpdate{}; uint64_t previousUpdate{};
State(SDL_Window*&, std::vector<std::string>&); State(SDL_Window*&, Settings& settings, std::vector<std::string>&);
void loop(SDL_Window*&, Settings&); void loop(SDL_Window*&, Settings&);
}; };

View File

@@ -2,6 +2,8 @@
namespace anm2ed namespace anm2ed
{ {
void Storage::clear() { *this = Storage(); }
void Storage::labels_set(std::vector<std::string> labels) void Storage::labels_set(std::vector<std::string> labels)
{ {
labelsString = labels; labelsString = labels;

View File

@@ -1,6 +1,5 @@
#pragma once #pragma once
#include "anm2/anm2.h"
#include "imgui_.h" #include "imgui_.h"
namespace anm2ed namespace anm2ed
@@ -15,14 +14,7 @@ namespace anm2ed
std::vector<const char*> labels{}; std::vector<const char*> labels{};
imgui::MultiSelectStorage selection{}; imgui::MultiSelectStorage selection{};
void clear();
void labels_set(std::vector<std::string>); void labels_set(std::vector<std::string>);
}; };
class FrameStorage
{
public:
anm2::Type referenceType{anm2::NONE};
int referenceID{-1};
int referenceFrameIndex{-1};
};
} }

View File

@@ -70,33 +70,24 @@ namespace anm2ed::types
{ {
constexpr auto ID_NONE = -1; constexpr auto ID_NONE = -1;
constexpr ImVec2 to_imvec2(const glm::vec2& v) noexcept constexpr ImVec2 to_imvec2(const glm::vec2& v) noexcept { return {v.x, v.y}; }
{ constexpr glm::vec2 to_vec2(const ImVec2& v) noexcept { return {v.x, v.y}; }
return {v.x, v.y};
}
constexpr glm::vec2 to_vec2(const ImVec2& v) noexcept
{
return {v.x, v.y};
}
constexpr glm::ivec2 to_ivec2(const ImVec2& v) noexcept constexpr glm::ivec2 to_ivec2(const ImVec2& v) noexcept { return {v.x, v.y}; }
{
return {v.x, v.y};
}
constexpr ImVec4 to_imvec4(const glm::vec4& v) noexcept constexpr ImVec4 to_imvec4(const glm::vec4& v) noexcept { return {v.x, v.y, v.z, v.w}; }
{
return {v.x, v.y, v.z, v.w};
}
constexpr glm::vec4 to_vec4(const ImVec4& v) noexcept constexpr glm::vec4 to_vec4(const ImVec4& v) noexcept { return {v.x, v.y, v.z, v.w}; }
{
return {v.x, v.y, v.z, v.w};
}
template <typename T> constexpr T& dummy_value() template <typename T> constexpr T& dummy_value()
{ {
static T value{}; static T value{};
return value; return value;
} }
template <typename T> constexpr T& dummy_value_negative()
{
static T value{-1};
return value;
}
} }

View File

@@ -8,37 +8,14 @@
namespace anm2ed::util::filesystem namespace anm2ed::util::filesystem
{ {
std::string path_pref_get(const char* org, const char* app) std::string path_preferences_get()
{ {
auto path = SDL_GetPrefPath(org, app); auto path = SDL_GetPrefPath(nullptr, "anm2ed");
std::string string = path; std::string string = path;
SDL_free(path); SDL_free(path);
return string; return string;
} }
std::string path_preferences_get() { return path_pref_get(nullptr, "anm2ed"); }
std::string path_base_get() { return std::string(SDL_GetBasePath()); }
std::string path_executable_get() { return std::filesystem::path(path_base_get()) / "anm2ed"; }
#ifdef __unix__
std::string path_application_get()
{
return std::filesystem::path(path_pref_get(nullptr, "applications")) / "anm2ed.desktop";
}
std::string path_mime_get()
{
return std::filesystem::path(path_pref_get(nullptr, "mime/application")) / "x-anm2+xml.xml";
}
std::string path_icon_get() { return std::filesystem::path(path_preferences_get()) / "anm2ed.png"; }
std::string path_icon_file_get()
{
return std::filesystem::path(path_preferences_get()) / "application-x-anm2+xml.png";
}
#endif
bool path_is_exist(const std::string& path) bool path_is_exist(const std::string& path)
{ {
std::error_code errorCode; std::error_code errorCode;

View File

@@ -5,17 +5,7 @@
namespace anm2ed::util::filesystem namespace anm2ed::util::filesystem
{ {
#ifdef __unix__
std::string path_application_get();
std::string path_mime_get();
std::string path_icon_get();
std::string path_icon_file_get();
#endif
std::string path_pref_get();
std::string path_preferences_get(); std::string path_preferences_get();
std::string path_base_get();
std::string path_executable_get();
bool path_is_exist(const std::string&); bool path_is_exist(const std::string&);
bool path_is_extension(const std::string&, const std::string&); bool path_is_extension(const std::string&, const std::string&);

View File

@@ -12,15 +12,40 @@ namespace anm2ed::util::vector
return index >= 0 && index < (int)v.size() ? &v[index] : nullptr; return index >= 0 && index < (int)v.size() ? &v[index] : nullptr;
} }
template <typename T> bool in_bounds(std::vector<T>& v, int index) template <typename T> int find_index(std::vector<T>& v, T& value)
{ {
return index >= 0 && index < (int)v.size(); auto it = std::find(v.begin(), v.end(), value);
if (it == v.end()) return -1;
return (int)(std::distance(v.begin(), it));
} }
template <typename T> bool in_bounds(std::vector<T>& v, int index) { return index >= 0 && index < (int)v.size(); }
template <typename T> void clamp_in_bounds(std::vector<T>& v, int& index) template <typename T> void clamp_in_bounds(std::vector<T>& v, int& index)
{ {
index = std::clamp(index, 0, (int)v.size() - 1); index = std::clamp(index, 0, (int)v.size() - 1);
} }
template <typename T> int move_index(std::vector<T>& v, int source, int dest)
{
auto size = (int)(v.size());
if (source < 0 || source >= size) return -1;
dest = std::clamp(dest, 0, size - 1);
if (source == dest) return dest;
auto isInsertAfter = source < dest;
T item = std::move(v[source]);
v.erase(v.begin() + source);
if (dest > source) --dest; // destination shifts when removing earlier slot
if (isInsertAfter) ++dest; // drop after original target
dest = std::clamp(dest, 0, (int)(v.size()));
v.insert(v.begin() + dest, std::move(item));
return dest;
}
template <typename T> std::set<int> move_indices(std::vector<T>& v, std::vector<int>& indices, int index) template <typename T> std::set<int> move_indices(std::vector<T>& v, std::vector<int>& indices, int index)
{ {
if (indices.empty()) return {}; if (indices.empty()) return {};
@@ -60,5 +85,4 @@ namespace anm2ed::util::vector
return moveIndices; return moveIndices;
} }
} }