Holy vibecode Batman!
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
You are a senior level professional software developer, etc.
|
||||
|
||||
Use X Macros liberally and lookup tables (maps, etc.) for querying data, etc. Avoid functions for this purpose (i.e., don't map enums to strings with a function). Define X Macros in .cpp files if possible, headers if not.
|
||||
|
||||
Try to use structs/defines for data in general.
|
||||
|
||||
Prefer enums instead of bools for functions in cases where it makes sense (i.e., instead of true/false, have descriptive enums).
|
||||
|
||||
If user asks for "how to", or "why", that's an indicator to generate no code and simply answer their prompt.
|
||||
|
||||
Avoid hardcoding specific solutions and always see if there's ways to optimize/crunch down existing code to support whatever solution user is asking for. Your goal is the least lines/files for the same functionality, while maintaing most clarity/simplicy. Do not use comments.
|
||||
|
||||
Avoid use of anonymous namespaces. For namepsaces, try to have them match files; don't create superfluous namespaces within documents.
|
||||
|
||||
If an if statement has a single line inside, omit the braces.
|
||||
|
||||
Functions should use snake case and should typically be written like "[noun]_[verb]". Try and use a limited amount of verbs to describe functionality; "save", "load", etc.
|
||||
|
||||
For functions that check if true/false, write them like a question, starting with "is", i.e., "is_[noun]_[verb]".
|
||||
|
||||
|
||||
ImGui hidden labels, etc. that aren't user-facing should be typically like "##Name Here".
|
||||
|
||||
If a change is ordered that would impede past API, make it happen regardless without consideration of backwards compatibility.
|
||||
|
||||
With bools, ALWAYS name them "is[VariableName]", even if the variable would be gramatically incorrect.
|
||||
|
||||
When compiling/building, use as many threads as possible.
|
||||
+2
-2
@@ -88,8 +88,6 @@ set(TINYXML2_SRC external/tinyxml2/tinyxml2.cpp)
|
||||
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
|
||||
src/anm2/*.cpp
|
||||
src/anm2/*.hpp
|
||||
src/anm2_new/*.cpp
|
||||
src/anm2_new/*.hpp
|
||||
src/resource/*.cpp
|
||||
src/resource/*.hpp
|
||||
src/imgui/*.cpp
|
||||
@@ -100,6 +98,8 @@ file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
|
||||
src/imgui/popup/*.hpp
|
||||
src/imgui/wizard/*.cpp
|
||||
src/imgui/wizard/*.hpp
|
||||
src/util/imgui/*.cpp
|
||||
src/util/imgui/*.hpp
|
||||
src/util/*.cpp
|
||||
src/util/*.hpp
|
||||
src/window/*.cpp
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
#include "animation.hpp"
|
||||
|
||||
#include "map_.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "unordered_map_.hpp"
|
||||
#include "xml_.hpp"
|
||||
#include <ranges>
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Animation::Animation(XMLElement* element)
|
||||
{
|
||||
int id{};
|
||||
|
||||
xml::query_string_attribute(element, "Name", &name);
|
||||
element->QueryIntAttribute("FrameNum", &frameNum);
|
||||
element->QueryBoolAttribute("Loop", &isLoop);
|
||||
|
||||
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
|
||||
rootAnimation = Item(rootAnimationElement, ROOT);
|
||||
|
||||
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
|
||||
{
|
||||
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
|
||||
child = child->NextSiblingElement("LayerAnimation"))
|
||||
{
|
||||
layerAnimations.emplace(id, Item(child, LAYER, &id));
|
||||
layerOrder.emplace_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
|
||||
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
|
||||
child = child->NextSiblingElement("NullAnimation"))
|
||||
nullAnimations.emplace(id, Item(child, NULL_, &id));
|
||||
|
||||
if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER);
|
||||
}
|
||||
|
||||
Item* Animation::item_get(Type type, int id)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ROOT:
|
||||
return &rootAnimation;
|
||||
case LAYER:
|
||||
return unordered_map::find(layerAnimations, id);
|
||||
case NULL_:
|
||||
return map::find(nullAnimations, id);
|
||||
case TRIGGER:
|
||||
return &triggers;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Animation::item_remove(Type type, int id)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LAYER:
|
||||
layerAnimations.erase(id);
|
||||
for (auto [i, value] : std::views::enumerate(layerOrder))
|
||||
if (value == id) layerOrder.erase(layerOrder.begin() + i);
|
||||
break;
|
||||
case NULL_:
|
||||
nullAnimations.erase(id);
|
||||
break;
|
||||
case ROOT:
|
||||
case TRIGGER:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XMLElement* Animation::to_element(XMLDocument& document, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Animation");
|
||||
element->SetAttribute("Name", name.c_str());
|
||||
element->SetAttribute("FrameNum", frameNum);
|
||||
element->SetAttribute("Loop", isLoop);
|
||||
|
||||
rootAnimation.serialize(document, element, ROOT, -1, flags);
|
||||
|
||||
auto layerAnimationsElement = document.NewElement("LayerAnimations");
|
||||
for (auto& i : layerOrder)
|
||||
{
|
||||
Item& layerAnimation = layerAnimations.at(i);
|
||||
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i, flags);
|
||||
}
|
||||
element->InsertEndChild(layerAnimationsElement);
|
||||
|
||||
auto nullAnimationsElement = document.NewElement("NullAnimations");
|
||||
for (auto& [id, nullAnimation] : nullAnimations)
|
||||
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id, flags);
|
||||
element->InsertEndChild(nullAnimationsElement);
|
||||
|
||||
triggers.serialize(document, element, TRIGGER, -1, flags);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
void Animation::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, flags));
|
||||
}
|
||||
|
||||
std::string Animation::to_string()
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
int Animation::length()
|
||||
{
|
||||
int length{};
|
||||
|
||||
if (int rootAnimationLength = rootAnimation.length(ROOT); rootAnimationLength > length)
|
||||
length = rootAnimationLength;
|
||||
|
||||
for (auto& layerAnimation : layerAnimations | std::views::values)
|
||||
if (int layerAnimationLength = layerAnimation.length(LAYER); layerAnimationLength > length)
|
||||
length = layerAnimationLength;
|
||||
|
||||
for (auto& nullAnimation : nullAnimations | std::views::values)
|
||||
if (int nullAnimationLength = nullAnimation.length(NULL_); nullAnimationLength > length)
|
||||
length = nullAnimationLength;
|
||||
|
||||
if (int triggersLength = triggers.length(TRIGGER); triggersLength > length) length = triggersLength;
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
void Animation::fit_length() { frameNum = length(); }
|
||||
|
||||
vec4 Animation::rect(bool isRootTransform)
|
||||
{
|
||||
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
|
||||
|
||||
float minX = std::numeric_limits<float>::infinity();
|
||||
float minY = std::numeric_limits<float>::infinity();
|
||||
float maxX = -std::numeric_limits<float>::infinity();
|
||||
float maxY = -std::numeric_limits<float>::infinity();
|
||||
bool any = false;
|
||||
|
||||
for (float t = 0.0f; t < (float)frameNum; t += 1.0f)
|
||||
{
|
||||
mat4 transform(1.0f);
|
||||
|
||||
if (isRootTransform)
|
||||
{
|
||||
auto root = rootAnimation.frame_generate(t, ROOT);
|
||||
transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation);
|
||||
}
|
||||
|
||||
for (auto& [id, layerAnimation] : layerAnimations)
|
||||
{
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
auto frame = layerAnimation.frame_generate(t, LAYER);
|
||||
|
||||
if (frame.size == vec2() || !frame.isVisible) continue;
|
||||
|
||||
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
|
||||
math::percent_to_unit(frame.scale), frame.rotation);
|
||||
for (auto& corner : CORNERS)
|
||||
{
|
||||
vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f);
|
||||
minX = std::min(minX, world.x);
|
||||
minY = std::min(minY, world.y);
|
||||
maxX = std::max(maxX, world.x);
|
||||
maxY = std::max(maxY, world.y);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!any) return vec4(-1.0f);
|
||||
return {minX, minY, maxX - minX, maxY - minY};
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "item.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
constexpr auto FRAME_NUM_MIN = 1;
|
||||
constexpr auto FRAME_NUM_MAX = FRAME_DURATION_MAX;
|
||||
|
||||
class Animation
|
||||
{
|
||||
public:
|
||||
std::string name{};
|
||||
int frameNum{FRAME_NUM_MIN};
|
||||
bool isLoop{true};
|
||||
Item rootAnimation;
|
||||
std::unordered_map<int, Item> layerAnimations{};
|
||||
std::vector<int> layerOrder{};
|
||||
std::map<int, Item> nullAnimations{};
|
||||
Item triggers;
|
||||
|
||||
Animation() = default;
|
||||
Animation(tinyxml2::XMLElement*);
|
||||
Item* item_get(Type, int = -1);
|
||||
void item_remove(Type, int = -1);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
|
||||
std::string to_string();
|
||||
int length();
|
||||
void fit_length();
|
||||
glm::vec4 rect(bool);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#include "animations.hpp"
|
||||
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace tinyxml2;
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Animations::Animations(XMLElement* element)
|
||||
{
|
||||
xml::query_string_attribute(element, "DefaultAnimation", &defaultAnimation);
|
||||
|
||||
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
|
||||
items.push_back(Animation(child));
|
||||
}
|
||||
|
||||
XMLElement* Animations::to_element(XMLDocument& document, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Animations");
|
||||
element->SetAttribute("DefaultAnimation", defaultAnimation.c_str());
|
||||
for (auto& animation : items)
|
||||
animation.serialize(document, element, flags);
|
||||
return element;
|
||||
}
|
||||
|
||||
void Animations::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, flags));
|
||||
}
|
||||
|
||||
int Animations::length()
|
||||
{
|
||||
int length{};
|
||||
|
||||
for (auto& animation : items)
|
||||
if (int animationLength = animation.length(); animationLength > length) length = animationLength;
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "animation.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
constexpr auto MERGED_STRING = "(Merged)";
|
||||
|
||||
struct Animations
|
||||
{
|
||||
std::string defaultAnimation{};
|
||||
std::vector<Animation> items{};
|
||||
|
||||
Animations() = default;
|
||||
Animations(tinyxml2::XMLElement*);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
|
||||
int length();
|
||||
};
|
||||
}
|
||||
+1823
-474
File diff suppressed because it is too large
Load Diff
+328
-66
@@ -1,97 +1,359 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "icon.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
#include "animations.hpp"
|
||||
#include "content.hpp"
|
||||
#include "info.hpp"
|
||||
#define ANM2_ELEMENT_TYPES \
|
||||
X(UNKNOWN, "") \
|
||||
X(ANIMATED_ACTOR, "AnimatedActor") \
|
||||
X(INFO, "Info") \
|
||||
X(CONTENT, "Content") \
|
||||
X(SPRITESHEETS, "Spritesheets") \
|
||||
X(SPRITESHEET, "Spritesheet") \
|
||||
X(REGION, "Region") \
|
||||
X(LAYERS, "Layers") \
|
||||
X(LAYER_ELEMENT, "Layer") \
|
||||
X(NULLS, "Nulls") \
|
||||
X(NULL_ELEMENT, "Null") \
|
||||
X(EVENTS, "Events") \
|
||||
X(EVENT_ELEMENT, "Event") \
|
||||
X(SOUNDS, "Sounds") \
|
||||
X(SOUND_ELEMENT, "Sound") \
|
||||
X(ANIMATIONS, "Animations") \
|
||||
X(ANIMATION, "Animation") \
|
||||
X(ROOT_ANIMATION, "RootAnimation") \
|
||||
X(LAYER_ANIMATIONS, "LayerAnimations") \
|
||||
X(LAYER_ANIMATION, "LayerAnimation") \
|
||||
X(NULL_ANIMATIONS, "NullAnimations") \
|
||||
X(NULL_ANIMATION, "NullAnimation") \
|
||||
X(GROUP, "Group") \
|
||||
X(TRIGGERS, "Triggers") \
|
||||
X(FRAME, "Frame") \
|
||||
X(TRIGGER, "Trigger")
|
||||
|
||||
namespace anm2ed::anm2
|
||||
namespace anm2ed
|
||||
{
|
||||
enum class ElementType
|
||||
{
|
||||
#define X(symbol, tag) symbol,
|
||||
ANM2_ELEMENT_TYPES
|
||||
#undef X
|
||||
COUNT
|
||||
};
|
||||
|
||||
enum class Interpolation
|
||||
{
|
||||
NONE,
|
||||
LINEAR,
|
||||
EASE_IN,
|
||||
EASE_OUT,
|
||||
EASE_IN_OUT,
|
||||
COUNT
|
||||
};
|
||||
|
||||
enum class Origin
|
||||
{
|
||||
CUSTOM,
|
||||
TOP_LEFT,
|
||||
CENTER,
|
||||
COUNT
|
||||
};
|
||||
|
||||
enum class Compatibility
|
||||
{
|
||||
ISAAC,
|
||||
ANM2ED,
|
||||
ANM2ED_LIMITED,
|
||||
COUNT
|
||||
};
|
||||
|
||||
enum class ItemType
|
||||
{
|
||||
NONE,
|
||||
ROOT,
|
||||
LAYER,
|
||||
NULL_,
|
||||
TRIGGER,
|
||||
COUNT
|
||||
};
|
||||
|
||||
inline constexpr int NONE = (int)ItemType::NONE;
|
||||
inline constexpr int ROOT = (int)ItemType::ROOT;
|
||||
inline constexpr int LAYER = (int)ItemType::LAYER;
|
||||
inline constexpr int NULL_ = (int)ItemType::NULL_;
|
||||
inline constexpr int TRIGGER = (int)ItemType::TRIGGER;
|
||||
|
||||
inline constexpr int FRAME_DURATION_MIN = 1;
|
||||
inline constexpr int FRAME_DURATION_MAX = 1000000;
|
||||
inline constexpr int FRAME_NUM_MIN = 1;
|
||||
inline constexpr int FRAME_NUM_MAX = FRAME_DURATION_MAX;
|
||||
inline constexpr int FPS_MIN = 1;
|
||||
inline constexpr int FPS_MAX = 120;
|
||||
|
||||
inline constexpr int ISAAC = (int)Compatibility::ISAAC;
|
||||
inline constexpr int ANM2ED = (int)Compatibility::ANM2ED;
|
||||
inline constexpr int ANM2ED_LIMITED = (int)Compatibility::ANM2ED_LIMITED;
|
||||
|
||||
enum class SpritesheetMergeOrigin
|
||||
{
|
||||
APPEND_RIGHT,
|
||||
APPEND_BOTTOM,
|
||||
COUNT
|
||||
};
|
||||
|
||||
inline constexpr int APPEND_RIGHT = (int)SpritesheetMergeOrigin::APPEND_RIGHT;
|
||||
inline constexpr int APPEND_BOTTOM = (int)SpritesheetMergeOrigin::APPEND_BOTTOM;
|
||||
|
||||
inline const glm::vec4 ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f);
|
||||
inline const glm::vec4 ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f);
|
||||
inline const glm::vec4 ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f);
|
||||
|
||||
inline const glm::vec4 LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f);
|
||||
inline const glm::vec4 LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f);
|
||||
inline const glm::vec4 LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f);
|
||||
|
||||
inline const glm::vec4 NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f);
|
||||
inline const glm::vec4 NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f);
|
||||
inline const glm::vec4 NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f);
|
||||
|
||||
inline const glm::vec4 TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f);
|
||||
inline const glm::vec4 TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f);
|
||||
inline const glm::vec4 TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f);
|
||||
|
||||
#define ANM2_ITEM_TYPES \
|
||||
X(NONE, STRING_UNDEFINED, "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \
|
||||
X(ROOT, BASIC_ROOT, "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \
|
||||
X(LAYER, BASIC_LAYER_ANIMATION, "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, \
|
||||
LAYER_COLOR_HOVERED) \
|
||||
X(NULL_, BASIC_NULL_ANIMATION, "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, \
|
||||
NULL_COLOR_HOVERED) \
|
||||
X(TRIGGER, BASIC_TRIGGERS, "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE, \
|
||||
TRIGGER_COLOR_HOVERED)
|
||||
|
||||
constexpr StringType TYPE_STRINGS[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string,
|
||||
ANM2_ITEM_TYPES
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr const char* TYPE_ITEM_STRINGS[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString,
|
||||
ANM2_ITEM_TYPES
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr resource::icon::Type TYPE_ICONS[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon,
|
||||
ANM2_ITEM_TYPES
|
||||
#undef X
|
||||
};
|
||||
|
||||
inline const glm::vec4 TYPE_COLOR[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color,
|
||||
ANM2_ITEM_TYPES
|
||||
#undef X
|
||||
};
|
||||
|
||||
inline const glm::vec4 TYPE_COLOR_ACTIVE[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive,
|
||||
ANM2_ITEM_TYPES
|
||||
#undef X
|
||||
};
|
||||
|
||||
inline const glm::vec4 TYPE_COLOR_HOVERED[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered,
|
||||
ANM2_ITEM_TYPES
|
||||
#undef X
|
||||
};
|
||||
|
||||
enum class ChangeType
|
||||
{
|
||||
ADJUST,
|
||||
ADD,
|
||||
SUBTRACT,
|
||||
MULTIPLY,
|
||||
DIVIDE
|
||||
};
|
||||
|
||||
enum Flag
|
||||
{
|
||||
NO_SOUNDS = 1 << 0,
|
||||
NO_REGIONS = 1 << 1,
|
||||
FRAME_NO_REGION_VALUES = 1 << 2,
|
||||
INTERPOLATION_BOOL_ONLY = 1 << 3,
|
||||
NO_GROUPS = 1 << 4
|
||||
};
|
||||
|
||||
using Flags = int;
|
||||
|
||||
constexpr bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; }
|
||||
|
||||
struct Options
|
||||
{
|
||||
Compatibility compatibility{Compatibility::ANM2ED};
|
||||
bool isBakeSpecialInterpolatedFrames{};
|
||||
bool isRoundScale{true};
|
||||
bool isRoundRotation{true};
|
||||
};
|
||||
|
||||
struct FrameChange
|
||||
{
|
||||
std::optional<bool> isVisible{};
|
||||
std::optional<Interpolation> interpolation{};
|
||||
std::optional<float> rotation{};
|
||||
std::optional<int> duration{};
|
||||
std::optional<int> regionId{};
|
||||
std::optional<float> pivotX{};
|
||||
std::optional<float> pivotY{};
|
||||
std::optional<float> cropX{};
|
||||
std::optional<float> cropY{};
|
||||
std::optional<float> positionX{};
|
||||
std::optional<float> positionY{};
|
||||
std::optional<float> sizeX{};
|
||||
std::optional<float> sizeY{};
|
||||
std::optional<float> scaleX{};
|
||||
std::optional<float> scaleY{};
|
||||
std::optional<float> colorOffsetR{};
|
||||
std::optional<float> colorOffsetG{};
|
||||
std::optional<float> colorOffsetB{};
|
||||
std::optional<float> tintR{};
|
||||
std::optional<float> tintG{};
|
||||
std::optional<float> tintB{};
|
||||
std::optional<float> tintA{};
|
||||
std::optional<bool> isFlipX{};
|
||||
std::optional<bool> isFlipY{};
|
||||
};
|
||||
|
||||
struct Element
|
||||
{
|
||||
ElementType type{ElementType::UNKNOWN};
|
||||
std::string tag{};
|
||||
std::vector<Element> children{};
|
||||
std::string name{};
|
||||
std::string createdBy{"robot"};
|
||||
std::string createdOn{};
|
||||
std::filesystem::path path{};
|
||||
std::string defaultAnimation{};
|
||||
int id{-1};
|
||||
int layerId{-1};
|
||||
int nullId{-1};
|
||||
int spritesheetId{};
|
||||
int fps{30};
|
||||
int version{};
|
||||
int frameNum{1};
|
||||
int duration{1};
|
||||
int atFrame{-1};
|
||||
int eventId{-1};
|
||||
int regionId{-1};
|
||||
int soundId{-1};
|
||||
int groupId{-1};
|
||||
bool isLoop{true};
|
||||
bool isVisible{true};
|
||||
bool isShowRect{};
|
||||
bool isExpanded{true};
|
||||
Interpolation interpolation{Interpolation::NONE};
|
||||
Origin origin{Origin::CUSTOM};
|
||||
float rotation{};
|
||||
std::vector<int> soundIds{};
|
||||
glm::vec2 pivot{};
|
||||
glm::vec2 crop{};
|
||||
glm::vec2 position{};
|
||||
glm::vec2 size{};
|
||||
glm::vec2 scale{100.0f, 100.0f};
|
||||
glm::vec3 colorOffset{};
|
||||
glm::vec4 tint{types::color::WHITE};
|
||||
};
|
||||
|
||||
struct Reference
|
||||
{
|
||||
int animationIndex{-1};
|
||||
Type itemType{NONE};
|
||||
int itemType{(int)ItemType::NONE};
|
||||
int itemID{-1};
|
||||
int frameIndex{-1};
|
||||
|
||||
auto operator<=>(const Reference&) const = default;
|
||||
};
|
||||
|
||||
Flags flags_for(Compatibility);
|
||||
Element element_make(ElementType);
|
||||
Element element_read(const tinyxml2::XMLElement*);
|
||||
std::string element_to_string(const Element&, Flags = 0);
|
||||
tinyxml2::XMLElement* element_to_xml(tinyxml2::XMLDocument&, const Element&, Flags = 0);
|
||||
Element* element_child_first_get(Element&, ElementType);
|
||||
const Element* element_child_first_get(const Element&, ElementType);
|
||||
Element* element_child_id_get(Element&, ElementType, int);
|
||||
const Element* element_child_id_get(const Element&, ElementType, int);
|
||||
int element_child_next_id_get(const Element&, ElementType);
|
||||
int element_child_max_id_get(const Element&, ElementType);
|
||||
bool element_child_id_erase(Element&, ElementType, int);
|
||||
Element* element_first_get(Element&, ElementType);
|
||||
const Element* element_first_get(const Element&, ElementType);
|
||||
Element* animation_item_get(Element&, ItemType, int = -1);
|
||||
const Element* animation_item_get(const Element&, ItemType, int = -1);
|
||||
Element* track_frame_get(Element&, int);
|
||||
const Element* track_frame_get(const Element&, int);
|
||||
Element frame_generate(const Element&, float);
|
||||
int frame_index_from_at_frame_get(const Element&, int);
|
||||
int frame_index_from_time_get(const Element&, float);
|
||||
float frame_time_from_index_get(const Element&, int);
|
||||
void frame_bake(Element&, int, int, bool, bool);
|
||||
void frames_generate_from_grid(Element&, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int);
|
||||
bool frames_deserialize(Element&, const std::string&, int, std::set<int>&, std::string* = nullptr);
|
||||
void frames_sort_by_at_frame(Element&);
|
||||
void frames_change(Element&, FrameChange, ItemType, ChangeType, const std::set<int>&);
|
||||
int track_length_get(const Element&);
|
||||
int animation_length_get(const Element&);
|
||||
|
||||
class Anm2
|
||||
{
|
||||
public:
|
||||
bool isValid{true};
|
||||
Info info{};
|
||||
Content content{};
|
||||
Animations animations{};
|
||||
Element root{};
|
||||
|
||||
Anm2();
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Flags = 0);
|
||||
bool serialize(const std::filesystem::path&, std::string* = nullptr, Flags = 0, bool = false, bool = true,
|
||||
bool = true);
|
||||
std::string to_string(Flags = 0);
|
||||
Anm2(const std::filesystem::path&, std::string* = nullptr);
|
||||
uint64_t hash();
|
||||
|
||||
bool load(const std::filesystem::path&, std::string* = nullptr);
|
||||
bool save(const std::filesystem::path&, std::string* = nullptr, Options = {}) const;
|
||||
std::string to_string(Options = {}) const;
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Options = {}) const;
|
||||
std::uint64_t hash(Options = {}) const;
|
||||
bool is_special_interpolated_frames() const;
|
||||
void special_interpolated_frames_bake(int, bool, bool);
|
||||
void region_frames_sync(bool);
|
||||
Anm2 normalized_for_serialize() const;
|
||||
|
||||
Spritesheet* spritesheet_get(int);
|
||||
bool spritesheet_add(const std::filesystem::path&, const std::filesystem::path&, int&);
|
||||
bool spritesheet_pack(int, int);
|
||||
bool regions_trim(int, const std::set<int>&);
|
||||
std::vector<std::string> spritesheet_labels_get();
|
||||
std::vector<int> spritesheet_ids_get();
|
||||
std::set<int> spritesheets_unused();
|
||||
bool spritesheets_merge(const std::set<int>&, SpritesheetMergeOrigin, bool, bool, origin::Type);
|
||||
bool spritesheets_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type type,
|
||||
std::string*);
|
||||
std::vector<std::string> region_labels_get(Spritesheet&);
|
||||
std::vector<int> region_ids_get(Spritesheet&);
|
||||
std::set<int> regions_unused(Spritesheet&);
|
||||
void scan_and_set_regions();
|
||||
|
||||
void layer_add(int&);
|
||||
std::set<int> layers_unused();
|
||||
std::set<int> layers_unused(Animation&);
|
||||
bool layers_deserialize(const std::string&, types::merge::Type, std::string*);
|
||||
|
||||
void null_add(int&);
|
||||
std::set<int> nulls_unused();
|
||||
std::set<int> nulls_unused(Animation&);
|
||||
bool nulls_deserialize(const std::string&, types::merge::Type, std::string*);
|
||||
|
||||
void event_add(int&);
|
||||
std::vector<std::string> event_labels_get();
|
||||
std::vector<int> event_ids_get();
|
||||
std::set<int> events_unused();
|
||||
bool events_deserialize(const std::string&, types::merge::Type, std::string*);
|
||||
|
||||
bool sound_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id);
|
||||
std::vector<std::string> sound_labels_get();
|
||||
std::vector<int> sound_ids_get();
|
||||
std::set<int> sounds_unused();
|
||||
bool sounds_deserialize(const std::string&, const std::filesystem::path&, types::merge::Type, std::string*);
|
||||
|
||||
Animation* animation_get(int);
|
||||
std::vector<std::string> animation_labels_get();
|
||||
int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
|
||||
Element* element_get(ElementType);
|
||||
const Element* element_get(ElementType) const;
|
||||
Element* element_get(ElementType, int);
|
||||
const Element* element_get(ElementType, int) const;
|
||||
Element* element_get(int, ItemType, int = -1);
|
||||
const Element* element_get(int, ItemType, int = -1) const;
|
||||
Element* element_get(int, ItemType, int, int);
|
||||
const Element* element_get(int, ItemType, int, int) const;
|
||||
std::set<int> element_unused(ElementType) const;
|
||||
std::set<int> element_unused(ElementType, const Element&) const;
|
||||
std::set<int> element_unused(ElementType, int) const;
|
||||
bool deserialize(ElementType, const std::string&, bool, std::string* = nullptr, const std::filesystem::path& = {});
|
||||
bool regions_deserialize(int, const std::string&, bool, std::string* = nullptr);
|
||||
Element frame_effective(int, const Element&) const;
|
||||
glm::vec4 animation_rect(const Element&, bool) const;
|
||||
int layer_animation_add(int, int = -1, int = -1, std::string = {}, int = 0,
|
||||
types::destination::Type = types::destination::ALL);
|
||||
int null_animation_add(int, int = -1, std::string = {}, bool = false,
|
||||
types::destination::Type = types::destination::ALL);
|
||||
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
|
||||
Frame frame_effective(int, const Frame&) const;
|
||||
glm::vec4 animation_rect(Animation&, bool) const;
|
||||
bool has_special_interpolated_frames() const;
|
||||
void bake_special_interpolated_frames(int, bool, bool);
|
||||
|
||||
Item* item_get(int, Type, int = -1);
|
||||
Reference layer_animation_add(Reference = {}, int = -1, std::string = {}, int = 0,
|
||||
types::destination::Type = types::destination::ALL);
|
||||
Reference null_animation_add(Reference = {}, std::string = {}, bool = false,
|
||||
types::destination::Type = types::destination::ALL);
|
||||
|
||||
Frame* frame_get(int, Type, int, int = -1);
|
||||
void merge(const Anm2&, const std::filesystem::path&, const std::filesystem::path&);
|
||||
int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include "vector_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::types;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Animation* Anm2::animation_get(int animationIndex) { return vector::find(animations.items, animationIndex); }
|
||||
|
||||
std::vector<std::string> Anm2::animation_labels_get()
|
||||
{
|
||||
std::vector<std::string> labels{};
|
||||
labels.emplace_back("None");
|
||||
for (auto& animation : animations.items)
|
||||
labels.emplace_back(animation.name);
|
||||
return labels;
|
||||
}
|
||||
|
||||
bool Anm2::animations_deserialize(const std::string& string, int start, std::set<int>& indices,
|
||||
std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
if (!document.FirstChildElement("Animation"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid animation(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
int count{};
|
||||
for (auto element = document.FirstChildElement("Animation"); element;
|
||||
element = element->NextSiblingElement("Animation"))
|
||||
{
|
||||
auto index = start + count;
|
||||
auto& animation = *animations.items.insert(animations.items.begin() + start + count, Animation(element));
|
||||
|
||||
for (auto& trigger : animation.triggers.frames)
|
||||
if (!content.events.contains(trigger.eventID)) trigger.eventID = -1;
|
||||
|
||||
indices.insert(index);
|
||||
count++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int Anm2::animations_merge(int target, std::set<int>& sources, merge::Type type, bool isDeleteAfter)
|
||||
{
|
||||
auto& items = animations.items;
|
||||
auto& animation = animations.items.at(target);
|
||||
|
||||
if (!animation.name.ends_with(MERGED_STRING)) animation.name = animation.name + " " + MERGED_STRING;
|
||||
|
||||
auto merge_item = [&](Item& destination, Item& source)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case merge::APPEND:
|
||||
destination.frames.insert(destination.frames.end(), source.frames.begin(), source.frames.end());
|
||||
break;
|
||||
case merge::PREPEND:
|
||||
destination.frames.insert(destination.frames.begin(), source.frames.begin(), source.frames.end());
|
||||
break;
|
||||
case merge::REPLACE:
|
||||
if (destination.frames.size() < source.frames.size()) destination.frames.resize(source.frames.size());
|
||||
for (int i = 0; i < (int)source.frames.size(); i++)
|
||||
destination.frames[i] = source.frames[i];
|
||||
break;
|
||||
case merge::IGNORE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& i : sources)
|
||||
{
|
||||
if (i == target) continue;
|
||||
if (i < 0 || i >= (int)items.size()) continue;
|
||||
|
||||
auto& source = items.at(i);
|
||||
|
||||
merge_item(animation.rootAnimation, source.rootAnimation);
|
||||
|
||||
for (auto& [id, layerAnimation] : source.layerAnimations)
|
||||
{
|
||||
if (!animation.layerAnimations.contains(id))
|
||||
{
|
||||
animation.layerAnimations[id] = layerAnimation;
|
||||
animation.layerOrder.emplace_back(id);
|
||||
}
|
||||
merge_item(animation.layerAnimations[id], layerAnimation);
|
||||
}
|
||||
|
||||
for (auto& [id, nullAnimation] : source.nullAnimations)
|
||||
{
|
||||
if (!animation.nullAnimations.contains(id)) animation.nullAnimations[id] = nullAnimation;
|
||||
merge_item(animation.nullAnimations[id], nullAnimation);
|
||||
}
|
||||
|
||||
merge_item(animation.triggers, source.triggers);
|
||||
}
|
||||
|
||||
if (isDeleteAfter)
|
||||
{
|
||||
for (auto& source : std::ranges::reverse_view(sources))
|
||||
{
|
||||
if (source == target) continue;
|
||||
items.erase(items.begin() + source);
|
||||
}
|
||||
}
|
||||
|
||||
int finalIndex = target;
|
||||
|
||||
if (isDeleteAfter)
|
||||
{
|
||||
int numDeletedBefore = 0;
|
||||
for (auto& idx : sources)
|
||||
{
|
||||
if (idx == target) continue;
|
||||
if (idx >= 0 && idx < target) ++numDeletedBefore;
|
||||
}
|
||||
finalIndex -= numDeletedBefore;
|
||||
}
|
||||
|
||||
animation.frameNum = animation.length();
|
||||
|
||||
return finalIndex;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
#include "map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
void Anm2::event_add(int& id)
|
||||
{
|
||||
id = map::next_id_get(content.events);
|
||||
content.events[id] = Event();
|
||||
}
|
||||
|
||||
std::vector<std::string> Anm2::event_labels_get()
|
||||
{
|
||||
std::vector<std::string> labels{};
|
||||
labels.emplace_back(localize.get(BASIC_NONE));
|
||||
for (auto& event : content.events | std::views::values)
|
||||
labels.emplace_back(event.name);
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<int> Anm2::event_ids_get()
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.emplace_back(-1);
|
||||
for (auto& id : content.events | std::views::keys)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::set<int> Anm2::events_unused()
|
||||
{
|
||||
std::set<int> used{};
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
for (auto& frame : animation.triggers.frames)
|
||||
if (frame.eventID != -1) used.insert(frame.eventID);
|
||||
|
||||
std::set<int> unused{};
|
||||
for (auto& id : content.events | std::views::keys)
|
||||
if (!used.contains(id)) unused.insert(id);
|
||||
|
||||
return unused;
|
||||
}
|
||||
|
||||
bool Anm2::events_deserialize(const std::string& string, merge::Type type, std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
int id{};
|
||||
|
||||
if (!document.FirstChildElement("Event"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid event(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto element = document.FirstChildElement("Event"); element; element = element->NextSiblingElement("Event"))
|
||||
{
|
||||
auto event = Event(element, id);
|
||||
if (type == merge::APPEND) id = map::next_id_get(content.events);
|
||||
content.events[id] = event;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include "map_.hpp"
|
||||
#include "types.hpp"
|
||||
#include "unordered_map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Item* Anm2::item_get(int animationIndex, Type type, int id)
|
||||
{
|
||||
if (Animation* animation = animation_get(animationIndex))
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ROOT:
|
||||
return &animation->rootAnimation;
|
||||
case LAYER:
|
||||
return unordered_map::find(animation->layerAnimations, id);
|
||||
case NULL_:
|
||||
return map::find(animation->nullAnimations, id);
|
||||
case TRIGGER:
|
||||
return &animation->triggers;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Reference Anm2::layer_animation_add(Reference reference, int insertBeforeID, std::string name, int spritesheetID,
|
||||
destination::Type destination)
|
||||
{
|
||||
auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID;
|
||||
auto& layer = content.layers[id];
|
||||
|
||||
layer.name = !name.empty() ? name : layer.name;
|
||||
layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0;
|
||||
|
||||
auto add = [&](Animation* animation, int id, bool insertBeforeReference)
|
||||
{
|
||||
animation->layerAnimations[id] = Item();
|
||||
|
||||
if (insertBeforeReference && insertBeforeID != -1)
|
||||
{
|
||||
auto it = std::find(animation->layerOrder.begin(), animation->layerOrder.end(), insertBeforeID);
|
||||
if (it != animation->layerOrder.end())
|
||||
{
|
||||
animation->layerOrder.insert(it, id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
animation->layerOrder.push_back(id);
|
||||
};
|
||||
|
||||
if (destination == destination::ALL)
|
||||
{
|
||||
for (size_t index = 0; index < animations.items.size(); ++index)
|
||||
{
|
||||
auto& animation = animations.items[index];
|
||||
if (!animation.layerAnimations.contains(id)) add(&animation, id, true);
|
||||
}
|
||||
}
|
||||
else if (destination == destination::THIS)
|
||||
{
|
||||
if (auto animation = animation_get(reference.animationIndex))
|
||||
if (!animation->layerAnimations.contains(id)) add(animation, id, true);
|
||||
}
|
||||
|
||||
return {reference.animationIndex, LAYER, id};
|
||||
}
|
||||
|
||||
Reference Anm2::null_animation_add(Reference reference, std::string name, bool isShowRect, destination::Type destination)
|
||||
{
|
||||
auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID;
|
||||
auto& null = content.nulls[id];
|
||||
|
||||
null.name = !name.empty() ? name : null.name;
|
||||
null.isShowRect = isShowRect;
|
||||
|
||||
auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); };
|
||||
|
||||
if (destination == destination::ALL)
|
||||
{
|
||||
for (auto& animation : animations.items)
|
||||
if (!animation.nullAnimations.contains(id)) add(&animation, id);
|
||||
}
|
||||
else if (destination == destination::THIS)
|
||||
{
|
||||
if (auto animation = animation_get(reference.animationIndex))
|
||||
if (!animation->nullAnimations.contains(id)) add(animation, id);
|
||||
}
|
||||
|
||||
return {reference.animationIndex, NULL_, id};
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
#include "map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
void Anm2::layer_add(int& id)
|
||||
{
|
||||
id = map::next_id_get(content.layers);
|
||||
content.layers[id] = Layer();
|
||||
}
|
||||
|
||||
std::set<int> Anm2::layers_unused()
|
||||
{
|
||||
std::set<int> used{};
|
||||
std::set<int> unused{};
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
for (auto& id : animation.layerAnimations | std::views::keys)
|
||||
used.insert(id);
|
||||
|
||||
for (auto& id : content.layers | std::views::keys)
|
||||
if (!used.contains(id)) unused.insert(id);
|
||||
|
||||
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)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
int id{};
|
||||
|
||||
if (!document.FirstChildElement("Layer"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid layer(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto element = document.FirstChildElement("Layer"); element; element = element->NextSiblingElement("Layer"))
|
||||
{
|
||||
auto layer = Layer(element, id);
|
||||
if (type == merge::APPEND) id = map::next_id_get(content.layers);
|
||||
content.layers[id] = layer;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
#include "map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
void Anm2::null_add(int& id)
|
||||
{
|
||||
id = map::next_id_get(content.nulls);
|
||||
content.nulls[id] = Null();
|
||||
}
|
||||
|
||||
std::set<int> Anm2::nulls_unused()
|
||||
{
|
||||
std::set<int> used{};
|
||||
std::set<int> unused{};
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
for (auto& id : animation.nullAnimations | std::views::keys)
|
||||
used.insert(id);
|
||||
|
||||
for (auto& id : content.nulls | std::views::keys)
|
||||
if (!used.contains(id)) unused.insert(id);
|
||||
|
||||
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)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
int id{};
|
||||
|
||||
if (!document.FirstChildElement("Null"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid null(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto element = document.FirstChildElement("Null"); element; element = element->NextSiblingElement("Null"))
|
||||
{
|
||||
auto null = Null(element, id);
|
||||
if (type == merge::APPEND) id = map::next_id_get(content.nulls);
|
||||
content.nulls[id] = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "map_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
bool Anm2::sound_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id)
|
||||
{
|
||||
id = map::next_id_get(content.sounds);
|
||||
content.sounds[id] = Sound(directory, path);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> Anm2::sound_labels_get()
|
||||
{
|
||||
std::vector<std::string> labels{};
|
||||
labels.emplace_back(localize.get(BASIC_NONE));
|
||||
for (auto& [id, sound] : content.sounds)
|
||||
{
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
labels.emplace_back(std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString)));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<int> Anm2::sound_ids_get()
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.emplace_back(-1);
|
||||
for (auto& [id, sound] : content.sounds)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::set<int> Anm2::sounds_unused()
|
||||
{
|
||||
std::set<int> used;
|
||||
for (auto& animation : animations.items)
|
||||
for (auto& trigger : animation.triggers.frames)
|
||||
for (auto& soundID : trigger.soundIDs)
|
||||
if (content.sounds.contains(soundID)) used.insert(soundID);
|
||||
|
||||
std::set<int> unused;
|
||||
for (auto& [id, sound] : content.sounds)
|
||||
if (!used.contains(id)) unused.insert(id);
|
||||
|
||||
return unused;
|
||||
}
|
||||
|
||||
bool Anm2::sounds_deserialize(const std::string& string, const std::filesystem::path& directory, merge::Type type,
|
||||
std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
int id{};
|
||||
|
||||
if (!document.FirstChildElement("Sound"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid sound(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
|
||||
for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound"))
|
||||
{
|
||||
auto sound = Sound(element, id);
|
||||
if (type == merge::APPEND) id = map::next_id_get(content.sounds);
|
||||
content.sounds[id] = std::move(sound);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,700 +0,0 @@
|
||||
#include "anm2.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "map_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Spritesheet* Anm2::spritesheet_get(int id) { return map::find(content.spritesheets, id); }
|
||||
|
||||
bool Anm2::spritesheet_add(const std::filesystem::path& directory, const std::filesystem::path& path, int& id)
|
||||
{
|
||||
Spritesheet spritesheet(directory, path);
|
||||
if (!spritesheet.is_valid()) return false;
|
||||
id = map::next_id_get(content.spritesheets);
|
||||
content.spritesheets[id] = std::move(spritesheet);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Anm2::spritesheet_pack(int id, int padding)
|
||||
{
|
||||
const int packingPadding = std::max(0, padding);
|
||||
|
||||
struct RectI
|
||||
{
|
||||
int x{};
|
||||
int y{};
|
||||
int w{};
|
||||
int h{};
|
||||
};
|
||||
|
||||
struct PackItem
|
||||
{
|
||||
int regionID{-1};
|
||||
int srcX{};
|
||||
int srcY{};
|
||||
int width{};
|
||||
int height{};
|
||||
int packWidth{};
|
||||
int packHeight{};
|
||||
};
|
||||
|
||||
class MaxRectsPacker
|
||||
{
|
||||
int width{};
|
||||
int height{};
|
||||
std::vector<RectI> freeRects{};
|
||||
|
||||
static bool intersects(const RectI& a, const RectI& b)
|
||||
{
|
||||
return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y);
|
||||
}
|
||||
|
||||
static bool contains(const RectI& a, const RectI& b)
|
||||
{
|
||||
return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
|
||||
}
|
||||
|
||||
void split_free_rects(const RectI& used)
|
||||
{
|
||||
std::vector<RectI> next{};
|
||||
next.reserve(freeRects.size() * 2);
|
||||
|
||||
for (auto& free : freeRects)
|
||||
{
|
||||
if (!intersects(free, used))
|
||||
{
|
||||
next.push_back(free);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h});
|
||||
if (used.x + used.w < free.x + free.w)
|
||||
next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h});
|
||||
if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y});
|
||||
if (used.y + used.h < free.y + free.h)
|
||||
next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)});
|
||||
}
|
||||
|
||||
freeRects = std::move(next);
|
||||
}
|
||||
|
||||
void prune_free_rects()
|
||||
{
|
||||
for (int i = 0; i < (int)freeRects.size(); i++)
|
||||
{
|
||||
if (freeRects[i].w <= 0 || freeRects[i].h <= 0)
|
||||
{
|
||||
freeRects.erase(freeRects.begin() + i--);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = i + 1; j < (int)freeRects.size();)
|
||||
{
|
||||
if (contains(freeRects[i], freeRects[j]))
|
||||
freeRects.erase(freeRects.begin() + j);
|
||||
else if (contains(freeRects[j], freeRects[i]))
|
||||
{
|
||||
freeRects.erase(freeRects.begin() + i);
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
else
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
MaxRectsPacker(int width, int height) : width(width), height(height), freeRects({{0, 0, width, height}}) {}
|
||||
|
||||
bool insert(int width, int height, RectI& result)
|
||||
{
|
||||
int bestShort = std::numeric_limits<int>::max();
|
||||
int bestLong = std::numeric_limits<int>::max();
|
||||
RectI best{};
|
||||
bool found{};
|
||||
|
||||
for (auto& free : freeRects)
|
||||
{
|
||||
if (width > free.w || height > free.h) continue;
|
||||
|
||||
int leftOverW = free.w - width;
|
||||
int leftOverH = free.h - height;
|
||||
int shortSide = std::min(leftOverW, leftOverH);
|
||||
int longSide = std::max(leftOverW, leftOverH);
|
||||
|
||||
if (shortSide < bestShort || (shortSide == bestShort && longSide < bestLong))
|
||||
{
|
||||
bestShort = shortSide;
|
||||
bestLong = longSide;
|
||||
best = {free.x, free.y, width, height};
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) return false;
|
||||
|
||||
result = best;
|
||||
split_free_rects(best);
|
||||
prune_free_rects();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
auto pack_regions = [&](const std::vector<PackItem>& items, int& packedWidth, int& packedHeight,
|
||||
std::unordered_map<int, RectI>& packedRects)
|
||||
{
|
||||
if (items.empty()) return false;
|
||||
|
||||
int maxWidth{};
|
||||
int maxHeight{};
|
||||
int sumWidth{};
|
||||
int sumHeight{};
|
||||
int64_t totalArea{};
|
||||
for (auto& item : items)
|
||||
{
|
||||
maxWidth = std::max(maxWidth, item.packWidth);
|
||||
maxHeight = std::max(maxHeight, item.packHeight);
|
||||
sumWidth += item.packWidth;
|
||||
sumHeight += item.packHeight;
|
||||
totalArea += (int64_t)item.packWidth * item.packHeight;
|
||||
}
|
||||
|
||||
if (maxWidth <= 0 || maxHeight <= 0) return false;
|
||||
|
||||
int bestSquareDelta = std::numeric_limits<int>::max();
|
||||
int bestArea = std::numeric_limits<int>::max();
|
||||
int bestWidth{};
|
||||
int bestHeight{};
|
||||
std::unordered_map<int, RectI> bestRects{};
|
||||
|
||||
int startWidth = maxWidth;
|
||||
int endWidth = std::max(startWidth, sumWidth);
|
||||
int step = std::max(1, (endWidth - startWidth) / 512);
|
||||
|
||||
for (int candidateWidth = startWidth; candidateWidth <= endWidth; candidateWidth += step)
|
||||
{
|
||||
int candidateHeightMin = std::max(maxHeight, (int)std::ceil((double)totalArea / candidateWidth));
|
||||
bool isValid{};
|
||||
int usedWidth{};
|
||||
int usedHeight{};
|
||||
std::unordered_map<int, RectI> candidateRects{};
|
||||
|
||||
// Grow candidate height until this width can actually fit all rectangles.
|
||||
for (int candidateHeight = candidateHeightMin; candidateHeight <= sumHeight; candidateHeight++)
|
||||
{
|
||||
MaxRectsPacker packer(candidateWidth, candidateHeight);
|
||||
candidateRects.clear();
|
||||
isValid = true;
|
||||
usedWidth = 0;
|
||||
usedHeight = 0;
|
||||
|
||||
for (auto& item : items)
|
||||
{
|
||||
RectI rect{};
|
||||
if (!packer.insert(item.packWidth, item.packHeight, rect))
|
||||
{
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
candidateRects[item.regionID] = rect;
|
||||
usedWidth = std::max(usedWidth, rect.x + rect.w);
|
||||
usedHeight = std::max(usedHeight, rect.y + rect.h);
|
||||
}
|
||||
|
||||
if (isValid) break;
|
||||
}
|
||||
|
||||
if (!isValid) continue;
|
||||
|
||||
int area = usedWidth * usedHeight;
|
||||
int squareDelta = std::abs(usedWidth - usedHeight);
|
||||
if (squareDelta < bestSquareDelta || (squareDelta == bestSquareDelta && area < bestArea))
|
||||
{
|
||||
bestSquareDelta = squareDelta;
|
||||
bestArea = area;
|
||||
bestWidth = usedWidth;
|
||||
bestHeight = usedHeight;
|
||||
bestRects = std::move(candidateRects);
|
||||
if (bestArea == totalArea && bestSquareDelta == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestArea == std::numeric_limits<int>::max()) return false;
|
||||
|
||||
packedWidth = bestWidth;
|
||||
packedHeight = bestHeight;
|
||||
packedRects = std::move(bestRects);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!content.spritesheets.contains(id)) return false;
|
||||
auto& spritesheet = content.spritesheets.at(id);
|
||||
if (!spritesheet.texture.is_valid() || spritesheet.texture.pixels.empty()) return false;
|
||||
if (spritesheet.regions.empty()) return false;
|
||||
|
||||
std::vector<PackItem> items{};
|
||||
items.reserve(spritesheet.regions.size());
|
||||
|
||||
for (auto& [regionID, region] : spritesheet.regions)
|
||||
{
|
||||
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
|
||||
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
|
||||
auto size = glm::max(maxPoint - minPoint, glm::ivec2(1));
|
||||
int packWidth = size.x + packingPadding * 2;
|
||||
int packHeight = size.y + packingPadding * 2;
|
||||
items.push_back({regionID, minPoint.x, minPoint.y, size.x, size.y, packWidth, packHeight});
|
||||
}
|
||||
|
||||
std::sort(items.begin(), items.end(), [](const PackItem& a, const PackItem& b)
|
||||
{
|
||||
int areaA = a.width * a.height;
|
||||
int areaB = b.width * b.height;
|
||||
if (areaA != areaB) return areaA > areaB;
|
||||
return a.regionID < b.regionID;
|
||||
});
|
||||
|
||||
int packedWidth{};
|
||||
int packedHeight{};
|
||||
std::unordered_map<int, RectI> packedRects{};
|
||||
if (!pack_regions(items, packedWidth, packedHeight, packedRects)) return false;
|
||||
if (packedWidth <= 0 || packedHeight <= 0) return false;
|
||||
|
||||
auto textureSize = spritesheet.texture.size;
|
||||
auto& sourcePixels = spritesheet.texture.pixels;
|
||||
std::vector<uint8_t> packedPixels((size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0);
|
||||
|
||||
for (auto& item : items)
|
||||
{
|
||||
if (!packedRects.contains(item.regionID)) continue;
|
||||
auto destinationRect = packedRects.at(item.regionID);
|
||||
|
||||
for (int y = 0; y < item.height; y++)
|
||||
{
|
||||
for (int x = 0; x < item.width; x++)
|
||||
{
|
||||
int sourceX = item.srcX + x;
|
||||
int sourceY = item.srcY + y;
|
||||
int destinationX = destinationRect.x + packingPadding + x;
|
||||
int destinationY = destinationRect.y + packingPadding + y;
|
||||
|
||||
if (sourceX < 0 || sourceY < 0 || sourceX >= textureSize.x || sourceY >= textureSize.y) continue;
|
||||
if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight)
|
||||
continue;
|
||||
|
||||
auto sourceIndex = ((size_t)sourceY * textureSize.x + sourceX) * resource::texture::CHANNELS;
|
||||
auto destinationIndex =
|
||||
((size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS;
|
||||
std::copy_n(sourcePixels.data() + sourceIndex, resource::texture::CHANNELS,
|
||||
packedPixels.data() + destinationIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spritesheet.texture = resource::Texture(packedPixels.data(), {packedWidth, packedHeight});
|
||||
|
||||
for (auto& [regionID, region] : spritesheet.regions)
|
||||
if (packedRects.contains(regionID))
|
||||
{
|
||||
auto& rect = packedRects.at(regionID);
|
||||
region.crop = {rect.x + packingPadding, rect.y + packingPadding};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Anm2::regions_trim(int spritesheetID, const std::set<int>& ids)
|
||||
{
|
||||
auto spritesheet = spritesheet_get(spritesheetID);
|
||||
if (!spritesheet || !spritesheet->texture.is_valid() || spritesheet->texture.pixels.empty() || ids.empty())
|
||||
return false;
|
||||
|
||||
auto& texture = spritesheet->texture;
|
||||
bool changed{};
|
||||
|
||||
for (auto id : ids)
|
||||
{
|
||||
if (!spritesheet->regions.contains(id)) continue;
|
||||
auto& region = spritesheet->regions.at(id);
|
||||
|
||||
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
|
||||
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
|
||||
|
||||
int minX = std::max(0, minPoint.x);
|
||||
int minY = std::max(0, minPoint.y);
|
||||
int maxX = std::min(texture.size.x, maxPoint.x);
|
||||
int maxY = std::min(texture.size.y, maxPoint.y);
|
||||
|
||||
if (minX >= maxX || minY >= maxY) continue;
|
||||
|
||||
int contentMinX = std::numeric_limits<int>::max();
|
||||
int contentMinY = std::numeric_limits<int>::max();
|
||||
int contentMaxX = std::numeric_limits<int>::min();
|
||||
int contentMaxY = std::numeric_limits<int>::min();
|
||||
|
||||
for (int y = minY; y < maxY; y++)
|
||||
{
|
||||
for (int x = minX; x < maxX; x++)
|
||||
{
|
||||
auto index = ((size_t)y * texture.size.x + x) * resource::texture::CHANNELS;
|
||||
if (index + resource::texture::CHANNELS > texture.pixels.size()) continue;
|
||||
|
||||
auto r = texture.pixels[index + 0];
|
||||
auto g = texture.pixels[index + 1];
|
||||
auto b = texture.pixels[index + 2];
|
||||
auto a = texture.pixels[index + 3];
|
||||
if (r == 0 && g == 0 && b == 0 && a == 0) continue;
|
||||
|
||||
contentMinX = std::min(contentMinX, x);
|
||||
contentMinY = std::min(contentMinY, y);
|
||||
contentMaxX = std::max(contentMaxX, x);
|
||||
contentMaxY = std::max(contentMaxY, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentMinX == std::numeric_limits<int>::max()) continue;
|
||||
|
||||
auto newCrop = glm::vec2(contentMinX, contentMinY);
|
||||
auto newSize = glm::vec2(contentMaxX - contentMinX + 1, contentMaxY - contentMinY + 1);
|
||||
if (region.crop != newCrop || region.size != newSize)
|
||||
{
|
||||
auto previousCrop = region.crop;
|
||||
region.crop = newCrop;
|
||||
region.size = newSize;
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = {static_cast<int>(region.size.x / 2.0f), static_cast<int>(region.size.y / 2.0f)};
|
||||
else
|
||||
// Preserve the same texture-space pivot location when trimming shifts region crop.
|
||||
region.pivot -= (region.crop - previousCrop);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
std::set<int> Anm2::spritesheets_unused()
|
||||
{
|
||||
std::set<int> used{};
|
||||
for (auto& layer : content.layers | std::views::values)
|
||||
if (layer.is_spritesheet_valid()) used.insert(layer.spritesheetID);
|
||||
|
||||
std::set<int> unused{};
|
||||
for (auto& id : content.spritesheets | std::views::keys)
|
||||
if (!used.contains(id)) unused.insert(id);
|
||||
|
||||
return unused;
|
||||
}
|
||||
|
||||
bool Anm2::spritesheets_merge(const std::set<int>& ids, SpritesheetMergeOrigin mergeOrigin, bool isMakeRegions,
|
||||
bool isMakePrimaryRegion, origin::Type regionOrigin)
|
||||
{
|
||||
if (ids.size() < 2) return false;
|
||||
|
||||
auto baseId = *ids.begin();
|
||||
if (!content.spritesheets.contains(baseId)) return false;
|
||||
for (auto id : ids)
|
||||
if (!content.spritesheets.contains(id)) return false;
|
||||
|
||||
auto& base = content.spritesheets.at(baseId);
|
||||
if (!base.texture.is_valid()) return false;
|
||||
|
||||
std::unordered_map<int, glm::ivec2> offsets{};
|
||||
offsets[baseId] = {};
|
||||
|
||||
auto baseTextureSize = base.texture.size;
|
||||
auto mergedTexture = base.texture;
|
||||
for (auto id : ids)
|
||||
{
|
||||
if (id == baseId) continue;
|
||||
|
||||
auto& spritesheet = content.spritesheets.at(id);
|
||||
if (!spritesheet.texture.is_valid()) return false;
|
||||
|
||||
offsets[id] = mergeOrigin == APPEND_RIGHT ? glm::ivec2(mergedTexture.size.x, 0)
|
||||
: glm::ivec2(0, mergedTexture.size.y);
|
||||
mergedTexture = resource::Texture::merge_append(mergedTexture, spritesheet.texture,
|
||||
mergeOrigin == APPEND_RIGHT);
|
||||
}
|
||||
base.texture = std::move(mergedTexture);
|
||||
|
||||
std::unordered_map<int, std::unordered_map<int, int>> regionIdMap{};
|
||||
|
||||
if (isMakeRegions)
|
||||
{
|
||||
if (base.regionOrder.size() != base.regions.size())
|
||||
{
|
||||
base.regionOrder.clear();
|
||||
base.regionOrder.reserve(base.regions.size());
|
||||
for (auto id : base.regions | std::views::keys)
|
||||
base.regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
if (isMakePrimaryRegion)
|
||||
{
|
||||
auto baseLocationRegionID = map::next_id_get(base.regions);
|
||||
auto baseFilename = path::to_utf8(base.path.stem());
|
||||
auto baseLocationRegionName = baseFilename.empty() ? std::format("#{}", baseId) : baseFilename;
|
||||
auto baseLocationRegionPivot =
|
||||
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(baseTextureSize) * 0.5f : glm::vec2();
|
||||
base.regions[baseLocationRegionID] = {
|
||||
.name = baseLocationRegionName,
|
||||
.crop = {},
|
||||
.pivot = glm::ivec2(baseLocationRegionPivot),
|
||||
.size = baseTextureSize,
|
||||
.origin = regionOrigin,
|
||||
};
|
||||
base.regionOrder.push_back(baseLocationRegionID);
|
||||
}
|
||||
|
||||
for (auto id : ids)
|
||||
{
|
||||
if (id == baseId) continue;
|
||||
|
||||
auto& source = content.spritesheets.at(id);
|
||||
auto sheetOffset = offsets.at(id);
|
||||
|
||||
auto locationRegionID = map::next_id_get(base.regions);
|
||||
auto sourceFilename = path::to_utf8(source.path.stem());
|
||||
auto locationRegionName = sourceFilename.empty() ? std::format("#{}", id) : sourceFilename;
|
||||
auto locationRegionPivot =
|
||||
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(source.texture.size) * 0.5f : glm::vec2();
|
||||
base.regions[locationRegionID] = {
|
||||
.name = locationRegionName,
|
||||
.crop = sheetOffset,
|
||||
.pivot = glm::ivec2(locationRegionPivot),
|
||||
.size = source.texture.size,
|
||||
.origin = regionOrigin,
|
||||
};
|
||||
base.regionOrder.push_back(locationRegionID);
|
||||
|
||||
for (auto& [sourceRegionID, sourceRegion] : source.regions)
|
||||
{
|
||||
auto destinationRegionID = map::next_id_get(base.regions);
|
||||
auto destinationRegion = sourceRegion;
|
||||
destinationRegion.crop += sheetOffset;
|
||||
base.regions[destinationRegionID] = destinationRegion;
|
||||
base.regionOrder.push_back(destinationRegionID);
|
||||
regionIdMap[id][sourceRegionID] = destinationRegionID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<int, int> layerSpritesheetBefore{};
|
||||
for (auto& [layerID, layer] : content.layers)
|
||||
{
|
||||
if (!ids.contains(layer.spritesheetID)) continue;
|
||||
layerSpritesheetBefore[layerID] = layer.spritesheetID;
|
||||
layer.spritesheetID = baseId;
|
||||
}
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
{
|
||||
for (auto& [layerID, item] : animation.layerAnimations)
|
||||
{
|
||||
if (!layerSpritesheetBefore.contains(layerID)) continue;
|
||||
auto sourceSpritesheetID = layerSpritesheetBefore.at(layerID);
|
||||
if (sourceSpritesheetID == baseId) continue;
|
||||
|
||||
for (auto& frame : item.frames)
|
||||
{
|
||||
if (frame.regionID == -1) continue;
|
||||
|
||||
if (isMakeRegions && regionIdMap.contains(sourceSpritesheetID) &&
|
||||
regionIdMap.at(sourceSpritesheetID).contains(frame.regionID))
|
||||
frame.regionID = regionIdMap.at(sourceSpritesheetID).at(frame.regionID);
|
||||
else
|
||||
frame.regionID = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto id : ids)
|
||||
if (id != baseId) content.spritesheets.erase(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> Anm2::spritesheet_labels_get()
|
||||
{
|
||||
std::vector<std::string> labels{};
|
||||
for (auto& [id, spritesheet] : content.spritesheets)
|
||||
{
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
labels.emplace_back(std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<int> Anm2::spritesheet_ids_get()
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
for (auto& [id, spritesheet] : content.spritesheets)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::vector<std::string> Anm2::region_labels_get(Spritesheet& spritesheet)
|
||||
{
|
||||
auto rebuild_order = [&]()
|
||||
{
|
||||
spritesheet.regionOrder.clear();
|
||||
spritesheet.regionOrder.reserve(spritesheet.regions.size());
|
||||
for (auto id : spritesheet.regions | std::views::keys)
|
||||
spritesheet.regionOrder.push_back(id);
|
||||
};
|
||||
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
|
||||
rebuild_order();
|
||||
else
|
||||
{
|
||||
bool isOrderValid = true;
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
if (!spritesheet.regions.contains(id))
|
||||
{
|
||||
isOrderValid = false;
|
||||
break;
|
||||
}
|
||||
if (!isOrderValid) rebuild_order();
|
||||
}
|
||||
|
||||
std::vector<std::string> labels{};
|
||||
labels.emplace_back(localize.get(BASIC_NONE));
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
labels.emplace_back(spritesheet.regions.at(id).name);
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::vector<int> Anm2::region_ids_get(Spritesheet& spritesheet)
|
||||
{
|
||||
auto rebuild_order = [&]()
|
||||
{
|
||||
spritesheet.regionOrder.clear();
|
||||
spritesheet.regionOrder.reserve(spritesheet.regions.size());
|
||||
for (auto id : spritesheet.regions | std::views::keys)
|
||||
spritesheet.regionOrder.push_back(id);
|
||||
};
|
||||
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
|
||||
rebuild_order();
|
||||
else
|
||||
{
|
||||
bool isOrderValid = true;
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
if (!spritesheet.regions.contains(id))
|
||||
{
|
||||
isOrderValid = false;
|
||||
break;
|
||||
}
|
||||
if (!isOrderValid) rebuild_order();
|
||||
}
|
||||
|
||||
std::vector<int> ids{};
|
||||
ids.emplace_back(-1);
|
||||
for (auto id : spritesheet.regionOrder)
|
||||
ids.emplace_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::set<int> Anm2::regions_unused(Spritesheet& spritesheet)
|
||||
{
|
||||
std::set<int> used{};
|
||||
|
||||
for (auto& animation : animations.items)
|
||||
{
|
||||
for (auto& layerAnimation : animation.layerAnimations | std::views::values)
|
||||
{
|
||||
for (auto& frame : layerAnimation.frames)
|
||||
if (frame.regionID != -1) used.insert(frame.regionID);
|
||||
}
|
||||
}
|
||||
|
||||
std::set<int> unused{};
|
||||
for (auto& id : spritesheet.regions | std::views::keys)
|
||||
if (!used.contains(id)) unused.insert(id);
|
||||
|
||||
return unused;
|
||||
}
|
||||
|
||||
void Anm2::scan_and_set_regions()
|
||||
{
|
||||
for (auto& animation : animations.items)
|
||||
{
|
||||
for (auto& [layerID, item] : animation.layerAnimations)
|
||||
{
|
||||
auto layer = map::find(content.layers, layerID);
|
||||
if (!layer) continue;
|
||||
|
||||
auto spritesheet = spritesheet_get(layer->spritesheetID);
|
||||
if (!spritesheet || spritesheet->regions.empty()) continue;
|
||||
|
||||
for (auto& frame : item.frames)
|
||||
{
|
||||
if (frame.regionID != -1) continue;
|
||||
|
||||
auto frameCrop = glm::ivec2(frame.crop);
|
||||
auto frameSize = glm::ivec2(frame.size);
|
||||
auto framePivot = glm::ivec2(frame.pivot);
|
||||
|
||||
for (auto& [regionID, region] : spritesheet->regions)
|
||||
{
|
||||
if (glm::ivec2(region.crop) == frameCrop && glm::ivec2(region.size) == frameSize &&
|
||||
glm::ivec2(region.pivot) == framePivot)
|
||||
{
|
||||
frame.regionID = regionID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Anm2::spritesheets_deserialize(const std::string& string, const std::filesystem::path& directory,
|
||||
merge::Type type, std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
int id{};
|
||||
|
||||
if (!document.FirstChildElement("Spritesheet"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid spritesheet(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
|
||||
for (auto element = document.FirstChildElement("Spritesheet"); element;
|
||||
element = element->NextSiblingElement("Spritesheet"))
|
||||
{
|
||||
auto spritesheet = Spritesheet(element, id);
|
||||
if (type == merge::APPEND) id = map::next_id_get(content.spritesheets);
|
||||
content.spritesheets[id] = std::move(spritesheet);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "icon.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
#include <glm/glm/vec2.hpp>
|
||||
#include <glm/glm/vec3.hpp>
|
||||
#include <glm/glm/vec4.hpp>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
inline const glm::vec4 ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f);
|
||||
inline const glm::vec4 ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f);
|
||||
inline const glm::vec4 ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f);
|
||||
|
||||
inline const glm::vec4 LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f);
|
||||
inline const glm::vec4 LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f);
|
||||
inline const glm::vec4 LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f);
|
||||
|
||||
inline const glm::vec4 NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f);
|
||||
inline const glm::vec4 NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f);
|
||||
inline const glm::vec4 NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f);
|
||||
|
||||
inline const glm::vec4 TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f);
|
||||
inline const glm::vec4 TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f);
|
||||
inline const glm::vec4 TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f);
|
||||
|
||||
#define TYPE_LIST \
|
||||
X(NONE, STRING_UNDEFINED, "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \
|
||||
X(ROOT, BASIC_ROOT, "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \
|
||||
X(LAYER, BASIC_LAYER_ANIMATION, "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, \
|
||||
LAYER_COLOR_HOVERED) \
|
||||
X(NULL_, BASIC_NULL_ANIMATION, "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, \
|
||||
NULL_COLOR_HOVERED) \
|
||||
X(TRIGGER, BASIC_TRIGGERS, "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE, \
|
||||
TRIGGER_COLOR_HOVERED)
|
||||
|
||||
enum Type
|
||||
{
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) symbol,
|
||||
TYPE_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr StringType TYPE_STRINGS[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string,
|
||||
TYPE_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr const char* TYPE_ITEM_STRINGS[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString,
|
||||
TYPE_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr resource::icon::Type TYPE_ICONS[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon,
|
||||
TYPE_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
inline const glm::vec4 TYPE_COLOR[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color,
|
||||
TYPE_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
inline const glm::vec4 TYPE_COLOR_ACTIVE[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive,
|
||||
TYPE_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
inline const glm::vec4 TYPE_COLOR_HOVERED[] = {
|
||||
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered,
|
||||
TYPE_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
enum ChangeType
|
||||
{
|
||||
ADJUST,
|
||||
ADD,
|
||||
SUBTRACT,
|
||||
MULTIPLY,
|
||||
DIVIDE
|
||||
};
|
||||
|
||||
enum Compatibility
|
||||
{
|
||||
ISAAC,
|
||||
ANM2ED,
|
||||
ANM2ED_LIMITED,
|
||||
COUNT
|
||||
};
|
||||
|
||||
enum SpritesheetMergeOrigin
|
||||
{
|
||||
APPEND_RIGHT,
|
||||
APPEND_BOTTOM
|
||||
};
|
||||
|
||||
enum Flag
|
||||
{
|
||||
NO_SOUNDS = 1 << 0,
|
||||
NO_REGIONS = 1 << 1,
|
||||
FRAME_NO_REGION_VALUES = 1 << 2,
|
||||
INTERPOLATION_BOOL_ONLY = 1 << 3
|
||||
};
|
||||
|
||||
typedef int Flags;
|
||||
|
||||
inline bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; }
|
||||
|
||||
inline const std::unordered_map<Compatibility, Flags> COMPATIBILITY_FLAGS = {
|
||||
{ISAAC, NO_SOUNDS | NO_REGIONS | FRAME_NO_REGION_VALUES | INTERPOLATION_BOOL_ONLY},
|
||||
{ANM2ED, 0},
|
||||
{ANM2ED_LIMITED, FRAME_NO_REGION_VALUES}};
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
#include "content.hpp"
|
||||
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Content::Content(XMLElement* element)
|
||||
{
|
||||
int id{};
|
||||
if (auto spritesheetsElement = element->FirstChildElement("Spritesheets"))
|
||||
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
|
||||
child = child->NextSiblingElement("Spritesheet"))
|
||||
spritesheets.emplace(id, Spritesheet(child, id));
|
||||
|
||||
if (auto layersElement = element->FirstChildElement("Layers"))
|
||||
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
|
||||
layers.emplace(id, Layer(child, id));
|
||||
|
||||
if (auto nullsElement = element->FirstChildElement("Nulls"))
|
||||
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
|
||||
nulls.emplace(id, Null(child, id));
|
||||
|
||||
if (auto eventsElement = element->FirstChildElement("Events"))
|
||||
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
|
||||
events.emplace(id, Event(child, id));
|
||||
|
||||
if (auto soundsElement = element->FirstChildElement("Sounds"))
|
||||
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
|
||||
sounds.emplace(id, Sound(child, id));
|
||||
}
|
||||
|
||||
void Content::serialize(XMLDocument& document, XMLElement* parent, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Content");
|
||||
|
||||
auto spritesheetsElement = document.NewElement("Spritesheets");
|
||||
for (auto& [id, spritesheet] : spritesheets)
|
||||
spritesheet.serialize(document, spritesheetsElement, id, flags);
|
||||
element->InsertEndChild(spritesheetsElement);
|
||||
|
||||
auto layersElement = document.NewElement("Layers");
|
||||
for (auto& [id, layer] : layers)
|
||||
layer.serialize(document, layersElement, id);
|
||||
element->InsertEndChild(layersElement);
|
||||
|
||||
auto nullsElement = document.NewElement("Nulls");
|
||||
for (auto& [id, null] : nulls)
|
||||
null.serialize(document, nullsElement, id);
|
||||
element->InsertEndChild(nullsElement);
|
||||
|
||||
auto eventsElement = document.NewElement("Events");
|
||||
for (auto& [id, event] : events)
|
||||
event.serialize(document, eventsElement, id);
|
||||
element->InsertEndChild(eventsElement);
|
||||
|
||||
if (!has_flag(flags, NO_SOUNDS) && !sounds.empty())
|
||||
{
|
||||
auto soundsElement = document.NewElement("Sounds");
|
||||
for (auto& [id, sound] : sounds)
|
||||
sound.serialize(document, soundsElement, id);
|
||||
element->InsertEndChild(soundsElement);
|
||||
}
|
||||
|
||||
parent->InsertEndChild(element);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "event.hpp"
|
||||
#include "layer.hpp"
|
||||
#include "null.hpp"
|
||||
#include "sound.hpp"
|
||||
#include "spritesheet.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
struct Content
|
||||
{
|
||||
std::map<int, Spritesheet> spritesheets{};
|
||||
std::map<int, Layer> layers{};
|
||||
std::map<int, Null> nulls{};
|
||||
std::map<int, Event> events{};
|
||||
std::map<int, Sound> sounds{};
|
||||
|
||||
Content() = default;
|
||||
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Flags = 0);
|
||||
Content(tinyxml2::XMLElement*);
|
||||
};
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#include "event.hpp"
|
||||
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Event::Event(XMLElement* element, int& id)
|
||||
{
|
||||
if (!element) return;
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_string_attribute(element, "Name", &name);
|
||||
}
|
||||
|
||||
XMLElement* Event::to_element(XMLDocument& document, int id)
|
||||
{
|
||||
auto element = document.NewElement("Event");
|
||||
element->SetAttribute("Id", id);
|
||||
element->SetAttribute("Name", name.c_str());
|
||||
return element;
|
||||
}
|
||||
|
||||
void Event::serialize(XMLDocument& document, XMLElement* parent, int id)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, id));
|
||||
}
|
||||
|
||||
std::string Event::to_string(int id)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, id));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
std::string name{};
|
||||
|
||||
Event() = default;
|
||||
Event(tinyxml2::XMLElement*, int&);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
|
||||
std::string to_string(int);
|
||||
};
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
#include "frame.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "math_.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Frame::Interpolation interpolation_from_xml(const char* value, bool fallback)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
if (std::strcmp(value, "EaseIn") == 0) return Frame::Interpolation::EASE_IN;
|
||||
if (std::strcmp(value, "EaseOut") == 0) return Frame::Interpolation::EASE_OUT;
|
||||
if (std::strcmp(value, "EaseInOut") == 0) return Frame::Interpolation::EASE_IN_OUT;
|
||||
}
|
||||
return fallback ? Frame::Interpolation::LINEAR : Frame::Interpolation::NONE;
|
||||
}
|
||||
|
||||
const char* interpolation_to_xml(Frame::Interpolation interpolation)
|
||||
{
|
||||
switch (interpolation)
|
||||
{
|
||||
case Frame::Interpolation::EASE_IN:
|
||||
return "EaseIn";
|
||||
case Frame::Interpolation::EASE_OUT:
|
||||
return "EaseOut";
|
||||
case Frame::Interpolation::EASE_IN_OUT:
|
||||
return "EaseInOut";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame::Frame(XMLElement* element, Type type)
|
||||
{
|
||||
bool isInterpolatedBool{};
|
||||
const char* interpolationValue = element->Attribute("Interpolated");
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case ROOT:
|
||||
case NULL_:
|
||||
element->QueryFloatAttribute("XPosition", &position.x);
|
||||
element->QueryFloatAttribute("YPosition", &position.y);
|
||||
element->QueryFloatAttribute("XScale", &scale.x);
|
||||
element->QueryFloatAttribute("YScale", &scale.y);
|
||||
element->QueryIntAttribute("Delay", &duration);
|
||||
element->QueryBoolAttribute("Visible", &isVisible);
|
||||
xml::query_color_attribute(element, "RedTint", tint.r);
|
||||
xml::query_color_attribute(element, "GreenTint", tint.g);
|
||||
xml::query_color_attribute(element, "BlueTint", tint.b);
|
||||
xml::query_color_attribute(element, "AlphaTint", tint.a);
|
||||
xml::query_color_attribute(element, "RedOffset", colorOffset.r);
|
||||
xml::query_color_attribute(element, "GreenOffset", colorOffset.g);
|
||||
xml::query_color_attribute(element, "BlueOffset", colorOffset.b);
|
||||
element->QueryFloatAttribute("Rotation", &rotation);
|
||||
element->QueryBoolAttribute("Interpolated", &isInterpolatedBool);
|
||||
if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool);
|
||||
break;
|
||||
case LAYER:
|
||||
element->QueryIntAttribute("RegionId", ®ionID);
|
||||
element->QueryFloatAttribute("XPosition", &position.x);
|
||||
element->QueryFloatAttribute("YPosition", &position.y);
|
||||
element->QueryFloatAttribute("XPivot", &pivot.x);
|
||||
element->QueryFloatAttribute("YPivot", &pivot.y);
|
||||
element->QueryFloatAttribute("XCrop", &crop.x);
|
||||
element->QueryFloatAttribute("YCrop", &crop.y);
|
||||
element->QueryFloatAttribute("Width", &size.x);
|
||||
element->QueryFloatAttribute("Height", &size.y);
|
||||
element->QueryFloatAttribute("XScale", &scale.x);
|
||||
element->QueryFloatAttribute("YScale", &scale.y);
|
||||
element->QueryIntAttribute("Delay", &duration);
|
||||
element->QueryBoolAttribute("Visible", &isVisible);
|
||||
xml::query_color_attribute(element, "RedTint", tint.r);
|
||||
xml::query_color_attribute(element, "GreenTint", tint.g);
|
||||
xml::query_color_attribute(element, "BlueTint", tint.b);
|
||||
xml::query_color_attribute(element, "AlphaTint", tint.a);
|
||||
xml::query_color_attribute(element, "RedOffset", colorOffset.r);
|
||||
xml::query_color_attribute(element, "GreenOffset", colorOffset.g);
|
||||
xml::query_color_attribute(element, "BlueOffset", colorOffset.b);
|
||||
element->QueryFloatAttribute("Rotation", &rotation);
|
||||
element->QueryBoolAttribute("Interpolated", &isInterpolatedBool);
|
||||
if (interpolationValue) interpolation = interpolation_from_xml(interpolationValue, isInterpolatedBool);
|
||||
break;
|
||||
case TRIGGER:
|
||||
{
|
||||
element->QueryIntAttribute("EventId", &eventID);
|
||||
|
||||
int soundID{};
|
||||
// Backwards compatibility with old formats
|
||||
if (element->QueryIntAttribute("SoundId", &soundID) == XML_SUCCESS) soundIDs.push_back(soundID);
|
||||
|
||||
for (auto child = element->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
|
||||
{
|
||||
child->QueryIntAttribute("Id", &soundID);
|
||||
soundIDs.push_back(soundID);
|
||||
}
|
||||
|
||||
element->QueryIntAttribute("AtFrame", &atFrame);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XMLElement* Frame::to_element(XMLDocument& document, Type type, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame");
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case ROOT:
|
||||
case NULL_:
|
||||
element->SetAttribute("XPosition", position.x);
|
||||
element->SetAttribute("YPosition", position.y);
|
||||
element->SetAttribute("Delay", duration);
|
||||
element->SetAttribute("Visible", isVisible);
|
||||
element->SetAttribute("XScale", scale.x);
|
||||
element->SetAttribute("YScale", scale.y);
|
||||
element->SetAttribute("RedTint", math::float_to_uint8(tint.r));
|
||||
element->SetAttribute("GreenTint", math::float_to_uint8(tint.g));
|
||||
element->SetAttribute("BlueTint", math::float_to_uint8(tint.b));
|
||||
element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a));
|
||||
element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r));
|
||||
element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g));
|
||||
element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b));
|
||||
element->SetAttribute("Rotation", rotation);
|
||||
if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE ||
|
||||
interpolation == Interpolation::LINEAR)
|
||||
element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR);
|
||||
else if (const char* interpolationValue = interpolation_to_xml(interpolation))
|
||||
element->SetAttribute("Interpolated", interpolationValue);
|
||||
break;
|
||||
case LAYER:
|
||||
{
|
||||
bool noRegions = has_flag(flags, NO_REGIONS);
|
||||
bool frameNoRegionValues = has_flag(flags, FRAME_NO_REGION_VALUES);
|
||||
bool hasValidRegion = !noRegions && regionID != -1;
|
||||
bool writeRegionValues = !frameNoRegionValues || !hasValidRegion;
|
||||
|
||||
if (hasValidRegion) element->SetAttribute("RegionId", regionID);
|
||||
element->SetAttribute("XPosition", position.x);
|
||||
element->SetAttribute("YPosition", position.y);
|
||||
if (writeRegionValues)
|
||||
{
|
||||
element->SetAttribute("XPivot", pivot.x);
|
||||
element->SetAttribute("YPivot", pivot.y);
|
||||
element->SetAttribute("XCrop", crop.x);
|
||||
element->SetAttribute("YCrop", crop.y);
|
||||
element->SetAttribute("Width", size.x);
|
||||
element->SetAttribute("Height", size.y);
|
||||
}
|
||||
element->SetAttribute("XScale", scale.x);
|
||||
element->SetAttribute("YScale", scale.y);
|
||||
element->SetAttribute("Delay", duration);
|
||||
element->SetAttribute("Visible", isVisible);
|
||||
element->SetAttribute("RedTint", math::float_to_uint8(tint.r));
|
||||
element->SetAttribute("GreenTint", math::float_to_uint8(tint.g));
|
||||
element->SetAttribute("BlueTint", math::float_to_uint8(tint.b));
|
||||
element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a));
|
||||
element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r));
|
||||
element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g));
|
||||
element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b));
|
||||
element->SetAttribute("Rotation", rotation);
|
||||
if (has_flag(flags, INTERPOLATION_BOOL_ONLY) || interpolation == Interpolation::NONE ||
|
||||
interpolation == Interpolation::LINEAR)
|
||||
element->SetAttribute("Interpolated", interpolation == Interpolation::LINEAR);
|
||||
else if (const char* interpolationValue = interpolation_to_xml(interpolation))
|
||||
element->SetAttribute("Interpolated", interpolationValue);
|
||||
break;
|
||||
}
|
||||
case TRIGGER:
|
||||
if (eventID != -1) element->SetAttribute("EventId", eventID);
|
||||
|
||||
if (!has_flag(flags, NO_SOUNDS))
|
||||
for (auto& id : soundIDs)
|
||||
{
|
||||
if (id == -1) continue;
|
||||
auto soundChild = element->InsertNewChildElement("Sound");
|
||||
soundChild->SetAttribute("Id", id);
|
||||
}
|
||||
|
||||
element->SetAttribute("AtFrame", atFrame);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
void Frame::serialize(XMLDocument& document, XMLElement* parent, Type type, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, type, flags));
|
||||
}
|
||||
|
||||
std::string Frame::to_string(Type type, Flags flags)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, type, flags));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
void Frame::shorten() { duration = glm::clamp(--duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); }
|
||||
void Frame::extend() { duration = glm::clamp(++duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); }
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "anm2_type.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
constexpr auto FRAME_DURATION_MIN = 1;
|
||||
constexpr auto FRAME_DURATION_MAX = 1000000;
|
||||
|
||||
class Frame
|
||||
{
|
||||
public:
|
||||
enum Interpolation
|
||||
{
|
||||
NONE,
|
||||
LINEAR,
|
||||
EASE_IN,
|
||||
EASE_OUT,
|
||||
EASE_IN_OUT
|
||||
};
|
||||
|
||||
bool isVisible{true};
|
||||
Interpolation interpolation{NONE};
|
||||
float rotation{};
|
||||
int duration{FRAME_DURATION_MIN};
|
||||
int atFrame{-1};
|
||||
int eventID{-1};
|
||||
int regionID{-1};
|
||||
std::vector<int> soundIDs{};
|
||||
glm::vec2 pivot{};
|
||||
glm::vec2 crop{};
|
||||
glm::vec2 position{};
|
||||
glm::vec2 size{};
|
||||
glm::vec2 scale{100, 100};
|
||||
glm::vec3 colorOffset{};
|
||||
glm::vec4 tint{types::color::WHITE};
|
||||
|
||||
Frame() = default;
|
||||
Frame(tinyxml2::XMLElement*, Type);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, Flags = 0);
|
||||
std::string to_string(Type type, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, Flags = 0);
|
||||
void shorten();
|
||||
void extend();
|
||||
};
|
||||
|
||||
struct FrameChange
|
||||
{
|
||||
std::optional<bool> isVisible{};
|
||||
std::optional<Frame::Interpolation> interpolation{};
|
||||
std::optional<float> rotation{};
|
||||
std::optional<int> duration{};
|
||||
std::optional<int> regionID{};
|
||||
std::optional<float> pivotX{};
|
||||
std::optional<float> pivotY{};
|
||||
std::optional<float> cropX{};
|
||||
std::optional<float> cropY{};
|
||||
std::optional<float> positionX{};
|
||||
std::optional<float> positionY{};
|
||||
std::optional<float> sizeX{};
|
||||
std::optional<float> sizeY{};
|
||||
std::optional<float> scaleX{};
|
||||
std::optional<float> scaleY{};
|
||||
std::optional<float> colorOffsetR{};
|
||||
std::optional<float> colorOffsetG{};
|
||||
std::optional<float> colorOffsetB{};
|
||||
std::optional<float> tintR{};
|
||||
std::optional<float> tintG{};
|
||||
std::optional<float> tintB{};
|
||||
std::optional<float> tintA{};
|
||||
std::optional<bool> isFlipX{};
|
||||
std::optional<bool> isFlipY{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#include "info.hpp"
|
||||
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Info::Info(XMLElement* element)
|
||||
{
|
||||
if (!element) return;
|
||||
xml::query_string_attribute(element, "CreatedBy", &createdBy);
|
||||
xml::query_string_attribute(element, "CreatedOn", &createdOn);
|
||||
element->QueryIntAttribute("Fps", &fps);
|
||||
element->QueryIntAttribute("Version", &version);
|
||||
}
|
||||
|
||||
XMLElement* Info::to_element(XMLDocument& document)
|
||||
{
|
||||
auto element = document.NewElement("Info");
|
||||
element->SetAttribute("CreatedBy", createdBy.c_str());
|
||||
element->SetAttribute("CreatedOn", createdOn.c_str());
|
||||
element->SetAttribute("Fps", fps);
|
||||
element->SetAttribute("Version", version);
|
||||
return element;
|
||||
}
|
||||
|
||||
void Info::serialize(XMLDocument& document, XMLElement* parent)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document));
|
||||
}
|
||||
|
||||
std::string Info::to_string()
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
constexpr auto FPS_MIN = 1;
|
||||
constexpr auto FPS_MAX = 120;
|
||||
|
||||
class Info
|
||||
{
|
||||
public:
|
||||
std::string createdBy{"robot"};
|
||||
std::string createdOn{};
|
||||
int fps = 30;
|
||||
int version{};
|
||||
|
||||
Info() = default;
|
||||
Info(tinyxml2::XMLElement*);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument& document);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
|
||||
std::string to_string();
|
||||
};
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
#include "item.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ranges>
|
||||
|
||||
#include "vector_.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
float interpolation_factor(Frame::Interpolation interpolation, float value)
|
||||
{
|
||||
value = glm::clamp(value, 0.0f, 1.0f);
|
||||
|
||||
switch (interpolation)
|
||||
{
|
||||
case Frame::Interpolation::LINEAR:
|
||||
return value;
|
||||
case Frame::Interpolation::EASE_IN:
|
||||
return value * value;
|
||||
case Frame::Interpolation::EASE_OUT:
|
||||
return 1.0f - ((1.0f - value) * (1.0f - value));
|
||||
case Frame::Interpolation::EASE_IN_OUT:
|
||||
return value < 0.5f ? (2.0f * value * value) : (1.0f - std::pow(-2.0f * value + 2.0f, 2.0f) * 0.5f);
|
||||
case Frame::Interpolation::NONE:
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item::Item(XMLElement* element, Type type, int* id)
|
||||
{
|
||||
if (type == LAYER && id) element->QueryIntAttribute("LayerId", id);
|
||||
if (type == NULL_ && id) element->QueryIntAttribute("NullId", id);
|
||||
|
||||
element->QueryBoolAttribute("Visible", &isVisible);
|
||||
|
||||
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
|
||||
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
|
||||
frames.push_back(Frame(child, type));
|
||||
}
|
||||
|
||||
XMLElement* Item::to_element(XMLDocument& document, Type type, int id, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement(TYPE_ITEM_STRINGS[type]);
|
||||
|
||||
if (type == LAYER) element->SetAttribute("LayerId", id);
|
||||
if (type == NULL_) element->SetAttribute("NullId", id);
|
||||
if (type == LAYER || type == NULL_) element->SetAttribute("Visible", isVisible);
|
||||
|
||||
if (type == TRIGGER) frames_sort_by_at_frame();
|
||||
|
||||
for (auto& frame : frames)
|
||||
frame.serialize(document, element, type, flags);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
void Item::serialize(XMLDocument& document, XMLElement* parent, Type type, int id, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, type, id, flags));
|
||||
}
|
||||
|
||||
std::string Item::to_string(Type type, int id, Flags flags)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, type, id, flags));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
int Item::length(Type type)
|
||||
{
|
||||
int length{};
|
||||
|
||||
if (type == TRIGGER)
|
||||
for (auto& frame : frames)
|
||||
length = frame.atFrame > length ? frame.atFrame : length;
|
||||
else
|
||||
for (auto& frame : frames)
|
||||
length += frame.duration;
|
||||
|
||||
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 frame{};
|
||||
frame.isVisible = false;
|
||||
|
||||
if (frames.empty()) return frame;
|
||||
|
||||
time = time < 0.0f ? 0.0f : time;
|
||||
|
||||
Frame* frameNext = nullptr;
|
||||
int durationCurrent = 0;
|
||||
int durationNext = 0;
|
||||
|
||||
for (auto [i, iFrame] : std::views::enumerate(frames))
|
||||
{
|
||||
if (type == TRIGGER)
|
||||
{
|
||||
if ((int)time == iFrame.atFrame)
|
||||
{
|
||||
frame = iFrame;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
frame = iFrame;
|
||||
|
||||
durationNext += frame.duration;
|
||||
|
||||
if (time >= durationCurrent && time < durationNext)
|
||||
{
|
||||
if (i + 1 < (int)frames.size())
|
||||
frameNext = &frames[i + 1];
|
||||
else
|
||||
frameNext = nullptr;
|
||||
break;
|
||||
}
|
||||
|
||||
durationCurrent += frame.duration;
|
||||
}
|
||||
}
|
||||
|
||||
if (type != TRIGGER && frame.interpolation != Frame::Interpolation::NONE && frameNext && frame.duration > 1)
|
||||
{
|
||||
auto interpolation =
|
||||
interpolation_factor(frame.interpolation, (time - durationCurrent) / (durationNext - durationCurrent));
|
||||
|
||||
frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation);
|
||||
frame.position = glm::mix(frame.position, frameNext->position, interpolation);
|
||||
frame.scale = glm::mix(frame.scale, frameNext->scale, interpolation);
|
||||
frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, interpolation);
|
||||
frame.tint = glm::mix(frame.tint, frameNext->tint, interpolation);
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void Item::frames_change(FrameChange change, anm2::Type itemType, ChangeType changeType, std::set<int>& selection)
|
||||
{
|
||||
const auto clamp_identity = [](auto value) { return value; };
|
||||
const auto clamp01 = [](auto value) { return glm::clamp(value, 0.0f, 1.0f); };
|
||||
const auto clamp_duration = [](int value) { return std::max(FRAME_DURATION_MIN, value); };
|
||||
|
||||
if (selection.empty()) return;
|
||||
|
||||
auto apply_scalar_with_clamp = [&](auto& target, const auto& optionalValue, auto clampFunc)
|
||||
{
|
||||
if (!optionalValue) return;
|
||||
auto value = *optionalValue;
|
||||
|
||||
switch (changeType)
|
||||
{
|
||||
case ADJUST:
|
||||
target = clampFunc(value);
|
||||
break;
|
||||
case ADD:
|
||||
target = clampFunc(target + value);
|
||||
break;
|
||||
case SUBTRACT:
|
||||
target = clampFunc(target - value);
|
||||
break;
|
||||
case MULTIPLY:
|
||||
target = clampFunc(target * value);
|
||||
break;
|
||||
case DIVIDE:
|
||||
if (value == decltype(value){}) return;
|
||||
target = clampFunc(target / value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
auto apply_scalar = [&](auto& target, const auto& optionalValue)
|
||||
{ apply_scalar_with_clamp(target, optionalValue, clamp_identity); };
|
||||
|
||||
for (auto i : selection)
|
||||
{
|
||||
if (!vector::in_bounds(frames, i)) continue;
|
||||
Frame& frame = frames[i];
|
||||
|
||||
if (change.isVisible) frame.isVisible = *change.isVisible;
|
||||
if (change.interpolation) frame.interpolation = *change.interpolation;
|
||||
if (change.isFlipX) frame.scale.x = -frame.scale.x;
|
||||
if (change.isFlipY) frame.scale.y = -frame.scale.y;
|
||||
|
||||
apply_scalar(frame.rotation, change.rotation);
|
||||
apply_scalar_with_clamp(frame.duration, change.duration, clamp_duration);
|
||||
|
||||
if (itemType == LAYER)
|
||||
{
|
||||
apply_scalar(frame.crop.x, change.cropX);
|
||||
apply_scalar(frame.crop.y, change.cropY);
|
||||
|
||||
apply_scalar(frame.pivot.x, change.pivotX);
|
||||
apply_scalar(frame.pivot.y, change.pivotY);
|
||||
|
||||
apply_scalar(frame.size.x, change.sizeX);
|
||||
apply_scalar(frame.size.y, change.sizeY);
|
||||
|
||||
if (change.regionID) frame.regionID = *change.regionID;
|
||||
}
|
||||
|
||||
apply_scalar(frame.position.x, change.positionX);
|
||||
apply_scalar(frame.position.y, change.positionY);
|
||||
|
||||
apply_scalar(frame.scale.x, change.scaleX);
|
||||
apply_scalar(frame.scale.y, change.scaleY);
|
||||
|
||||
apply_scalar_with_clamp(frame.colorOffset.x, change.colorOffsetR, clamp01);
|
||||
apply_scalar_with_clamp(frame.colorOffset.y, change.colorOffsetG, clamp01);
|
||||
apply_scalar_with_clamp(frame.colorOffset.z, change.colorOffsetB, clamp01);
|
||||
|
||||
apply_scalar_with_clamp(frame.tint.x, change.tintR, clamp01);
|
||||
apply_scalar_with_clamp(frame.tint.y, change.tintG, clamp01);
|
||||
apply_scalar_with_clamp(frame.tint.z, change.tintB, clamp01);
|
||||
apply_scalar_with_clamp(frame.tint.w, change.tintA, clamp01);
|
||||
}
|
||||
}
|
||||
|
||||
bool Item::frames_deserialize(const std::string& string, Type type, int start, std::set<int>& indices,
|
||||
std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
int count{};
|
||||
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;
|
||||
frames.insert(frames.begin() + start + count, Frame(element, type));
|
||||
indices.insert(index);
|
||||
count++;
|
||||
}
|
||||
|
||||
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)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Item::frames_bake(int index, int interval, bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
if (!vector::in_bounds(frames, index)) return;
|
||||
|
||||
auto original = frames[index];
|
||||
if (original.duration == FRAME_DURATION_MIN)
|
||||
{
|
||||
frames[index].interpolation = Frame::Interpolation::NONE;
|
||||
return;
|
||||
}
|
||||
|
||||
auto nextFrame = vector::in_bounds(frames, index + 1) ? frames[index + 1] : original;
|
||||
|
||||
int duration{};
|
||||
int i = index;
|
||||
|
||||
while (duration < original.duration)
|
||||
{
|
||||
Frame baked = original;
|
||||
float interpolation = interpolation_factor(original.interpolation, (float)duration / original.duration);
|
||||
baked.duration = std::min(interval, original.duration - duration);
|
||||
baked.interpolation = Frame::Interpolation::NONE;
|
||||
baked.rotation = glm::mix(original.rotation, nextFrame.rotation, interpolation);
|
||||
baked.position = glm::mix(original.position, nextFrame.position, interpolation);
|
||||
baked.scale = glm::mix(original.scale, nextFrame.scale, interpolation);
|
||||
baked.colorOffset = glm::mix(original.colorOffset, nextFrame.colorOffset, interpolation);
|
||||
baked.tint = glm::mix(original.tint, nextFrame.tint, interpolation);
|
||||
if (isRoundScale) baked.scale = vec2(ivec2(baked.scale));
|
||||
if (isRoundRotation) baked.rotation = (int)baked.rotation;
|
||||
|
||||
if (i == index)
|
||||
frames[i] = baked;
|
||||
else
|
||||
frames.insert(frames.begin() + i, baked);
|
||||
i++;
|
||||
|
||||
duration += baked.duration;
|
||||
}
|
||||
}
|
||||
|
||||
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++)
|
||||
{
|
||||
Frame frame{};
|
||||
frame.duration = duration;
|
||||
frame.pivot = pivot;
|
||||
frame.size = size;
|
||||
frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
float Item::frame_time_from_index_get(int index)
|
||||
{
|
||||
if (!vector::in_bounds(frames, index)) return 0.0f;
|
||||
|
||||
float time{};
|
||||
for (auto [i, frame] : std::views::enumerate(frames))
|
||||
{
|
||||
if (i == index) return time;
|
||||
time += frame.duration;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
int Item::frame_index_from_time_get(float time)
|
||||
{
|
||||
if (frames.empty()) return -1;
|
||||
if (time <= 0.0f) return 0;
|
||||
|
||||
float duration{};
|
||||
for (auto [i, frame] : std::views::enumerate(frames))
|
||||
{
|
||||
duration += frame.duration;
|
||||
if (time < duration) return (int)i;
|
||||
}
|
||||
|
||||
return (int)frames.size() - 1;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "frame.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
class Item
|
||||
{
|
||||
public:
|
||||
std::vector<Frame> frames{};
|
||||
bool isVisible{true};
|
||||
|
||||
Item() = default;
|
||||
Item(tinyxml2::XMLElement*, Type, int* = nullptr);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type, int, Flags = 0);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1, Flags = 0);
|
||||
std::string to_string(Type, int = -1, Flags = 0);
|
||||
int length(Type);
|
||||
Frame frame_generate(float, Type);
|
||||
void frames_change(FrameChange, anm2::Type, ChangeType, std::set<int>&);
|
||||
bool frames_deserialize(const std::string&, Type, int, std::set<int>&, std::string*);
|
||||
void frames_bake(int, int, bool, bool);
|
||||
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);
|
||||
int frame_index_from_time_get(float);
|
||||
float frame_time_from_index_get(int);
|
||||
};
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#include "layer.hpp"
|
||||
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Layer::Layer(XMLElement* element, int& id)
|
||||
{
|
||||
if (!element) return;
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_string_attribute(element, "Name", &name);
|
||||
element->QueryIntAttribute("SpritesheetId", &spritesheetID);
|
||||
}
|
||||
|
||||
XMLElement* Layer::to_element(XMLDocument& document, int id)
|
||||
{
|
||||
auto element = document.NewElement("Layer");
|
||||
element->SetAttribute("Id", id);
|
||||
element->SetAttribute("Name", name.c_str());
|
||||
element->SetAttribute("SpritesheetId", spritesheetID);
|
||||
return element;
|
||||
}
|
||||
|
||||
void Layer::serialize(XMLDocument& document, XMLElement* parent, int id)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, id));
|
||||
}
|
||||
|
||||
std::string Layer::to_string(int id)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, id));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
bool Layer::is_spritesheet_valid()
|
||||
{
|
||||
return spritesheetID > -1;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
class Layer
|
||||
{
|
||||
public:
|
||||
std::string name{};
|
||||
int spritesheetID{};
|
||||
|
||||
Layer() = default;
|
||||
Layer(tinyxml2::XMLElement*, int&);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
|
||||
std::string to_string(int);
|
||||
bool is_spritesheet_valid();
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#include "null.hpp"
|
||||
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Null::Null(XMLElement* element, int& id)
|
||||
{
|
||||
if (!element) return;
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_string_attribute(element, "Name", &name);
|
||||
element->QueryBoolAttribute("ShowRect", &isShowRect);
|
||||
}
|
||||
|
||||
XMLElement* Null::to_element(XMLDocument& document, int id)
|
||||
{
|
||||
auto element = document.NewElement("Null");
|
||||
element->SetAttribute("Id", id);
|
||||
element->SetAttribute("Name", name.c_str());
|
||||
if (isShowRect) element->SetAttribute("ShowRect", isShowRect);
|
||||
return element;
|
||||
}
|
||||
|
||||
void Null::serialize(XMLDocument& document, XMLElement* parent, int id)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, id));
|
||||
}
|
||||
|
||||
std::string Null::to_string(int id)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, id));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
class Null
|
||||
{
|
||||
public:
|
||||
std::string name{};
|
||||
bool isShowRect{};
|
||||
|
||||
Null() = default;
|
||||
Null(tinyxml2::XMLElement*, int&);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument& document, int id);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
|
||||
std::string to_string(int);
|
||||
};
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
#include "sound.hpp"
|
||||
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
Sound::Sound(const Sound& other) : path(other.path), audio(other.audio) {}
|
||||
|
||||
Sound& Sound::operator=(const Sound& other)
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
path = other.path;
|
||||
audio = other.audio;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Sound::Sound(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
this->path = path::lower_case_backslash_handle(this->path);
|
||||
audio = Audio(this->path);
|
||||
}
|
||||
|
||||
Sound::Sound(XMLElement* element, int& id)
|
||||
{
|
||||
if (!element) return;
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_path_attribute(element, "Path", &path);
|
||||
path = path::lower_case_backslash_handle(path);
|
||||
audio = Audio(path);
|
||||
}
|
||||
|
||||
XMLElement* Sound::to_element(XMLDocument& document, int id)
|
||||
{
|
||||
auto element = document.NewElement("Sound");
|
||||
element->SetAttribute("Id", id);
|
||||
auto pathString = path::to_utf8(path);
|
||||
element->SetAttribute("Path", pathString.c_str());
|
||||
return element;
|
||||
}
|
||||
|
||||
void Sound::serialize(XMLDocument& document, XMLElement* parent, int id)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, id));
|
||||
}
|
||||
|
||||
std::string Sound::to_string(int id)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, id));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
void Sound::reload(const std::filesystem::path& directory) { *this = Sound(directory, this->path); }
|
||||
bool Sound::is_valid() { return audio.is_valid(); }
|
||||
void Sound::play() { audio.play(); }
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "audio.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
class Sound
|
||||
{
|
||||
public:
|
||||
std::filesystem::path path{};
|
||||
resource::Audio audio{};
|
||||
|
||||
Sound() = default;
|
||||
Sound(Sound&&) noexcept = default;
|
||||
Sound& operator=(Sound&&) noexcept = default;
|
||||
|
||||
Sound(const Sound&);
|
||||
Sound& operator=(const Sound&);
|
||||
Sound(tinyxml2::XMLElement*, int&);
|
||||
Sound(const std::filesystem::path&, const std::filesystem::path&);
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
|
||||
std::string to_string(int);
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
|
||||
void reload(const std::filesystem::path&);
|
||||
bool is_valid();
|
||||
void play();
|
||||
};
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
#include "spritesheet.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <ranges>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "map_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "working_directory.hpp"
|
||||
#include "xml_.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::types;
|
||||
using namespace tinyxml2;
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const char* origin_to_string(Spritesheet::Region::Origin origin)
|
||||
{
|
||||
switch (origin)
|
||||
{
|
||||
case Spritesheet::Region::TOP_LEFT:
|
||||
return "TopLeft";
|
||||
case Spritesheet::Region::ORIGIN_CENTER:
|
||||
return "Center";
|
||||
case Spritesheet::Region::CUSTOM:
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Spritesheet::Region::Origin origin_from_string(const char* originString)
|
||||
{
|
||||
if (!originString) return Spritesheet::Region::CUSTOM;
|
||||
if (std::string(originString) == "TopLeft") return Spritesheet::Region::TOP_LEFT;
|
||||
if (std::string(originString) == "Center") return Spritesheet::Region::ORIGIN_CENTER;
|
||||
return Spritesheet::Region::CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
Spritesheet::Spritesheet(XMLElement* element, int& id)
|
||||
{
|
||||
if (!element) return;
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_path_attribute(element, "Path", &path);
|
||||
// Spritesheet paths from Isaac Rebirth are made with the assumption that paths are case-insensitive
|
||||
// However when using the resource dumper, the spritesheet paths are all lowercase (on Linux anyway)
|
||||
// This will handle this case and make the paths OS-agnostic
|
||||
path = path::lower_case_backslash_handle(path);
|
||||
texture = Texture(path);
|
||||
|
||||
regionOrder.clear();
|
||||
for (auto child = element->FirstChildElement("Region"); child; child = child->NextSiblingElement("Region"))
|
||||
{
|
||||
Region region{};
|
||||
int id{};
|
||||
child->QueryIntAttribute("Id", &id);
|
||||
xml::query_string_attribute(child, "Name", ®ion.name);
|
||||
child->QueryFloatAttribute("XCrop", ®ion.crop.x);
|
||||
child->QueryFloatAttribute("YCrop", ®ion.crop.y);
|
||||
child->QueryFloatAttribute("Width", ®ion.size.x);
|
||||
child->QueryFloatAttribute("Height", ®ion.size.y);
|
||||
region.origin = origin_from_string(child->Attribute("Origin"));
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)};
|
||||
else
|
||||
{
|
||||
child->QueryFloatAttribute("XPivot", ®ion.pivot.x);
|
||||
child->QueryFloatAttribute("YPivot", ®ion.pivot.y);
|
||||
}
|
||||
regions.emplace(id, std::move(region));
|
||||
regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
if (regionOrder.size() != regions.size())
|
||||
{
|
||||
regionOrder.clear();
|
||||
regionOrder.reserve(regions.size());
|
||||
for (auto id : regions | std::views::keys)
|
||||
regionOrder.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
Spritesheet::Spritesheet(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
auto loadPath = !path.empty() ? path::lower_case_backslash_handle(path) : this->path;
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
this->path = path::lower_case_backslash_handle(this->path);
|
||||
texture = Texture(!loadPath.empty() ? loadPath : this->path);
|
||||
}
|
||||
|
||||
XMLElement* Spritesheet::to_element(XMLDocument& document, int id, Flags flags)
|
||||
{
|
||||
auto element = document.NewElement("Spritesheet");
|
||||
element->SetAttribute("Id", id);
|
||||
auto pathString = path::to_utf8(path);
|
||||
element->SetAttribute("Path", pathString.c_str());
|
||||
|
||||
if (!has_flag(flags, NO_REGIONS))
|
||||
{
|
||||
if (regionOrder.size() != regions.size())
|
||||
{
|
||||
regionOrder.clear();
|
||||
regionOrder.reserve(regions.size());
|
||||
for (auto id : regions | std::views::keys)
|
||||
regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
for (auto id : regionOrder)
|
||||
{
|
||||
if (!regions.contains(id)) continue;
|
||||
auto& region = regions.at(id);
|
||||
auto regionElement = element->InsertNewChildElement("Region");
|
||||
regionElement->SetAttribute("Id", id);
|
||||
regionElement->SetAttribute("Name", region.name.c_str());
|
||||
regionElement->SetAttribute("XCrop", region.crop.x);
|
||||
regionElement->SetAttribute("YCrop", region.crop.y);
|
||||
regionElement->SetAttribute("Width", region.size.x);
|
||||
regionElement->SetAttribute("Height", region.size.y);
|
||||
if (auto originString = origin_to_string(region.origin); originString)
|
||||
regionElement->SetAttribute("Origin", originString);
|
||||
else
|
||||
{
|
||||
regionElement->SetAttribute("XPivot", region.pivot.x);
|
||||
regionElement->SetAttribute("YPivot", region.pivot.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
void Spritesheet::serialize(XMLDocument& document, XMLElement* parent, int id, Flags flags)
|
||||
{
|
||||
parent->InsertEndChild(to_element(document, id, flags));
|
||||
}
|
||||
|
||||
std::string Spritesheet::to_string(int id)
|
||||
{
|
||||
XMLDocument document{};
|
||||
document.InsertEndChild(to_element(document, id));
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
std::string Spritesheet::region_to_string(int id)
|
||||
{
|
||||
if (!regions.contains(id)) return {};
|
||||
|
||||
XMLDocument document{};
|
||||
auto element = document.NewElement("Region");
|
||||
auto& region = regions.at(id);
|
||||
element->SetAttribute("Id", id);
|
||||
element->SetAttribute("Name", region.name.c_str());
|
||||
element->SetAttribute("XCrop", region.crop.x);
|
||||
element->SetAttribute("YCrop", region.crop.y);
|
||||
element->SetAttribute("Width", region.size.x);
|
||||
element->SetAttribute("Height", region.size.y);
|
||||
if (auto originString = origin_to_string(region.origin); originString)
|
||||
element->SetAttribute("Origin", originString);
|
||||
else
|
||||
{
|
||||
element->SetAttribute("XPivot", region.pivot.x);
|
||||
element->SetAttribute("YPivot", region.pivot.y);
|
||||
}
|
||||
document.InsertEndChild(element);
|
||||
|
||||
return xml::document_to_string(document);
|
||||
}
|
||||
|
||||
bool Spritesheet::regions_deserialize(const std::string& string, merge::Type type, std::string* errorString)
|
||||
{
|
||||
XMLDocument document{};
|
||||
|
||||
if (document.Parse(string.c_str()) == XML_SUCCESS)
|
||||
{
|
||||
int id{};
|
||||
|
||||
if (!document.FirstChildElement("Region"))
|
||||
{
|
||||
if (errorString) *errorString = "No valid region(s).";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto element = document.FirstChildElement("Region"); element;
|
||||
element = element->NextSiblingElement("Region"))
|
||||
{
|
||||
Region region{};
|
||||
element->QueryIntAttribute("Id", &id);
|
||||
xml::query_string_attribute(element, "Name", ®ion.name);
|
||||
element->QueryFloatAttribute("XCrop", ®ion.crop.x);
|
||||
element->QueryFloatAttribute("YCrop", ®ion.crop.y);
|
||||
element->QueryFloatAttribute("Width", ®ion.size.x);
|
||||
element->QueryFloatAttribute("Height", ®ion.size.y);
|
||||
region.origin = origin_from_string(element->Attribute("Origin"));
|
||||
if (region.origin == Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = glm::ivec2(region.size / 2.0f);
|
||||
else
|
||||
{
|
||||
element->QueryFloatAttribute("XPivot", ®ion.pivot.x);
|
||||
element->QueryFloatAttribute("YPivot", ®ion.pivot.y);
|
||||
}
|
||||
|
||||
if (type == merge::APPEND) id = map::next_id_get(regions);
|
||||
regions[id] = std::move(region);
|
||||
if (std::find(regionOrder.begin(), regionOrder.end(), id) == regionOrder.end()) regionOrder.push_back(id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (errorString)
|
||||
*errorString = document.ErrorStr();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Spritesheet::save(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
if (this->path.empty()) return false;
|
||||
path::ensure_directory(this->path.parent_path());
|
||||
return texture.write_png(this->path);
|
||||
}
|
||||
|
||||
void Spritesheet::reload(const std::filesystem::path& directory, const std::filesystem::path& path)
|
||||
{
|
||||
WorkingDirectory workingDirectory(directory);
|
||||
auto loadPath = !path.empty() ? path::lower_case_backslash_handle(path) : this->path;
|
||||
this->path = !path.empty() ? path::make_relative(path) : this->path;
|
||||
this->path = path::lower_case_backslash_handle(this->path);
|
||||
texture = Texture(!loadPath.empty() ? loadPath : this->path);
|
||||
}
|
||||
bool Spritesheet::is_valid() { return texture.is_valid(); }
|
||||
|
||||
uint64_t Spritesheet::hash() const
|
||||
{
|
||||
auto hash_combine = [](std::size_t& seed, std::size_t value)
|
||||
{
|
||||
seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);
|
||||
};
|
||||
|
||||
std::size_t seed{};
|
||||
hash_combine(seed, std::hash<int>{}(texture.size.x));
|
||||
hash_combine(seed, std::hash<int>{}(texture.size.y));
|
||||
hash_combine(seed, std::hash<int>{}(texture.channels));
|
||||
hash_combine(seed, std::hash<int>{}(texture.filter));
|
||||
hash_combine(seed, std::hash<std::string>{}(path::to_utf8(path)));
|
||||
|
||||
if (!texture.pixels.empty())
|
||||
{
|
||||
std::string_view bytes(reinterpret_cast<const char*>(texture.pixels.data()), texture.pixels.size());
|
||||
hash_combine(seed, std::hash<std::string_view>{}(bytes));
|
||||
}
|
||||
else
|
||||
{
|
||||
hash_combine(seed, 0);
|
||||
}
|
||||
|
||||
return static_cast<uint64_t>(seed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <tinyxml2/tinyxml2.h>
|
||||
|
||||
#include "texture.hpp"
|
||||
#include "anm2_type.hpp"
|
||||
#include "types.hpp"
|
||||
#include "origin.hpp"
|
||||
|
||||
namespace anm2ed::anm2
|
||||
{
|
||||
class Spritesheet
|
||||
{
|
||||
public:
|
||||
struct Region
|
||||
{
|
||||
using Origin = origin::Type;
|
||||
static constexpr Origin TOP_LEFT = origin::TOP_LEFT;
|
||||
static constexpr Origin ORIGIN_CENTER = origin::ORIGIN_CENTER;
|
||||
static constexpr Origin CUSTOM = origin::CUSTOM;
|
||||
|
||||
std::string name{};
|
||||
glm::vec2 crop{};
|
||||
glm::vec2 pivot{};
|
||||
glm::vec2 size{};
|
||||
Origin origin{CUSTOM};
|
||||
};
|
||||
|
||||
std::filesystem::path path{};
|
||||
resource::Texture texture;
|
||||
|
||||
std::map<int, Region> regions{};
|
||||
std::vector<int> regionOrder{};
|
||||
|
||||
Spritesheet() = default;
|
||||
Spritesheet(tinyxml2::XMLElement*, int&);
|
||||
Spritesheet(const std::filesystem::path&, const std::filesystem::path& = {});
|
||||
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int, Flags = 0);
|
||||
std::string to_string(int id);
|
||||
std::string region_to_string(int id);
|
||||
bool regions_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
|
||||
bool save(const std::filesystem::path&, const std::filesystem::path& = {});
|
||||
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int, Flags = 0);
|
||||
void reload(const std::filesystem::path&, const std::filesystem::path& = {});
|
||||
bool is_valid();
|
||||
uint64_t hash() const;
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
#include <glm/gtc/matrix_inverse.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include "math_.hpp"
|
||||
#include "math.hpp"
|
||||
|
||||
using namespace glm;
|
||||
using namespace anm2ed::resource;
|
||||
|
||||
+15
-5
@@ -13,7 +13,7 @@
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
|
||||
@@ -25,13 +25,17 @@ namespace anm2ed
|
||||
|
||||
if (filelist && filelist[0] && strlen(filelist[0]) > 0)
|
||||
{
|
||||
self->path = path::from_utf8(filelist[0]);
|
||||
self->paths.clear();
|
||||
for (int i = 0; filelist[i] && strlen(filelist[i]) > 0; ++i)
|
||||
self->paths.push_back(path::from_utf8(filelist[i]));
|
||||
self->path = self->paths.empty() ? std::filesystem::path{} : self->paths.front();
|
||||
self->selectedFilter = filter;
|
||||
}
|
||||
else
|
||||
{
|
||||
self->selectedFilter = -1;
|
||||
self->path.clear();
|
||||
self->paths.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +45,21 @@ namespace anm2ed
|
||||
this->window = window;
|
||||
}
|
||||
|
||||
void Dialog::file_open(Type type)
|
||||
void Dialog::file_open(Type type, bool isMany)
|
||||
{
|
||||
if (type == Dialog::NONE) return;
|
||||
path.clear();
|
||||
paths.clear();
|
||||
SDL_ShowOpenFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
|
||||
nullptr, false);
|
||||
nullptr, isMany);
|
||||
this->type = type;
|
||||
}
|
||||
|
||||
void Dialog::file_save(Type type)
|
||||
{
|
||||
if (type == Dialog::NONE) return;
|
||||
path.clear();
|
||||
paths.clear();
|
||||
SDL_ShowSaveFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]),
|
||||
nullptr);
|
||||
this->type = type;
|
||||
@@ -60,6 +68,8 @@ namespace anm2ed
|
||||
void Dialog::folder_open(Type type)
|
||||
{
|
||||
if (type == Dialog::NONE) return;
|
||||
path.clear();
|
||||
paths.clear();
|
||||
SDL_ShowOpenFolderDialog(callback, this, window, nullptr, false);
|
||||
this->type = type;
|
||||
}
|
||||
@@ -80,6 +90,6 @@ namespace anm2ed
|
||||
|
||||
void Dialog::reset() { *this = Dialog(this->window); }
|
||||
|
||||
bool Dialog::is_selected(Type type) const { return this->type == type && !path.empty(); }
|
||||
bool Dialog::is_selected(Type type) const { return this->type == type && !paths.empty(); }
|
||||
|
||||
};
|
||||
|
||||
+4
-2
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@@ -42,7 +43,7 @@ namespace anm2ed
|
||||
|
||||
#define DIALOG_LIST \
|
||||
X(NONE, NO_FILTER) \
|
||||
X(ANM2_NEW, ANM2) \
|
||||
X(ANM2_CREATE, ANM2) \
|
||||
X(ANM2_OPEN, ANM2) \
|
||||
X(ANM2_SAVE, ANM2) \
|
||||
X(SOUND_OPEN, SOUND) \
|
||||
@@ -74,12 +75,13 @@ namespace anm2ed
|
||||
|
||||
SDL_Window* window{};
|
||||
std::filesystem::path path{};
|
||||
std::vector<std::filesystem::path> paths{};
|
||||
Type type{NONE};
|
||||
int selectedFilter{-1};
|
||||
|
||||
Dialog() = default;
|
||||
Dialog(SDL_Window*);
|
||||
void file_open(Type type);
|
||||
void file_open(Type type, bool isMany = false);
|
||||
void file_save(Type type);
|
||||
void folder_open(Type type);
|
||||
bool is_selected(Type type) const;
|
||||
|
||||
+1328
-183
File diff suppressed because it is too large
Load Diff
+34
-9
@@ -2,14 +2,21 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "snapshots.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "origin.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
class Manager;
|
||||
struct Command;
|
||||
|
||||
class Document
|
||||
{
|
||||
@@ -46,8 +53,10 @@ namespace anm2ed
|
||||
Storage& region = current.region;
|
||||
Storage& sound = current.sound;
|
||||
Storage& spritesheet = current.spritesheet;
|
||||
anm2::Anm2& anm2 = current.anm2;
|
||||
anm2::Reference& reference = current.reference;
|
||||
std::map<int, resource::Texture>& textures = current.textures;
|
||||
std::map<int, resource::Audio>& sounds = current.sounds;
|
||||
Anm2& anm2 = current.anm2;
|
||||
Reference& reference = current.reference;
|
||||
float& frameTime = current.frameTime;
|
||||
std::string& message = current.message;
|
||||
std::map<int, Storage> regionBySpritesheet{};
|
||||
@@ -68,17 +77,30 @@ namespace anm2ed
|
||||
bool isForceDirty{false};
|
||||
std::unordered_map<int, uint64_t> spritesheetHashes{};
|
||||
std::unordered_map<int, uint64_t> spritesheetSaveHashes{};
|
||||
std::unordered_map<int, std::filesystem::path> texturePaths{};
|
||||
std::unordered_map<int, std::filesystem::path> soundPaths{};
|
||||
bool isAnimationPreviewSet{false};
|
||||
bool isSpritesheetEditorSet{false};
|
||||
|
||||
Document(anm2::Anm2& anm2, const std::filesystem::path&);
|
||||
Document(const std::filesystem::path&, bool = false, std::string* = nullptr);
|
||||
Document(const Document&) = delete;
|
||||
Document& operator=(const Document&) = delete;
|
||||
Document(Document&&) noexcept;
|
||||
Document& operator=(Document&&) noexcept;
|
||||
bool save(const std::filesystem::path& = {}, std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED,
|
||||
bool save(const std::filesystem::path& = {}, std::string* = nullptr, Compatibility = Compatibility::ANM2ED,
|
||||
bool = false, bool = true, bool = true);
|
||||
void anm2_change(ChangeType);
|
||||
void assets_sync(ChangeType = ALL);
|
||||
void texture_change(int);
|
||||
resource::Texture* texture_get(int);
|
||||
const resource::Texture* texture_get(int) const;
|
||||
resource::Audio* sound_get(int);
|
||||
const resource::Audio* sound_get(int) const;
|
||||
bool regions_trim(int, const std::set<int>&);
|
||||
bool spritesheet_pack(int, int);
|
||||
bool spritesheets_merge(const std::set<int>&, bool, bool, bool, origin::Type);
|
||||
void scan_and_set_regions();
|
||||
bool file_merge(const std::filesystem::path&);
|
||||
void hash_set();
|
||||
void clean();
|
||||
void change(ChangeType);
|
||||
@@ -87,6 +109,7 @@ namespace anm2ed
|
||||
std::filesystem::path directory_get() const;
|
||||
std::filesystem::path filename_get() const;
|
||||
bool is_valid() const;
|
||||
void command_run(Manager&, Command&);
|
||||
void spritesheet_hash_update(int);
|
||||
void spritesheet_hash_set_saved(int);
|
||||
bool spritesheet_is_dirty(int);
|
||||
@@ -94,15 +117,17 @@ namespace anm2ed
|
||||
void spritesheet_hashes_reset();
|
||||
void spritesheet_hashes_sync();
|
||||
|
||||
anm2::Frame* frame_get();
|
||||
anm2::Item* item_get();
|
||||
anm2::Spritesheet* spritesheet_get();
|
||||
anm2::Animation* animation_get();
|
||||
Element* frame_get();
|
||||
Element* item_get();
|
||||
Element* spritesheet_get();
|
||||
Element* animation_get();
|
||||
|
||||
void spritesheet_add(const std::filesystem::path&);
|
||||
void spritesheets_add(const std::vector<std::filesystem::path>&);
|
||||
void sound_add(const std::filesystem::path&);
|
||||
void sounds_add(const std::vector<std::filesystem::path>&);
|
||||
|
||||
bool autosave(std::string* = nullptr, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true,
|
||||
bool autosave(std::string* = nullptr, Compatibility = Compatibility::ANM2ED, bool = false, bool = true,
|
||||
bool = true);
|
||||
std::filesystem::path autosave_path_get();
|
||||
std::filesystem::path path_from_autosave_get(const std::filesystem::path&);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "actions.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
bool is_action_shortcut_valid(int shortcutType) { return shortcutType >= 0 && shortcutType < SHORTCUT_COUNT; }
|
||||
|
||||
std::string action_shortcut_text_get(const Action& action, Settings& settings)
|
||||
{
|
||||
if (!is_action_shortcut_valid(action.shortcut)) return {};
|
||||
return settings.*SHORTCUT_MEMBERS[action.shortcut];
|
||||
}
|
||||
|
||||
ImGuiKeyChord action_chord_get(const Action& action, Manager& manager)
|
||||
{
|
||||
if (!is_action_shortcut_valid(action.shortcut)) return ImGuiKey_None;
|
||||
return manager.chords[action.shortcut];
|
||||
}
|
||||
|
||||
bool is_action_enabled(const Action& action) { return !action.isEnabled || action.isEnabled(); }
|
||||
|
||||
Action action_make(ActionType type, std::function<bool()> isEnabled, std::function<void()> run, StringType tooltip,
|
||||
int shortcut)
|
||||
{
|
||||
auto info = ACTION_INFOS[type];
|
||||
return {.type = type,
|
||||
.label = info.label,
|
||||
.tooltip = tooltip,
|
||||
.shortcut = shortcut == ACTION_COUNT ? info.shortcut : shortcut,
|
||||
.isEnabled = std::move(isEnabled),
|
||||
.run = std::move(run)};
|
||||
}
|
||||
|
||||
void Actions::add(Action action) { items.push_back(std::move(action)); }
|
||||
|
||||
void Actions::add(ActionType type, std::function<bool()> isEnabled, std::function<void()> run, StringType tooltip,
|
||||
int shortcut)
|
||||
{
|
||||
add(action_make(type, std::move(isEnabled), std::move(run), tooltip, shortcut));
|
||||
}
|
||||
|
||||
void Actions::separator() { items.push_back({.isSeparator = true}); }
|
||||
|
||||
void actions_undo_redo_add(Actions& actions, Manager& manager, Document& document)
|
||||
{
|
||||
actions.add(ACTION_UNDO, [&document]() { return document.is_able_to_undo(); },
|
||||
[&manager]()
|
||||
{ manager.command_push({manager.selected, [](Manager&, Document& document) { document.undo(); }}); });
|
||||
actions.add(ACTION_REDO, [&document]() { return document.is_able_to_redo(); },
|
||||
[&manager]()
|
||||
{ manager.command_push({manager.selected, [](Manager&, Document& document) { document.redo(); }}); });
|
||||
}
|
||||
|
||||
void actions_menu_draw(Actions& actions, Settings& settings)
|
||||
{
|
||||
bool isPreviousSeparator{};
|
||||
for (auto& action : actions.items)
|
||||
{
|
||||
if (action.isSeparator)
|
||||
{
|
||||
if (!isPreviousSeparator) ImGui::Separator();
|
||||
isPreviousSeparator = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto shortcutText = action_shortcut_text_get(action, settings);
|
||||
auto shortcutLabel = shortcutText.empty() ? nullptr : shortcutText.c_str();
|
||||
if (ImGui::MenuItem(localize.get(action.label), shortcutLabel, false, is_action_enabled(action)) && action.run)
|
||||
action.run();
|
||||
if (action.tooltip != STRING_UNDEFINED && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetItemTooltip("%s", localize.get(action.tooltip));
|
||||
isPreviousSeparator = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool actions_context_window_draw(const char* label, Actions& actions, Settings& settings, ImGuiPopupFlags flags)
|
||||
{
|
||||
if (!ImGui::BeginPopupContextWindow(label, flags)) return false;
|
||||
actions_menu_draw(actions, settings);
|
||||
ImGui::EndPopup();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool actions_popup_draw(const char* label, Actions& actions, Settings& settings)
|
||||
{
|
||||
if (!ImGui::BeginPopup(label)) return false;
|
||||
actions_menu_draw(actions, settings);
|
||||
ImGui::EndPopup();
|
||||
return true;
|
||||
}
|
||||
|
||||
void actions_shortcuts_update(Actions& actions, Manager& manager, types::shortcut::Type shortcutType)
|
||||
{
|
||||
for (auto& action : actions.items)
|
||||
{
|
||||
if (action.isSeparator || !action.run || !is_action_enabled(action)) continue;
|
||||
auto chord = action_chord_get(action, manager);
|
||||
if (!shortcut(chord, shortcutType)) continue;
|
||||
action.run();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void action_button_draw(Action& action, Manager& manager, Settings& settings, ImVec2 widgetSize, bool& isSameLine)
|
||||
{
|
||||
if (isSameLine) ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!is_action_enabled(action));
|
||||
shortcut(action_chord_get(action, manager));
|
||||
if (ImGui::Button(localize.get(action.label), widgetSize) && action.run) action.run();
|
||||
ImGui::EndDisabled();
|
||||
if (action.tooltip != STRING_UNDEFINED)
|
||||
{
|
||||
auto shortcutText = action_shortcut_text_get(action, settings);
|
||||
if (shortcutText.empty())
|
||||
ImGui::SetItemTooltip("%s", localize.get(action.tooltip));
|
||||
else
|
||||
set_item_tooltip_shortcut(localize.get(action.tooltip), shortcutText);
|
||||
}
|
||||
isSameLine = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "util/imgui/imgui.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
#define ACTIONS(X) \
|
||||
X(ACTION_UNDO, SHORTCUT_STRING_UNDO, SHORTCUT_UNDO) \
|
||||
X(ACTION_REDO, SHORTCUT_STRING_REDO, SHORTCUT_REDO) \
|
||||
X(ACTION_ADD, BASIC_ADD, SHORTCUT_ADD) \
|
||||
X(ACTION_REMOVE, BASIC_REMOVE, SHORTCUT_REMOVE) \
|
||||
X(ACTION_REMOVE_UNUSED, BASIC_REMOVE_UNUSED, SHORTCUT_REMOVE) \
|
||||
X(ACTION_DUPLICATE, BASIC_DUPLICATE, SHORTCUT_DUPLICATE) \
|
||||
X(ACTION_MERGE, BASIC_MERGE, SHORTCUT_MERGE) \
|
||||
X(ACTION_GROUP, BASIC_GROUP, SHORTCUT_GROUP) \
|
||||
X(ACTION_DEFAULT, BASIC_DEFAULT, SHORTCUT_DEFAULT) \
|
||||
X(ACTION_RENAME, BASIC_RENAME, SHORTCUT_RENAME) \
|
||||
X(ACTION_PROPERTIES, BASIC_PROPERTIES, -1) \
|
||||
X(ACTION_CUT, BASIC_CUT, SHORTCUT_CUT) \
|
||||
X(ACTION_COPY, BASIC_COPY, SHORTCUT_COPY) \
|
||||
X(ACTION_PASTE, BASIC_PASTE, SHORTCUT_PASTE) \
|
||||
X(ACTION_RELOAD, BASIC_RELOAD, -1) \
|
||||
X(ACTION_REPLACE, BASIC_REPLACE, -1) \
|
||||
X(ACTION_SAVE, BASIC_SAVE, -1) \
|
||||
X(ACTION_OPEN_DIRECTORY, BASIC_OPEN_DIRECTORY, -1) \
|
||||
X(ACTION_SET_FILE_PATH, BASIC_SET_FILE_PATH, -1) \
|
||||
X(ACTION_PACK, BASIC_PACK, -1) \
|
||||
X(ACTION_TRIM, BASIC_TRIM, -1) \
|
||||
X(ACTION_PLAY, LABEL_PLAY, SHORTCUT_PLAY_PAUSE) \
|
||||
X(ACTION_CENTER_VIEW, LABEL_CENTER_VIEW, SHORTCUT_CENTER_VIEW) \
|
||||
X(ACTION_FIT_VIEW, LABEL_FIT, SHORTCUT_FIT) \
|
||||
X(ACTION_ZOOM_IN, SHORTCUT_STRING_ZOOM_IN, SHORTCUT_ZOOM_IN) \
|
||||
X(ACTION_ZOOM_OUT, SHORTCUT_STRING_ZOOM_OUT, SHORTCUT_ZOOM_OUT)
|
||||
|
||||
enum ActionType
|
||||
{
|
||||
#define X(name, label, shortcut) name,
|
||||
ACTIONS(X)
|
||||
#undef X
|
||||
ACTION_COUNT
|
||||
};
|
||||
|
||||
struct ActionInfo
|
||||
{
|
||||
StringType label{};
|
||||
int shortcut{-1};
|
||||
};
|
||||
|
||||
constexpr ActionInfo ACTION_INFOS[] = {
|
||||
#define X(name, label, shortcut) {label, shortcut},
|
||||
ACTIONS(X)
|
||||
#undef X
|
||||
};
|
||||
|
||||
struct Action
|
||||
{
|
||||
ActionType type{};
|
||||
StringType label{STRING_UNDEFINED};
|
||||
StringType tooltip{STRING_UNDEFINED};
|
||||
int shortcut{-1};
|
||||
bool isSeparator{};
|
||||
std::function<bool()> isEnabled{};
|
||||
std::function<void()> run{};
|
||||
};
|
||||
|
||||
struct Actions
|
||||
{
|
||||
std::vector<Action> items{};
|
||||
|
||||
void add(Action);
|
||||
void add(ActionType, std::function<bool()>, std::function<void()>, StringType = STRING_UNDEFINED,
|
||||
int = ACTION_COUNT);
|
||||
void separator();
|
||||
};
|
||||
|
||||
bool is_action_enabled(const Action&);
|
||||
Action action_make(ActionType, std::function<bool()>, std::function<void()>, StringType = STRING_UNDEFINED,
|
||||
int = ACTION_COUNT);
|
||||
void actions_undo_redo_add(Actions&, Manager&, Document&);
|
||||
void actions_menu_draw(Actions&, Settings&);
|
||||
bool actions_context_window_draw(const char*, Actions&, Settings&, ImGuiPopupFlags = ImGuiPopupFlags_MouseButtonRight);
|
||||
bool actions_popup_draw(const char*, Actions&, Settings&);
|
||||
void actions_shortcuts_update(Actions&, Manager&, types::shortcut::Type = types::shortcut::FOCUSED);
|
||||
void action_button_draw(Action&, Manager&, Settings&, ImVec2, bool&);
|
||||
}
|
||||
+13
-7
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
bool Dockspace::is_canvas_focused_get() const { return isCanvasFocused; }
|
||||
|
||||
void Dockspace::tick(Manager& manager, Settings& settings, float deltaSeconds)
|
||||
{
|
||||
if (auto document = manager.get(); document)
|
||||
@@ -11,6 +13,7 @@ namespace anm2ed::imgui
|
||||
void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings,
|
||||
Resources& resources, Dialog& dialog, Clipboard& clipboard)
|
||||
{
|
||||
isCanvasFocused = false;
|
||||
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto windowHeight = viewport->Size.y - taskbar.height - documents.height;
|
||||
@@ -30,18 +33,21 @@ namespace anm2ed::imgui
|
||||
if (ImGui::DockSpace(ImGui::GetID("##DockSpace"), ImVec2(), ImGuiDockNodeFlags_PassthruCentralNode))
|
||||
{
|
||||
if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources);
|
||||
if (settings.windowIsAnimations) animations.update(manager, settings, resources, clipboard);
|
||||
if (settings.windowIsRegions) regions.update(manager, settings, resources, clipboard);
|
||||
if (settings.windowIsEvents) events.update(manager, settings, resources, clipboard);
|
||||
if (settings.windowIsAnimations) window_update(animations, manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsRegions) window_update(regions, manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsEvents) window_update(events, manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsFrameProperties) frameProperties.update(manager, settings);
|
||||
if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard);
|
||||
if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard);
|
||||
if (settings.windowIsLayers) window_update(layers, manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsNulls) window_update(nulls, manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsOnionskin) onionskin.update(manager, settings);
|
||||
if (settings.windowIsSounds) sounds.update(manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsSounds) window_update(sounds, manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources);
|
||||
if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsSpritesheets)
|
||||
window_update(spritesheets, manager, settings, resources, dialog, clipboard);
|
||||
if (settings.windowIsTimeline) timeline.update(manager, settings, resources, clipboard);
|
||||
if (settings.windowIsTools) tools.update(manager, settings, resources);
|
||||
isCanvasFocused = (settings.windowIsAnimationPreview && animationPreview.is_focused_get()) ||
|
||||
(settings.windowIsSpritesheetEditor && spritesheetEditor.is_focused_get());
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
+11
-15
@@ -1,44 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "autosave_restore.hpp"
|
||||
#include "popup/autosave_restore.hpp"
|
||||
#include "documents.hpp"
|
||||
#include "taskbar.hpp"
|
||||
#include "window/animation_preview.hpp"
|
||||
#include "window/animations.hpp"
|
||||
#include "window/regions.hpp"
|
||||
#include "window/events.hpp"
|
||||
#include "window/frame_properties.hpp"
|
||||
#include "window/layers.hpp"
|
||||
#include "window/nulls.hpp"
|
||||
#include "window/onionskin.hpp"
|
||||
#include "window/sounds.hpp"
|
||||
#include "window/spritesheet_editor.hpp"
|
||||
#include "window/spritesheets.hpp"
|
||||
#include "window/timeline.hpp"
|
||||
#include "window/tools.hpp"
|
||||
#include "window/welcome.hpp"
|
||||
#include "window/window.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Dockspace
|
||||
{
|
||||
AnimationPreview animationPreview;
|
||||
Animations animations;
|
||||
Regions regions;
|
||||
Events events;
|
||||
bool isCanvasFocused{};
|
||||
Window animations{animations_window_register()};
|
||||
Window regions{regions_window_register()};
|
||||
Window events{events_window_register()};
|
||||
FrameProperties frameProperties;
|
||||
Layers layers;
|
||||
Nulls nulls;
|
||||
Window layers{layers_window_register()};
|
||||
Window nulls{nulls_window_register()};
|
||||
Onionskin onionskin;
|
||||
SpritesheetEditor spritesheetEditor;
|
||||
Spritesheets spritesheets;
|
||||
Sounds sounds;
|
||||
Window spritesheets{spritesheets_window_register()};
|
||||
Window sounds{sounds_window_register()};
|
||||
Timeline timeline;
|
||||
Tools tools;
|
||||
Welcome welcome;
|
||||
AutosaveRestore autosaveRestore;
|
||||
|
||||
public:
|
||||
bool is_canvas_focused_get() const;
|
||||
void tick(Manager&, Settings&, float);
|
||||
void update(Taskbar&, Documents&, Manager&, Settings&, Resources&, Dialog&, Clipboard&);
|
||||
};
|
||||
|
||||
+68
-46
@@ -3,11 +3,11 @@
|
||||
#include <format>
|
||||
#include <vector>
|
||||
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "time_.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "time.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
@@ -31,16 +31,24 @@ namespace anm2ed::imgui
|
||||
pushedStyle = true;
|
||||
}
|
||||
|
||||
for (auto& document : manager.documents)
|
||||
for (auto i = 0; i < (int)manager.documents.size(); ++i)
|
||||
{
|
||||
auto& document = manager.documents[i];
|
||||
auto isDirty = document.is_dirty() && document.is_autosave_dirty();
|
||||
if (isDirty)
|
||||
{
|
||||
document.lastAutosaveTime += ImGui::GetIO().DeltaTime;
|
||||
if (document.lastAutosaveTime > time::SECOND_M)
|
||||
manager.autosave(document, (anm2::Compatibility)settings.fileCompatibility,
|
||||
settings.fileBakeSpecialInterpolatedFramesOnSave, settings.bakeIsRoundScale,
|
||||
settings.bakeIsRoundRotation);
|
||||
{
|
||||
auto compatibility = (Compatibility)settings.fileCompatibility;
|
||||
auto bakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave;
|
||||
auto isRoundScale = settings.bakeIsRoundScale;
|
||||
auto isRoundRotation = settings.bakeIsRoundRotation;
|
||||
manager.command_push({i,
|
||||
[compatibility, bakeFrames, isRoundScale, isRoundRotation](Manager& manager,
|
||||
Document& document)
|
||||
{ manager.autosave(document, compatibility, bakeFrames, isRoundScale, isRoundRotation); }});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +103,8 @@ namespace anm2ed::imgui
|
||||
auto isRequested = i == manager.pendingSelected;
|
||||
auto font = isDocumentDirty ? font::ITALICS : font::REGULAR;
|
||||
auto filename = path::to_utf8(document.filename_get());
|
||||
auto string =
|
||||
isDocumentDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) : filename;
|
||||
auto string = isDocumentDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename))
|
||||
: filename;
|
||||
auto label = std::format("{}###Document{}", string, i);
|
||||
|
||||
auto flags = isDocumentDirty ? ImGuiTabItemFlags_UnsavedDocument : 0;
|
||||
@@ -120,7 +128,8 @@ namespace anm2ed::imgui
|
||||
for (auto it = closeIndices.rbegin(); it != closeIndices.rend(); ++it)
|
||||
{
|
||||
if (closePopup.is_open() && closeDocumentIndex > *it) --closeDocumentIndex;
|
||||
manager.close(*it);
|
||||
auto index = *it;
|
||||
manager.command_push({.runManager = [index](Manager& manager) { manager.close(index); }});
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
@@ -137,10 +146,10 @@ namespace anm2ed::imgui
|
||||
auto filename = path::to_utf8(closeDocument.filename_get());
|
||||
auto isDocumentDirty = closeDocument.is_dirty() || closeDocument.isForceDirty;
|
||||
auto isSpritesheetDirty = closeDocument.spritesheet_any_dirty();
|
||||
auto promptLabel = isDocumentDirty && isSpritesheetDirty
|
||||
? LABEL_DOCUMENT_AND_SPRITESHEETS_MODIFIED_PROMPT
|
||||
: (isDocumentDirty ? LABEL_DOCUMENT_MODIFIED_PROMPT
|
||||
: LABEL_SPRITESHEETS_MODIFIED_PROMPT);
|
||||
auto promptLabel =
|
||||
isDocumentDirty && isSpritesheetDirty
|
||||
? LABEL_DOCUMENT_AND_SPRITESHEETS_MODIFIED_PROMPT
|
||||
: (isDocumentDirty ? LABEL_DOCUMENT_MODIFIED_PROMPT : LABEL_SPRITESHEETS_MODIFIED_PROMPT);
|
||||
auto prompt = std::vformat(localize.get(promptLabel), std::make_format_args(filename));
|
||||
ImGui::TextUnformatted(prompt.c_str());
|
||||
|
||||
@@ -156,8 +165,7 @@ namespace anm2ed::imgui
|
||||
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
|
||||
{
|
||||
bool isSaved = true;
|
||||
if (isDocumentDirty)
|
||||
isSaved = taskbar.save_manual(manager, settings, closeDocumentIndex);
|
||||
if (isDocumentDirty) isSaved = taskbar.save_manual(manager, settings, closeDocumentIndex);
|
||||
|
||||
if (!isSaved)
|
||||
{
|
||||
@@ -167,27 +175,38 @@ namespace anm2ed::imgui
|
||||
|
||||
if (isSpritesheetDirty)
|
||||
{
|
||||
for (auto& [id, spritesheet] : closeDocument.anm2.content.spritesheets)
|
||||
auto spritesheets = closeDocument.anm2.element_get(ElementType::SPRITESHEETS);
|
||||
if (spritesheets)
|
||||
{
|
||||
if (!closeDocument.spritesheet_is_dirty(id)) continue;
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
if (spritesheet.save(closeDocument.directory_get()))
|
||||
for (auto& spritesheet : spritesheets->children)
|
||||
{
|
||||
closeDocument.spritesheet_hash_set_saved(id);
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
if (spritesheet.type != ElementType::SPRITESHEET) continue;
|
||||
auto id = spritesheet.id;
|
||||
auto texture = closeDocument.texture_get(id);
|
||||
if (!texture || !closeDocument.spritesheet_is_dirty(id)) continue;
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
auto savePath = closeDocument.directory_get() / spritesheet.path;
|
||||
path::ensure_directory(savePath.parent_path());
|
||||
if (texture->write_png(savePath))
|
||||
{
|
||||
closeDocument.spritesheet_hash_set_saved(id);
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED),
|
||||
std::make_format_args(id, pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
manager.close(closeDocumentIndex);
|
||||
auto index = closeDocumentIndex;
|
||||
manager.command_push({.runManager = [index](Manager& manager) { manager.close(index); }});
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -195,7 +214,8 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_NO), widgetSize))
|
||||
{
|
||||
manager.close(closeDocumentIndex);
|
||||
auto index = closeDocumentIndex;
|
||||
manager.command_push({.runManager = [index](Manager& manager) { manager.close(index); }});
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -244,8 +264,12 @@ namespace anm2ed::imgui
|
||||
if (ImGui::MenuItem(manager.anm2DragDropPaths.size() > 1 ? localize.get(LABEL_DOCUMENTS_OPEN_MANY)
|
||||
: localize.get(LABEL_DOCUMENTS_OPEN_NEW)))
|
||||
{
|
||||
for (auto& path : manager.anm2DragDropPaths)
|
||||
manager.open(path);
|
||||
auto paths = manager.anm2DragDropPaths;
|
||||
manager.command_push({.runManager =
|
||||
[paths](Manager& manager)
|
||||
{
|
||||
for (auto& path : paths) manager.open(path);
|
||||
}});
|
||||
drag_drop_reset();
|
||||
}
|
||||
|
||||
@@ -254,16 +278,14 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (document)
|
||||
{
|
||||
auto merge_anm2s = [&]()
|
||||
{
|
||||
for (auto& path : manager.anm2DragDropPaths)
|
||||
{
|
||||
anm2::Anm2 source(path);
|
||||
document->anm2.merge(source, document->directory_get(), path.parent_path());
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT_PTR(document, localize.get(EDIT_MERGE_ANM2), Document::ALL, merge_anm2s());
|
||||
auto paths = manager.anm2DragDropPaths;
|
||||
manager.command_push({manager.selected,
|
||||
[paths](Manager&, Document& document)
|
||||
{
|
||||
document.snapshot(localize.get(EDIT_MERGE_ANM2));
|
||||
for (auto& path : paths) document.file_merge(path);
|
||||
document.change(Document::ALL);
|
||||
}});
|
||||
drag_drop_reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,695 +0,0 @@
|
||||
#include <imgui/imgui_internal.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
static auto isRenaming = false;
|
||||
|
||||
namespace
|
||||
{
|
||||
const std::vector<std::pair<ImGuiKey, const char*>> CANONICAL_KEY_NAMES = {
|
||||
{ImGuiKey_A, "A"},
|
||||
{ImGuiKey_B, "B"},
|
||||
{ImGuiKey_C, "C"},
|
||||
{ImGuiKey_D, "D"},
|
||||
{ImGuiKey_E, "E"},
|
||||
{ImGuiKey_F, "F"},
|
||||
{ImGuiKey_G, "G"},
|
||||
{ImGuiKey_H, "H"},
|
||||
{ImGuiKey_I, "I"},
|
||||
{ImGuiKey_J, "J"},
|
||||
{ImGuiKey_K, "K"},
|
||||
{ImGuiKey_L, "L"},
|
||||
{ImGuiKey_M, "M"},
|
||||
{ImGuiKey_N, "N"},
|
||||
{ImGuiKey_O, "O"},
|
||||
{ImGuiKey_P, "P"},
|
||||
{ImGuiKey_Q, "Q"},
|
||||
{ImGuiKey_R, "R"},
|
||||
{ImGuiKey_S, "S"},
|
||||
{ImGuiKey_T, "T"},
|
||||
{ImGuiKey_U, "U"},
|
||||
{ImGuiKey_V, "V"},
|
||||
{ImGuiKey_W, "W"},
|
||||
{ImGuiKey_X, "X"},
|
||||
{ImGuiKey_Y, "Y"},
|
||||
{ImGuiKey_Z, "Z"},
|
||||
{ImGuiKey_0, "0"},
|
||||
{ImGuiKey_1, "1"},
|
||||
{ImGuiKey_2, "2"},
|
||||
{ImGuiKey_3, "3"},
|
||||
{ImGuiKey_4, "4"},
|
||||
{ImGuiKey_5, "5"},
|
||||
{ImGuiKey_6, "6"},
|
||||
{ImGuiKey_7, "7"},
|
||||
{ImGuiKey_8, "8"},
|
||||
{ImGuiKey_9, "9"},
|
||||
{ImGuiKey_Keypad0, "Num0"},
|
||||
{ImGuiKey_Keypad1, "Num1"},
|
||||
{ImGuiKey_Keypad2, "Num2"},
|
||||
{ImGuiKey_Keypad3, "Num3"},
|
||||
{ImGuiKey_Keypad4, "Num4"},
|
||||
{ImGuiKey_Keypad5, "Num5"},
|
||||
{ImGuiKey_Keypad6, "Num6"},
|
||||
{ImGuiKey_Keypad7, "Num7"},
|
||||
{ImGuiKey_Keypad8, "Num8"},
|
||||
{ImGuiKey_Keypad9, "Num9"},
|
||||
{ImGuiKey_KeypadAdd, "NumAdd"},
|
||||
{ImGuiKey_KeypadSubtract, "NumSubtract"},
|
||||
{ImGuiKey_KeypadMultiply, "NumMultiply"},
|
||||
{ImGuiKey_KeypadDivide, "NumDivide"},
|
||||
{ImGuiKey_KeypadEnter, "NumEnter"},
|
||||
{ImGuiKey_KeypadDecimal, "NumDecimal"},
|
||||
{ImGuiKey_KeypadEqual, "NumEqual"},
|
||||
{ImGuiKey_F1, "F1"},
|
||||
{ImGuiKey_F2, "F2"},
|
||||
{ImGuiKey_F3, "F3"},
|
||||
{ImGuiKey_F4, "F4"},
|
||||
{ImGuiKey_F5, "F5"},
|
||||
{ImGuiKey_F6, "F6"},
|
||||
{ImGuiKey_F7, "F7"},
|
||||
{ImGuiKey_F8, "F8"},
|
||||
{ImGuiKey_F9, "F9"},
|
||||
{ImGuiKey_F10, "F10"},
|
||||
{ImGuiKey_F11, "F11"},
|
||||
{ImGuiKey_F12, "F12"},
|
||||
{ImGuiKey_UpArrow, "Up"},
|
||||
{ImGuiKey_DownArrow, "Down"},
|
||||
{ImGuiKey_LeftArrow, "Left"},
|
||||
{ImGuiKey_RightArrow, "Right"},
|
||||
{ImGuiKey_Space, "Space"},
|
||||
{ImGuiKey_Enter, "Enter"},
|
||||
{ImGuiKey_Escape, "Escape"},
|
||||
{ImGuiKey_Tab, "Tab"},
|
||||
{ImGuiKey_Backspace, "Backspace"},
|
||||
{ImGuiKey_Delete, "Delete"},
|
||||
{ImGuiKey_Insert, "Insert"},
|
||||
{ImGuiKey_Home, "Home"},
|
||||
{ImGuiKey_End, "End"},
|
||||
{ImGuiKey_PageUp, "PageUp"},
|
||||
{ImGuiKey_PageDown, "PageDown"},
|
||||
{ImGuiKey_Minus, "Minus"},
|
||||
{ImGuiKey_Equal, "Equal"},
|
||||
{ImGuiKey_LeftBracket, "LeftBracket"},
|
||||
{ImGuiKey_RightBracket, "RightBracket"},
|
||||
{ImGuiKey_Semicolon, "Semicolon"},
|
||||
{ImGuiKey_Apostrophe, "Apostrophe"},
|
||||
{ImGuiKey_Comma, "Comma"},
|
||||
{ImGuiKey_Period, "Period"},
|
||||
{ImGuiKey_Slash, "Slash"},
|
||||
{ImGuiKey_Backslash, "Backslash"},
|
||||
{ImGuiKey_GraveAccent, "GraveAccent"},
|
||||
};
|
||||
|
||||
const char* canonical_key_name(ImGuiKey key)
|
||||
{
|
||||
for (const auto& [mappedKey, name] : CANONICAL_KEY_NAMES)
|
||||
if (mappedKey == key) return name;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr ImVec4 COLOR_LIGHT_BUTTON{0.98f, 0.98f, 0.98f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TITLE_BG{0.78f, 0.78f, 0.78f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TITLE_BG_ACTIVE{0.64f, 0.64f, 0.64f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TITLE_BG_COLLAPSED{0.74f, 0.74f, 0.74f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TABLE_HEADER{0.78f, 0.78f, 0.78f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TAB{0.74f, 0.74f, 0.74f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TAB_HOVERED{0.82f, 0.82f, 0.82f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TAB_SELECTED{0.92f, 0.92f, 0.92f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED{0.70f, 0.70f, 0.70f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_SELECTED{0.86f, 0.86f, 0.86f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TAB_OVERLINE{0.55f, 0.55f, 0.55f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_OVERLINE{0.50f, 0.50f, 0.50f, 1.0f};
|
||||
constexpr ImVec4 COLOR_LIGHT_CHECK_MARK{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
constexpr auto FRAME_BORDER_SIZE = 1.0f;
|
||||
|
||||
void theme_set(theme::Type theme)
|
||||
{
|
||||
switch (theme)
|
||||
{
|
||||
case theme::LIGHT:
|
||||
ImGui::StyleColorsLight();
|
||||
break;
|
||||
case theme::DARK:
|
||||
default:
|
||||
ImGui::StyleColorsDark();
|
||||
break;
|
||||
case theme::CLASSIC:
|
||||
ImGui::StyleColorsClassic();
|
||||
break;
|
||||
}
|
||||
auto& style = ImGui::GetStyle();
|
||||
style.FrameBorderSize = FRAME_BORDER_SIZE;
|
||||
|
||||
if (theme == theme::LIGHT)
|
||||
{
|
||||
auto& colors = style.Colors;
|
||||
colors[ImGuiCol_Button] = COLOR_LIGHT_BUTTON;
|
||||
colors[ImGuiCol_TitleBg] = COLOR_LIGHT_TITLE_BG;
|
||||
colors[ImGuiCol_TitleBgActive] = COLOR_LIGHT_TITLE_BG_ACTIVE;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = COLOR_LIGHT_TITLE_BG_COLLAPSED;
|
||||
colors[ImGuiCol_TableHeaderBg] = COLOR_LIGHT_TABLE_HEADER;
|
||||
colors[ImGuiCol_Tab] = COLOR_LIGHT_TAB;
|
||||
colors[ImGuiCol_TabHovered] = COLOR_LIGHT_TAB_HOVERED;
|
||||
colors[ImGuiCol_TabSelected] = COLOR_LIGHT_TAB_SELECTED;
|
||||
colors[ImGuiCol_TabSelectedOverline] = COLOR_LIGHT_TAB_OVERLINE;
|
||||
colors[ImGuiCol_TabDimmed] = COLOR_LIGHT_TAB_DIMMED;
|
||||
colors[ImGuiCol_TabDimmedSelected] = COLOR_LIGHT_TAB_DIMMED_SELECTED;
|
||||
colors[ImGuiCol_TabDimmedSelectedOverline] = COLOR_LIGHT_TAB_DIMMED_OVERLINE;
|
||||
colors[ImGuiCol_CheckMark] = COLOR_LIGHT_CHECK_MARK;
|
||||
}
|
||||
}
|
||||
|
||||
int input_text_callback(ImGuiInputTextCallbackData* data)
|
||||
{
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize)
|
||||
{
|
||||
auto* string = (std::string*)(data->UserData);
|
||||
string->resize(data->BufTextLen);
|
||||
data->Buf = string->data();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool input_text_string(const char* label, std::string* string, ImGuiInputTextFlags flags)
|
||||
{
|
||||
flags |= ImGuiInputTextFlags_CallbackResize;
|
||||
return ImGui::InputText(label, string->data(), string->capacity() + 1, flags, input_text_callback, string);
|
||||
}
|
||||
|
||||
bool input_text_path(const char* label, std::filesystem::path* path, ImGuiInputTextFlags flags)
|
||||
{
|
||||
if (!path) return false;
|
||||
|
||||
auto pathUtf8 = path::to_utf8(*path);
|
||||
auto edited = input_text_string(label, &pathUtf8, flags);
|
||||
if (edited) *path = path::from_utf8(pathUtf8);
|
||||
|
||||
return edited;
|
||||
}
|
||||
|
||||
bool combo_negative_one_indexed(const std::string& label, int* index, std::vector<const char*>& strings)
|
||||
{
|
||||
*index += 1;
|
||||
bool isActivated = ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size());
|
||||
*index -= 1;
|
||||
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
bool combo_id_mapped(const std::string& label, int* id, const std::vector<int>& ids, std::vector<const char*>& labels)
|
||||
{
|
||||
if (!id) return false;
|
||||
|
||||
int index = -1;
|
||||
if (!ids.empty())
|
||||
{
|
||||
auto it = std::find(ids.begin(), ids.end(), *id);
|
||||
if (it != ids.end()) index = (int)std::distance(ids.begin(), it);
|
||||
}
|
||||
|
||||
bool isActivated = ImGui::Combo(label.c_str(), &index, labels.data(), (int)labels.size());
|
||||
if (isActivated)
|
||||
{
|
||||
if (index >= 0 && index < (int)ids.size())
|
||||
*id = ids[index];
|
||||
else
|
||||
*id = -1;
|
||||
}
|
||||
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
edit::Type drag_int_persistent(const char* label, int* value, float speed, int min, int max, const char* format,
|
||||
ImGuiSliderFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static int start{INT_MAX};
|
||||
auto persistent = value ? *value : 0;
|
||||
|
||||
ImGui::DragInt(label, &persistent, speed, min, max, format, flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = INT_MAX;
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type drag_float_persistent(const char* label, float* value, float speed, float min, float max,
|
||||
const char* format, ImGuiSliderFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static float start{NAN};
|
||||
auto persistent = value ? *value : 0;
|
||||
|
||||
ImGui::DragFloat(label, &persistent, speed, min, max, format, flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = NAN;
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type drag_float2_persistent(const char* label, vec2* value, float speed, float min, float max,
|
||||
const char* format, ImGuiSliderFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static vec2 start{NAN};
|
||||
auto persistent = value ? *value : vec2();
|
||||
|
||||
ImGui::DragFloat2(label, value_ptr(persistent), speed, min, max, format, flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = vec2{NAN};
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type color_edit3_persistent(const char* label, vec3* value, ImGuiColorEditFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static vec3 start{NAN};
|
||||
auto persistent = value ? *value : vec4();
|
||||
|
||||
ImGui::ColorEdit3(label, value_ptr(persistent), flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = vec4{NAN};
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
edit::Type color_edit4_persistent(const char* label, vec4* value, ImGuiColorEditFlags flags)
|
||||
{
|
||||
static bool isEditing{};
|
||||
static vec4 start{NAN};
|
||||
auto persistent = value ? *value : vec4();
|
||||
|
||||
ImGui::ColorEdit4(label, value_ptr(persistent), flags);
|
||||
if (!value) return edit::NONE;
|
||||
if (ImGui::IsItemActivated() && persistent != start)
|
||||
{
|
||||
isEditing = true;
|
||||
start = *value;
|
||||
return edit::START;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
isEditing = false;
|
||||
*value = persistent;
|
||||
start = vec4{NAN};
|
||||
return edit::END;
|
||||
}
|
||||
else if (isEditing)
|
||||
{
|
||||
*value = persistent;
|
||||
return edit::DURING;
|
||||
}
|
||||
|
||||
return edit::NONE;
|
||||
}
|
||||
|
||||
bool input_int_range(const char* label, int& value, int min, int max, int step, int stepFast,
|
||||
ImGuiInputTextFlags flags)
|
||||
{
|
||||
auto isActivated = ImGui::InputInt(label, &value, step, stepFast, flags);
|
||||
value = glm::clamp(value, min, max);
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
bool input_int2_range(const char* label, ivec2& value, ivec2 min, ivec2 max, ImGuiInputTextFlags flags)
|
||||
{
|
||||
auto isActivated = ImGui::InputInt2(label, value_ptr(value), flags);
|
||||
value = glm::clamp(value, min, max);
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
bool input_float_range(const char* label, float& value, float min, float max, float step, float stepFast,
|
||||
const char* format, ImGuiInputTextFlags flags)
|
||||
{
|
||||
auto isActivated = ImGui::InputFloat(label, &value, step, stepFast, format, flags);
|
||||
value = glm::clamp(value, min, max);
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
std::string& selectable_input_text_id()
|
||||
{
|
||||
static std::string editID{};
|
||||
return editID;
|
||||
}
|
||||
|
||||
bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected,
|
||||
ImGuiSelectableFlags flags, RenameState& state)
|
||||
{
|
||||
auto& editID = selectable_input_text_id();
|
||||
auto isRename = editID == id;
|
||||
bool isActivated{};
|
||||
|
||||
if (isRename)
|
||||
{
|
||||
auto finish = [&]()
|
||||
{
|
||||
editID.clear();
|
||||
isActivated = true;
|
||||
state = RENAME_FINISHED;
|
||||
isRenaming = false;
|
||||
};
|
||||
|
||||
if (state == RENAME_BEGIN)
|
||||
{
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
state = RENAME_EDITING;
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (input_text_string("##Edit", &text, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll))
|
||||
finish();
|
||||
if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) finish();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGui::Selectable(label.c_str(), isSelected, flags)) isActivated = true;
|
||||
|
||||
if (state == RENAME_FORCE_EDIT || (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)))
|
||||
{
|
||||
state = RENAME_BEGIN;
|
||||
editID = id;
|
||||
isActivated = true;
|
||||
isRenaming = true;
|
||||
}
|
||||
}
|
||||
|
||||
return isActivated;
|
||||
}
|
||||
|
||||
void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut)
|
||||
{
|
||||
ImGui::SetItemTooltip(
|
||||
"%s", std::vformat(localize.get(FORMAT_TOOLTIP_SHORTCUT), std::make_format_args(tooltip, shortcut)).c_str());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
struct CheckerStart
|
||||
{
|
||||
float position{};
|
||||
long long index{};
|
||||
};
|
||||
|
||||
CheckerStart checker_start(float minCoord, float offset, float step)
|
||||
{
|
||||
float world = minCoord + offset;
|
||||
long long idx = static_cast<long long>(std::floor(world / step));
|
||||
float first = minCoord - (world - static_cast<float>(idx) * step);
|
||||
return {first, idx};
|
||||
}
|
||||
}
|
||||
|
||||
void render_checker_background(ImDrawList* drawList, ImVec2 min, ImVec2 max, vec2 offset, float step)
|
||||
{
|
||||
if (!drawList || step <= 0.0f) return;
|
||||
|
||||
const ImU32 colorLight = IM_COL32(204, 204, 204, 255);
|
||||
const ImU32 colorDark = IM_COL32(128, 128, 128, 255);
|
||||
|
||||
auto [startY, rowIndex] = checker_start(min.y, offset.y, step);
|
||||
for (float y = startY; y < max.y; y += step, ++rowIndex)
|
||||
{
|
||||
float y1 = glm::max(y, min.y);
|
||||
float y2 = glm::min(y + step, max.y);
|
||||
if (y2 <= y1) continue;
|
||||
|
||||
auto [startX, columnIndex] = checker_start(min.x, offset.x, step);
|
||||
for (float x = startX; x < max.x; x += step, ++columnIndex)
|
||||
{
|
||||
float x1 = glm::max(x, min.x);
|
||||
float x2 = glm::min(x + step, max.x);
|
||||
if (x2 <= x1) continue;
|
||||
|
||||
bool isDark = ((rowIndex + columnIndex) & 1LL) != 0;
|
||||
drawList->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), isDark ? colorDark : colorLight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected)
|
||||
{
|
||||
auto* storage = static_cast<MultiSelectStorage*>(self->UserData);
|
||||
auto value = storage ? storage->resolve_index(id) : id;
|
||||
if (isSelected)
|
||||
storage->insert(value);
|
||||
else
|
||||
storage->erase(value);
|
||||
};
|
||||
|
||||
std::string chord_to_string(ImGuiKeyChord chord)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
if (chord & ImGuiMod_Ctrl) result += "Ctrl+";
|
||||
if (chord & ImGuiMod_Shift) result += "Shift+";
|
||||
if (chord & ImGuiMod_Alt) result += "Alt+";
|
||||
if (chord & ImGuiMod_Super) result += "Super+";
|
||||
|
||||
if (auto key = (ImGuiKey)(chord & ~ImGuiMod_Mask_); key != ImGuiKey_None)
|
||||
{
|
||||
if (const char* name = canonical_key_name(key); name && *name)
|
||||
result += name;
|
||||
else
|
||||
result += ImGui::GetKeyName(key);
|
||||
}
|
||||
|
||||
if (!result.empty() && result.back() == '+') result.pop_back();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ImGuiKeyChord string_to_chord(const std::string& string)
|
||||
{
|
||||
ImGuiKeyChord chord = 0;
|
||||
ImGuiKey baseKey = ImGuiKey_None;
|
||||
|
||||
std::stringstream ss(string);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, '+'))
|
||||
{
|
||||
token.erase(0, token.find_first_not_of(" \t\r\n"));
|
||||
token.erase(token.find_last_not_of(" \t\r\n") + 1);
|
||||
|
||||
if (token.empty()) continue;
|
||||
|
||||
if (auto it = MOD_MAP.find(token); it != MOD_MAP.end())
|
||||
chord |= it->second;
|
||||
else if (baseKey == ImGuiKey_None)
|
||||
if (auto it2 = KEY_MAP.find(token); it2 != KEY_MAP.end()) baseKey = it2->second;
|
||||
}
|
||||
|
||||
if (baseKey != ImGuiKey_None) chord |= baseKey;
|
||||
|
||||
return chord;
|
||||
}
|
||||
|
||||
float row_widget_width_get(int count, float width)
|
||||
{
|
||||
return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count;
|
||||
}
|
||||
|
||||
ImVec2 widget_size_with_row_get(int count, float width) { return ImVec2(row_widget_width_get(count, width), 0); }
|
||||
|
||||
float footer_height_get(int itemCount)
|
||||
{
|
||||
return ImGui::GetTextLineHeightWithSpacing() * itemCount + ImGui::GetStyle().WindowPadding.y +
|
||||
ImGui::GetStyle().ItemSpacing.y * (itemCount);
|
||||
}
|
||||
|
||||
ImVec2 footer_size_get(int itemCount)
|
||||
{
|
||||
return ImVec2(ImGui::GetContentRegionAvail().x, footer_height_get(itemCount));
|
||||
}
|
||||
|
||||
ImVec2 size_without_footer_get(int rowCount)
|
||||
{
|
||||
return ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - footer_height_get(rowCount));
|
||||
}
|
||||
|
||||
ImVec2 child_size_get(int rowCount)
|
||||
{
|
||||
return ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
(ImGui::GetFrameHeightWithSpacing() * rowCount) + (ImGui::GetStyle().WindowPadding.y * 2.0f));
|
||||
}
|
||||
|
||||
ImVec2 icon_size_get()
|
||||
{
|
||||
return ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing());
|
||||
}
|
||||
|
||||
bool shortcut(ImGuiKeyChord chord, shortcut::Type type)
|
||||
{
|
||||
if (chord == ImGuiKey_None) return false;
|
||||
|
||||
if (ImGui::GetTopMostPopupModal() != nullptr &&
|
||||
(type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET))
|
||||
return false;
|
||||
|
||||
int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal
|
||||
: ImGuiInputFlags_RouteFocused;
|
||||
flags |= ImGuiInputFlags_Repeat;
|
||||
|
||||
if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET)
|
||||
{
|
||||
ImGui::SetNextItemShortcut(chord, flags);
|
||||
return false;
|
||||
}
|
||||
|
||||
return ImGui::Shortcut(chord, flags);
|
||||
}
|
||||
|
||||
MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; }
|
||||
|
||||
void MultiSelectStorage::start(size_t size, ImGuiMultiSelectFlags flags)
|
||||
{
|
||||
internal.UserData = this;
|
||||
|
||||
io = ImGui::BeginMultiSelect(flags, this->size(), size);
|
||||
apply();
|
||||
}
|
||||
|
||||
void MultiSelectStorage::apply() { internal.ApplyRequests(io); }
|
||||
|
||||
void MultiSelectStorage::finish()
|
||||
{
|
||||
io = ImGui::EndMultiSelect();
|
||||
apply();
|
||||
}
|
||||
|
||||
void MultiSelectStorage::set_index_map(std::vector<int>* map) { indexMap = map; }
|
||||
|
||||
int MultiSelectStorage::resolve_index(int index) const
|
||||
{
|
||||
if (!indexMap) return index;
|
||||
if (index < 0 || index >= (int)indexMap->size()) return index;
|
||||
return (*indexMap)[index];
|
||||
}
|
||||
|
||||
PopupHelper::PopupHelper(StringType labelId, PopupType type, PopupPosition position)
|
||||
{
|
||||
this->labelId = labelId;
|
||||
this->type = type;
|
||||
this->position = position;
|
||||
}
|
||||
|
||||
void PopupHelper::open()
|
||||
{
|
||||
isOpen = true;
|
||||
isTriggered = true;
|
||||
isJustOpened = true;
|
||||
}
|
||||
|
||||
bool PopupHelper::is_open() { return isOpen; }
|
||||
|
||||
void PopupHelper::trigger()
|
||||
{
|
||||
if (isTriggered) ImGui::OpenPopup(localize.get(labelId));
|
||||
isTriggered = false;
|
||||
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
|
||||
switch (position)
|
||||
{
|
||||
case POPUP_CENTER:
|
||||
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f)));
|
||||
if (POPUP_IS_HEIGHT_SET[type])
|
||||
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type]));
|
||||
else
|
||||
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0));
|
||||
break;
|
||||
case POPUP_BY_ITEM:
|
||||
ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None);
|
||||
case POPUP_BY_CURSOR:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PopupHelper::end() { isJustOpened = false; }
|
||||
|
||||
void PopupHelper::close() { isOpen = false; }
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <glm/glm.hpp>
|
||||
#include <imgui/imgui.h>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "strings.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
constexpr auto DRAG_SPEED = 0.25f;
|
||||
constexpr auto DRAG_SPEED_FAST = 1.00f;
|
||||
constexpr auto STEP = 1.0f;
|
||||
constexpr auto STEP_FAST = 5.0f;
|
||||
|
||||
#define POPUP_LIST \
|
||||
X(POPUP_SMALL, 0.25f, true) \
|
||||
X(POPUP_NORMAL, 0.5f, true) \
|
||||
X(POPUP_TO_CONTENT, 0.0f, true) \
|
||||
X(POPUP_SMALL_NO_HEIGHT, 0.25f, false) \
|
||||
X(POPUP_NORMAL_NO_HEIGHT, 0.5f, false)
|
||||
|
||||
enum PopupType
|
||||
{
|
||||
#define X(name, multiplier, isHeightSet) name,
|
||||
POPUP_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
enum PopupPosition
|
||||
{
|
||||
POPUP_CENTER,
|
||||
POPUP_BY_ITEM,
|
||||
POPUP_BY_CURSOR
|
||||
};
|
||||
|
||||
enum RenameState
|
||||
{
|
||||
RENAME_SELECTABLE,
|
||||
RENAME_BEGIN,
|
||||
RENAME_EDITING,
|
||||
RENAME_FINISHED,
|
||||
RENAME_FORCE_EDIT
|
||||
};
|
||||
|
||||
constexpr float POPUP_MULTIPLIERS[] = {
|
||||
#define X(name, multiplier, isHeightSet) multiplier,
|
||||
POPUP_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr bool POPUP_IS_HEIGHT_SET[] = {
|
||||
#define X(name, multiplier, isHeightSet) isHeightSet,
|
||||
POPUP_LIST
|
||||
#undef X
|
||||
};
|
||||
|
||||
const std::unordered_map<std::string, ImGuiKey> KEY_MAP = {
|
||||
{"A", ImGuiKey_A},
|
||||
{"B", ImGuiKey_B},
|
||||
{"C", ImGuiKey_C},
|
||||
{"D", ImGuiKey_D},
|
||||
{"E", ImGuiKey_E},
|
||||
{"F", ImGuiKey_F},
|
||||
{"G", ImGuiKey_G},
|
||||
{"H", ImGuiKey_H},
|
||||
{"I", ImGuiKey_I},
|
||||
{"J", ImGuiKey_J},
|
||||
{"K", ImGuiKey_K},
|
||||
{"L", ImGuiKey_L},
|
||||
{"M", ImGuiKey_M},
|
||||
{"N", ImGuiKey_N},
|
||||
{"O", ImGuiKey_O},
|
||||
{"P", ImGuiKey_P},
|
||||
{"Q", ImGuiKey_Q},
|
||||
{"R", ImGuiKey_R},
|
||||
{"S", ImGuiKey_S},
|
||||
{"T", ImGuiKey_T},
|
||||
{"U", ImGuiKey_U},
|
||||
{"V", ImGuiKey_V},
|
||||
{"W", ImGuiKey_W},
|
||||
{"X", ImGuiKey_X},
|
||||
{"Y", ImGuiKey_Y},
|
||||
{"Z", ImGuiKey_Z},
|
||||
|
||||
{"0", ImGuiKey_0},
|
||||
{"1", ImGuiKey_1},
|
||||
{"2", ImGuiKey_2},
|
||||
{"3", ImGuiKey_3},
|
||||
{"4", ImGuiKey_4},
|
||||
{"5", ImGuiKey_5},
|
||||
{"6", ImGuiKey_6},
|
||||
{"7", ImGuiKey_7},
|
||||
{"8", ImGuiKey_8},
|
||||
{"9", ImGuiKey_9},
|
||||
|
||||
{"Num0", ImGuiKey_Keypad0},
|
||||
{"Num1", ImGuiKey_Keypad1},
|
||||
{"Num2", ImGuiKey_Keypad2},
|
||||
{"Num3", ImGuiKey_Keypad3},
|
||||
{"Num4", ImGuiKey_Keypad4},
|
||||
{"Num5", ImGuiKey_Keypad5},
|
||||
{"Num6", ImGuiKey_Keypad6},
|
||||
{"Num7", ImGuiKey_Keypad7},
|
||||
{"Num8", ImGuiKey_Keypad8},
|
||||
{"Num9", ImGuiKey_Keypad9},
|
||||
{"NumAdd", ImGuiKey_KeypadAdd},
|
||||
{"NumSubtract", ImGuiKey_KeypadSubtract},
|
||||
{"NumMultiply", ImGuiKey_KeypadMultiply},
|
||||
{"NumDivide", ImGuiKey_KeypadDivide},
|
||||
{"NumEnter", ImGuiKey_KeypadEnter},
|
||||
{"NumDecimal", ImGuiKey_KeypadDecimal},
|
||||
{"NumEqual", ImGuiKey_KeypadEqual},
|
||||
|
||||
{"F1", ImGuiKey_F1},
|
||||
{"F2", ImGuiKey_F2},
|
||||
{"F3", ImGuiKey_F3},
|
||||
{"F4", ImGuiKey_F4},
|
||||
{"F5", ImGuiKey_F5},
|
||||
{"F6", ImGuiKey_F6},
|
||||
{"F7", ImGuiKey_F7},
|
||||
{"F8", ImGuiKey_F8},
|
||||
{"F9", ImGuiKey_F9},
|
||||
{"F10", ImGuiKey_F10},
|
||||
{"F11", ImGuiKey_F11},
|
||||
{"F12", ImGuiKey_F12},
|
||||
|
||||
{"Up", ImGuiKey_UpArrow},
|
||||
{"Down", ImGuiKey_DownArrow},
|
||||
{"Left", ImGuiKey_LeftArrow},
|
||||
{"Right", ImGuiKey_RightArrow},
|
||||
|
||||
{"Space", ImGuiKey_Space},
|
||||
{"Enter", ImGuiKey_Enter},
|
||||
{"Escape", ImGuiKey_Escape},
|
||||
{"Tab", ImGuiKey_Tab},
|
||||
{"Backspace", ImGuiKey_Backspace},
|
||||
{"Delete", ImGuiKey_Delete},
|
||||
{"Insert", ImGuiKey_Insert},
|
||||
{"Home", ImGuiKey_Home},
|
||||
{"End", ImGuiKey_End},
|
||||
{"PageUp", ImGuiKey_PageUp},
|
||||
{"PageDown", ImGuiKey_PageDown},
|
||||
|
||||
{"Minus", ImGuiKey_Minus},
|
||||
{"Equal", ImGuiKey_Equal},
|
||||
{"LeftBracket", ImGuiKey_LeftBracket},
|
||||
{"RightBracket", ImGuiKey_RightBracket},
|
||||
{"Semicolon", ImGuiKey_Semicolon},
|
||||
{"Apostrophe", ImGuiKey_Apostrophe},
|
||||
{"Comma", ImGuiKey_Comma},
|
||||
{"Period", ImGuiKey_Period},
|
||||
{"Slash", ImGuiKey_Slash},
|
||||
{"Backslash", ImGuiKey_Backslash},
|
||||
{"GraveAccent", ImGuiKey_GraveAccent},
|
||||
|
||||
// Legacy aliases for older saved shortcut strings.
|
||||
{"Keypad0", ImGuiKey_Keypad0},
|
||||
{"Keypad1", ImGuiKey_Keypad1},
|
||||
{"Keypad2", ImGuiKey_Keypad2},
|
||||
{"Keypad3", ImGuiKey_Keypad3},
|
||||
{"Keypad4", ImGuiKey_Keypad4},
|
||||
{"Keypad5", ImGuiKey_Keypad5},
|
||||
{"Keypad6", ImGuiKey_Keypad6},
|
||||
{"Keypad7", ImGuiKey_Keypad7},
|
||||
{"Keypad8", ImGuiKey_Keypad8},
|
||||
{"Keypad9", ImGuiKey_Keypad9},
|
||||
{"KeypadAdd", ImGuiKey_KeypadAdd},
|
||||
{"KeypadSubtract", ImGuiKey_KeypadSubtract},
|
||||
{"KeypadMultiply", ImGuiKey_KeypadMultiply},
|
||||
{"KeypadDivide", ImGuiKey_KeypadDivide},
|
||||
{"KeypadEnter", ImGuiKey_KeypadEnter},
|
||||
{"KeypadDecimal", ImGuiKey_KeypadDecimal},
|
||||
{"KeypadEqual", ImGuiKey_KeypadEqual},
|
||||
{"UpArrow", ImGuiKey_UpArrow},
|
||||
{"DownArrow", ImGuiKey_DownArrow},
|
||||
{"LeftArrow", ImGuiKey_LeftArrow},
|
||||
{"RightArrow", ImGuiKey_RightArrow},
|
||||
};
|
||||
|
||||
const std::unordered_map<std::string, ImGuiKey> MOD_MAP = {
|
||||
{"Ctrl", ImGuiMod_Ctrl},
|
||||
{"Shift", ImGuiMod_Shift},
|
||||
{"Alt", ImGuiMod_Alt},
|
||||
{"Super", ImGuiMod_Super},
|
||||
};
|
||||
|
||||
void theme_set(types::theme::Type theme);
|
||||
std::string chord_to_string(ImGuiKeyChord);
|
||||
ImGuiKeyChord string_to_chord(const std::string&);
|
||||
float row_widget_width_get(int, float = ImGui::GetContentRegionAvail().x);
|
||||
ImVec2 widget_size_with_row_get(int, float = ImGui::GetContentRegionAvail().x);
|
||||
float footer_height_get(int = 1);
|
||||
ImVec2 footer_size_get(int = 1);
|
||||
ImVec2 size_without_footer_get(int = 1);
|
||||
ImVec2 child_size_get(int = 1);
|
||||
int input_text_callback(ImGuiInputTextCallbackData*);
|
||||
bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0);
|
||||
bool input_text_path(const char*, std::filesystem::path*, ImGuiInputTextFlags = 0);
|
||||
bool input_int_range(const char*, int&, int, int, int = STEP, int = STEP_FAST, ImGuiInputTextFlags = 0);
|
||||
bool input_int2_range(const char*, glm::ivec2&, glm::ivec2, glm::ivec2, ImGuiInputTextFlags = 0);
|
||||
bool input_float_range(const char*, float&, float, float, float = STEP, float = STEP_FAST, const char* = "%.3f",
|
||||
ImGuiInputTextFlags = 0);
|
||||
types::edit::Type drag_int_persistent(const char*, int*, float = DRAG_SPEED, int = {}, int = {}, const char* = "%d",
|
||||
ImGuiSliderFlags = 0);
|
||||
types::edit::Type drag_float_persistent(const char*, float*, float = DRAG_SPEED, float = {}, float = {},
|
||||
const char* = "%.3f", ImGuiSliderFlags = 0);
|
||||
types::edit::Type drag_float2_persistent(const char*, glm::vec2*, float = DRAG_SPEED, float = {}, float = {},
|
||||
const char* = "%.3f", ImGuiSliderFlags = 0);
|
||||
types::edit::Type color_edit3_persistent(const char*, glm::vec3*, ImGuiColorEditFlags = 0);
|
||||
types::edit::Type color_edit4_persistent(const char*, glm::vec4*, ImGuiColorEditFlags = 0);
|
||||
bool combo_negative_one_indexed(const std::string&, int*, std::vector<const char*>&);
|
||||
bool combo_id_mapped(const std::string&, int*, const std::vector<int>&, std::vector<const char*>&);
|
||||
std::string& selectable_input_text_id();
|
||||
bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected,
|
||||
ImGuiSelectableFlags flags, RenameState& state);
|
||||
void set_item_tooltip_shortcut(const char*, const std::string& = {});
|
||||
void external_storage_set(ImGuiSelectionExternalStorage*, int, bool);
|
||||
void render_checker_background(ImDrawList*, ImVec2, ImVec2, glm::vec2, float);
|
||||
ImVec2 icon_size_get();
|
||||
bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET);
|
||||
|
||||
class MultiSelectStorage : public std::set<int>
|
||||
{
|
||||
public:
|
||||
ImGuiSelectionExternalStorage internal{};
|
||||
ImGuiMultiSelectIO* io{};
|
||||
std::vector<int>* indexMap{};
|
||||
|
||||
using std::set<int>::set;
|
||||
using std::set<int>::operator=;
|
||||
using std::set<int>::begin;
|
||||
using std::set<int>::rbegin;
|
||||
using std::set<int>::end;
|
||||
using std::set<int>::size;
|
||||
using std::set<int>::insert;
|
||||
using std::set<int>::erase;
|
||||
|
||||
MultiSelectStorage();
|
||||
void start(size_t,
|
||||
ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ScopeWindow);
|
||||
void apply();
|
||||
void finish();
|
||||
void set_index_map(std::vector<int>*);
|
||||
int resolve_index(int) const;
|
||||
};
|
||||
|
||||
class PopupHelper
|
||||
{
|
||||
public:
|
||||
StringType labelId{};
|
||||
PopupType type{};
|
||||
PopupPosition position{};
|
||||
bool isOpen{};
|
||||
bool isTriggered{};
|
||||
bool isJustOpened{};
|
||||
|
||||
PopupHelper(StringType, PopupType = POPUP_NORMAL, PopupPosition = POPUP_CENTER);
|
||||
const char* label() const { return localize.get(labelId); }
|
||||
bool is_open();
|
||||
void open();
|
||||
void trigger();
|
||||
void end();
|
||||
void close();
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
@@ -16,11 +16,11 @@ namespace anm2ed::imgui::popup
|
||||
addItemSpritesheetID = {};
|
||||
}
|
||||
|
||||
std::set<int> ItemProperties::unused_items_get(anm2::Anm2& anm2, anm2::Animation* animation, anm2::Type type)
|
||||
std::set<int> ItemProperties::unused_items_get(Anm2& anm2, const Element* animation, int type)
|
||||
{
|
||||
if (!animation) return {};
|
||||
if (type == anm2::LAYER) return anm2.layers_unused(*animation);
|
||||
if (type == anm2::NULL_) return anm2.nulls_unused(*animation);
|
||||
if (type == LAYER) return anm2.element_unused(ElementType::LAYER_ELEMENT, *animation);
|
||||
if (type == NULL_) return anm2.element_unused(ElementType::NULL_ELEMENT, *animation);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ namespace anm2ed::imgui::popup
|
||||
popup.open();
|
||||
}
|
||||
|
||||
void ItemProperties::update(Manager& manager, Settings& settings, Document& document, anm2::Animation* animation,
|
||||
anm2::Reference& reference,
|
||||
const std::function<void(anm2::Type, int)>& referenceSetItem)
|
||||
void ItemProperties::update(Manager& manager, Settings& settings, Document& document, Reference& reference,
|
||||
const std::function<void(int, int)>& referenceSetItem)
|
||||
{
|
||||
auto& anm2 = document.anm2;
|
||||
auto animation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex);
|
||||
|
||||
popup.trigger();
|
||||
|
||||
@@ -70,18 +70,18 @@ namespace anm2ed::imgui::popup
|
||||
spaced_pair(
|
||||
[&]()
|
||||
{
|
||||
ImGui::RadioButton(localize.get(LABEL_LAYER), &type, anm2::LAYER);
|
||||
ImGui::RadioButton(localize.get(LABEL_LAYER), &type, LAYER);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_TYPE));
|
||||
},
|
||||
[&]()
|
||||
{
|
||||
ImGui::RadioButton(localize.get(LABEL_NULL), &type, anm2::NULL_);
|
||||
ImGui::RadioButton(localize.get(LABEL_NULL), &type, NULL_);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_TYPE));
|
||||
});
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_SOURCE));
|
||||
|
||||
auto isUnusedItems = animation && !unused_items_get(anm2, animation, (anm2::Type)type).empty();
|
||||
auto isUnusedItems = animation && !unused_items_get(anm2, animation, (int)type).empty();
|
||||
spaced_pair(
|
||||
[&]()
|
||||
{
|
||||
@@ -117,13 +117,13 @@ namespace anm2ed::imgui::popup
|
||||
|
||||
input_text_string(localize.get(BASIC_NAME), &addItemName);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME));
|
||||
if (type == anm2::LAYER)
|
||||
if (type == LAYER)
|
||||
{
|
||||
combo_id_mapped(localize.get(LABEL_SPRITESHEET), &addItemSpritesheetID, document.spritesheet.ids,
|
||||
document.spritesheet.labels);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET));
|
||||
}
|
||||
else if (type == anm2::NULL_)
|
||||
else if (type == NULL_)
|
||||
{
|
||||
ImGui::Checkbox(localize.get(LABEL_RECT), &addItemIsShowRect);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_RECT));
|
||||
@@ -131,11 +131,11 @@ namespace anm2ed::imgui::popup
|
||||
}
|
||||
else if (animation)
|
||||
{
|
||||
ImGui::SeparatorText(localize.get(type == anm2::LAYER ? LABEL_LAYER : LABEL_NULL));
|
||||
ImGui::SeparatorText(localize.get(type == LAYER ? LABEL_LAYER : LABEL_NULL));
|
||||
|
||||
if (ImGui::BeginChild("##Existing Items", ImVec2(0, 0)))
|
||||
{
|
||||
auto unusedItems = unused_items_get(anm2, animation, (anm2::Type)type);
|
||||
auto unusedItems = unused_items_get(anm2, animation, (int)type);
|
||||
if (addItemID != -1 && !unusedItems.contains(addItemID)) addItemID = -1;
|
||||
|
||||
for (auto id : unusedItems)
|
||||
@@ -144,30 +144,30 @@ namespace anm2ed::imgui::popup
|
||||
|
||||
ImGui::PushID(id);
|
||||
|
||||
if (type == anm2::LAYER)
|
||||
if (type == LAYER)
|
||||
{
|
||||
if (auto it = anm2.content.layers.find(id); it != anm2.content.layers.end())
|
||||
if (auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, id))
|
||||
{
|
||||
auto& layer = it->second;
|
||||
auto label = std::vformat(localize.get(FORMAT_LAYER),
|
||||
std::make_format_args(id, layer.name, layer.spritesheetID));
|
||||
std::make_format_args(id, layer->name, layer->spritesheetId));
|
||||
if (ImGui::Selectable(label.c_str(), isSelected))
|
||||
{
|
||||
addItemID = id;
|
||||
addItemSpritesheetID = layer.spritesheetID;
|
||||
addItemSpritesheetID = layer->spritesheetId;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == anm2::NULL_)
|
||||
else if (type == NULL_)
|
||||
{
|
||||
if (auto it = anm2.content.nulls.find(id); it != anm2.content.nulls.end())
|
||||
auto nulls = anm2.element_get(ElementType::NULLS);
|
||||
auto null = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr;
|
||||
if (null)
|
||||
{
|
||||
auto& null = it->second;
|
||||
auto label = std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name));
|
||||
auto label = std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null->name));
|
||||
if (ImGui::Selectable(label.c_str(), isSelected))
|
||||
{
|
||||
addItemID = id;
|
||||
addItemIsShowRect = null.isShowRect;
|
||||
addItemIsShowRect = null->isShowRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,20 +186,35 @@ namespace anm2ed::imgui::popup
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize))
|
||||
{
|
||||
anm2::Reference addReference{};
|
||||
int insertBeforeID = reference.itemType == anm2::LAYER ? reference.itemID : -1;
|
||||
auto queuedType = type;
|
||||
auto queuedDestination = destination;
|
||||
auto queuedAnimationIndex = reference.animationIndex;
|
||||
auto queuedInsertBeforeID = reference.itemType == LAYER ? reference.itemID : -1;
|
||||
auto queuedAddItemID = addItemID;
|
||||
auto queuedAddItemName = addItemName;
|
||||
auto queuedAddItemSpritesheetID = addItemSpritesheetID;
|
||||
auto queuedAddItemIsShowRect = addItemIsShowRect;
|
||||
auto queuedReferenceSetItem = referenceSetItem;
|
||||
|
||||
document.snapshot(localize.get(EDIT_ADD_ITEM));
|
||||
if (type == anm2::LAYER)
|
||||
addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, insertBeforeID,
|
||||
addItemName, addItemSpritesheetID, (destination::Type)destination);
|
||||
else if (type == anm2::NULL_)
|
||||
addReference = anm2.null_animation_add({reference.animationIndex, anm2::NULL_, addItemID}, addItemName,
|
||||
addItemIsShowRect, (destination::Type)destination);
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
int addId{-1};
|
||||
|
||||
document.change(Document::ITEMS);
|
||||
document.snapshot(localize.get(EDIT_ADD_ITEM));
|
||||
if (queuedType == LAYER)
|
||||
addId = document.anm2.layer_animation_add(
|
||||
queuedAnimationIndex, queuedAddItemID, queuedInsertBeforeID, queuedAddItemName,
|
||||
queuedAddItemSpritesheetID, (destination::Type)queuedDestination);
|
||||
else if (queuedType == NULL_)
|
||||
addId = document.anm2.null_animation_add(queuedAnimationIndex, queuedAddItemID,
|
||||
queuedAddItemName, queuedAddItemIsShowRect,
|
||||
(destination::Type)queuedDestination);
|
||||
|
||||
referenceSetItem(addReference.itemType, addReference.itemID);
|
||||
document.anm2_change(Document::ITEMS);
|
||||
|
||||
if (addId != -1) queuedReferenceSetItem((int)queuedType, addId);
|
||||
}});
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -20,11 +20,10 @@ namespace anm2ed::imgui::popup
|
||||
int addItemSpritesheetID{-1};
|
||||
|
||||
void reset();
|
||||
std::set<int> unused_items_get(anm2::Anm2&, anm2::Animation*, anm2::Type);
|
||||
std::set<int> unused_items_get(Anm2&, const Element*, int);
|
||||
|
||||
public:
|
||||
void open();
|
||||
void update(Manager&, Settings&, Document&, anm2::Animation*, anm2::Reference&,
|
||||
const std::function<void(anm2::Type, int)>&);
|
||||
void update(Manager&, Settings&, Document&, Reference&, const std::function<void(int, int)>&);
|
||||
};
|
||||
}
|
||||
|
||||
+84
-41
@@ -9,7 +9,7 @@
|
||||
|
||||
#include "document.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "types.hpp"
|
||||
@@ -25,35 +25,54 @@ namespace anm2ed::imgui
|
||||
{
|
||||
auto* document = manager.get(index);
|
||||
return document && settings.fileIsSpecialInterpolatedFramesOnSaveReminder &&
|
||||
document->anm2.has_special_interpolated_frames();
|
||||
document->anm2.is_special_interpolated_frames();
|
||||
}
|
||||
|
||||
void Taskbar::save_execute(Manager& manager, Settings& settings, const PendingSave& request, bool bakeFrames)
|
||||
bool Taskbar::save_execute(Manager& manager, Settings& settings, const PendingSave& request, bool bakeFrames)
|
||||
{
|
||||
manager.save(request.index, request.path, (anm2::Compatibility)settings.fileCompatibility, bakeFrames,
|
||||
settings.bakeIsRoundScale, settings.bakeIsRoundRotation);
|
||||
return manager.save(request.index, request.path, (Compatibility)settings.fileCompatibility, bakeFrames,
|
||||
settings.bakeIsRoundScale, settings.bakeIsRoundRotation);
|
||||
}
|
||||
|
||||
bool Taskbar::save_request(Manager& manager, Settings& settings, int index, const std::filesystem::path& path)
|
||||
void Taskbar::save_enqueue(Manager& manager, Settings& settings, const PendingSave& request, bool bakeFrames)
|
||||
{
|
||||
auto index = request.index;
|
||||
auto path = request.path;
|
||||
auto compatibility = (Compatibility)settings.fileCompatibility;
|
||||
auto isRoundScale = settings.bakeIsRoundScale;
|
||||
auto isRoundRotation = settings.bakeIsRoundRotation;
|
||||
|
||||
manager.command_push({.runManager =
|
||||
[=](Manager& manager)
|
||||
{ manager.save(index, path, compatibility, bakeFrames, isRoundScale, isRoundRotation); }});
|
||||
}
|
||||
|
||||
bool Taskbar::save_request(Manager& manager, Settings& settings, int index, const std::filesystem::path& path,
|
||||
bool isQueued)
|
||||
{
|
||||
auto* document = manager.get(index);
|
||||
if (!document) return false;
|
||||
|
||||
if (settings.fileIsSpecialInterpolatedFramesOnSaveReminder && document->anm2.has_special_interpolated_frames())
|
||||
if (settings.fileIsSpecialInterpolatedFramesOnSaveReminder && document->anm2.is_special_interpolated_frames())
|
||||
{
|
||||
pendingSave = {.index = index,
|
||||
.path = path,
|
||||
.isOpen = true,
|
||||
.disableReminder = false,
|
||||
.autoBakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave};
|
||||
.autoBakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave,
|
||||
.isQueued = isQueued};
|
||||
specialInterpolatedFramesReminderPopup.open();
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingSave request{.index = index, .path = path};
|
||||
auto bakeFrames = settings.fileBakeSpecialInterpolatedFramesOnSave;
|
||||
save_execute(manager, settings, request, bakeFrames);
|
||||
return true;
|
||||
if (isQueued)
|
||||
{
|
||||
save_enqueue(manager, settings, request, bakeFrames);
|
||||
return true;
|
||||
}
|
||||
return save_execute(manager, settings, request, bakeFrames);
|
||||
}
|
||||
|
||||
bool Taskbar::save_manual(Manager& manager, Settings& settings, int index, const std::filesystem::path& path)
|
||||
@@ -64,19 +83,25 @@ namespace anm2ed::imgui
|
||||
void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting)
|
||||
{
|
||||
auto document = manager.get();
|
||||
auto animation = document ? document->animation_get() : nullptr;
|
||||
auto item = document ? document->item_get() : nullptr;
|
||||
auto itemType = document ? (ItemType)document->reference.itemType : ItemType::NONE;
|
||||
auto animation =
|
||||
document ? document->anm2.element_get(ElementType::ANIMATION, document->reference.animationIndex) : nullptr;
|
||||
auto item =
|
||||
document ? document->anm2.element_get(document->reference.animationIndex, itemType, document->reference.itemID)
|
||||
: nullptr;
|
||||
auto frames = document ? &document->frames : nullptr;
|
||||
bool hasRegions = false;
|
||||
if (document)
|
||||
{
|
||||
for (auto& spritesheet : document->anm2.content.spritesheets | std::views::values)
|
||||
if (auto spritesheets = document->anm2.element_get(ElementType::SPRITESHEETS))
|
||||
{
|
||||
if (!spritesheet.regions.empty())
|
||||
{
|
||||
hasRegions = true;
|
||||
break;
|
||||
}
|
||||
for (auto& spritesheet : spritesheets->children)
|
||||
for (auto& child : spritesheet.children)
|
||||
if (spritesheet.type == ElementType::SPRITESHEET && child.type == ElementType::REGION)
|
||||
{
|
||||
hasRegions = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +111,10 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::BeginMenu(localize.get(LABEL_FILE_MENU)))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(BASIC_NEW), settings.shortcutNew.c_str())) dialog.file_save(Dialog::ANM2_NEW);
|
||||
if (ImGui::MenuItem(localize.get(BASIC_NEW), settings.shortcutNew.c_str()))
|
||||
dialog.file_save(Dialog::ANM2_CREATE);
|
||||
if (ImGui::MenuItem(localize.get(BASIC_OPEN), settings.shortcutOpen.c_str()))
|
||||
dialog.file_open(Dialog::ANM2_OPEN);
|
||||
dialog.file_open(Dialog::ANM2_OPEN, true);
|
||||
|
||||
auto recentFiles = manager.recent_files_ordered();
|
||||
if (ImGui::BeginMenu(localize.get(LABEL_OPEN_RECENT), !recentFiles.empty()))
|
||||
@@ -99,7 +125,8 @@ namespace anm2ed::imgui
|
||||
auto fileNameUtf8 = path::to_utf8(file.filename());
|
||||
auto filePathUtf8 = path::to_utf8(file);
|
||||
auto label = std::format(FILE_LABEL_FORMAT, fileNameUtf8, filePathUtf8);
|
||||
if (ImGui::MenuItem(label.c_str())) manager.open(file);
|
||||
if (ImGui::MenuItem(label.c_str()))
|
||||
manager.command_push({.runManager = [file](Manager& manager) { manager.open(file); }});
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
@@ -112,11 +139,11 @@ namespace anm2ed::imgui
|
||||
if (ImGui::MenuItem(localize.get(BASIC_SAVE), settings.shortcutSave.c_str(), false, document))
|
||||
{
|
||||
if (save_requires_special_prompt(manager, settings, manager.selected))
|
||||
save_request(manager, settings, manager.selected, document->path);
|
||||
save_request(manager, settings, manager.selected, document->path, true);
|
||||
else if (settings.fileIsWarnOverwrite)
|
||||
overwritePopup.open();
|
||||
else
|
||||
save_request(manager, settings, manager.selected, document->path);
|
||||
save_request(manager, settings, manager.selected, document->path, true);
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_SAVE_AS), settings.shortcutSaveAs.c_str(), false, document))
|
||||
@@ -128,33 +155,38 @@ namespace anm2ed::imgui
|
||||
if (ImGui::MenuItem(localize.get(LABEL_EXIT), settings.shortcutExit.c_str())) isQuitting = true;
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (dialog.is_selected(Dialog::ANM2_NEW))
|
||||
if (dialog.is_selected(Dialog::ANM2_CREATE))
|
||||
{
|
||||
manager.new_(dialog.path);
|
||||
auto path = dialog.path;
|
||||
manager.command_push({.runManager = [path](Manager& manager) { manager.new_(path); }});
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
if (dialog.is_selected(Dialog::ANM2_OPEN))
|
||||
{
|
||||
manager.open(dialog.path);
|
||||
auto paths = dialog.paths;
|
||||
manager.command_push({.runManager =
|
||||
[paths](Manager& manager)
|
||||
{
|
||||
for (auto& path : paths) manager.open(path);
|
||||
}});
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
if (dialog.is_selected(Dialog::ANM2_SAVE))
|
||||
{
|
||||
save_request(manager, settings, manager.selected, dialog.path);
|
||||
save_request(manager, settings, manager.selected, dialog.path, true);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu(localize.get(LABEL_WIZARD_MENU)))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID), nullptr, false,
|
||||
item && document->reference.itemType == anm2::LAYER))
|
||||
item && itemType == ItemType::LAYER))
|
||||
generatePopup.open();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_WIZARD_GENERATE_ANIMATION_FROM_GRID));
|
||||
|
||||
bool isChangeAllFramesAvailable =
|
||||
frames && !frames->selection.empty() && document->reference.itemType != anm2::TRIGGER;
|
||||
bool isChangeAllFramesAvailable = frames && !frames->selection.empty() && itemType != ItemType::TRIGGER;
|
||||
bool isChangeAllAnimationsAvailable = document && !document->animation.selection.empty();
|
||||
if (ImGui::MenuItem(localize.get(LABEL_CHANGE_ALL_FRAME_PROPERTIES), nullptr, false,
|
||||
isChangeAllFramesAvailable || isChangeAllAnimationsAvailable))
|
||||
@@ -163,8 +195,13 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_SCAN_AND_SET_REGIONS), nullptr, false, document && hasRegions))
|
||||
{
|
||||
DOCUMENT_EDIT_PTR(document, localize.get(EDIT_SCAN_AND_SET_REGIONS), Document::FRAMES,
|
||||
document->anm2.scan_and_set_regions());
|
||||
manager.command_push({manager.selected,
|
||||
[](Manager&, Document& document)
|
||||
{
|
||||
document.snapshot(localize.get(EDIT_SCAN_AND_SET_REGIONS));
|
||||
document.scan_and_set_regions();
|
||||
document.change(Document::FRAMES);
|
||||
}});
|
||||
toasts.push(localize.get(TOAST_SCAN_AND_SET_REGIONS));
|
||||
logger.info(localize.get(TOAST_SCAN_AND_SET_REGIONS, anm2ed::ENGLISH));
|
||||
}
|
||||
@@ -223,7 +260,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (document)
|
||||
{
|
||||
generateAnimationFromGrid.update(*document, resources, settings);
|
||||
generateAnimationFromGrid.update(manager, *document, resources, settings);
|
||||
if (generateAnimationFromGrid.isEnd) generatePopup.close();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -235,7 +272,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (document)
|
||||
{
|
||||
changeAllFrameProperties.update(*document, settings, true);
|
||||
changeAllFrameProperties.update(manager, *document, settings, true);
|
||||
if (changeAllFrameProperties.isChanged) changePopup.close();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -285,7 +322,7 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
|
||||
{
|
||||
save_request(manager, settings);
|
||||
save_request(manager, settings, manager.selected, {}, true);
|
||||
overwritePopup.close();
|
||||
}
|
||||
|
||||
@@ -312,7 +349,10 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (pendingSave.disableReminder) settings.fileIsSpecialInterpolatedFramesOnSaveReminder = false;
|
||||
settings.fileBakeSpecialInterpolatedFramesOnSave = pendingSave.autoBakeFrames;
|
||||
save_execute(manager, settings, pendingSave, true);
|
||||
if (pendingSave.isQueued)
|
||||
save_enqueue(manager, settings, pendingSave, true);
|
||||
else
|
||||
save_execute(manager, settings, pendingSave, true);
|
||||
pendingSave = {};
|
||||
specialInterpolatedFramesReminderPopup.close();
|
||||
}
|
||||
@@ -322,7 +362,10 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (pendingSave.disableReminder) settings.fileIsSpecialInterpolatedFramesOnSaveReminder = false;
|
||||
settings.fileBakeSpecialInterpolatedFramesOnSave = pendingSave.autoBakeFrames;
|
||||
save_execute(manager, settings, pendingSave, false);
|
||||
if (pendingSave.isQueued)
|
||||
save_enqueue(manager, settings, pendingSave, false);
|
||||
else
|
||||
save_execute(manager, settings, pendingSave, false);
|
||||
pendingSave = {};
|
||||
specialInterpolatedFramesReminderPopup.close();
|
||||
}
|
||||
@@ -340,16 +383,16 @@ namespace anm2ed::imgui
|
||||
|
||||
aboutPopup.end();
|
||||
|
||||
if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_NEW);
|
||||
if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(Dialog::ANM2_OPEN);
|
||||
if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_CREATE);
|
||||
if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(Dialog::ANM2_OPEN, true);
|
||||
if (shortcut(manager.chords[SHORTCUT_SAVE], shortcut::GLOBAL))
|
||||
{
|
||||
if (save_requires_special_prompt(manager, settings))
|
||||
save_request(manager, settings);
|
||||
save_request(manager, settings, manager.selected, {}, true);
|
||||
else if (settings.fileIsWarnOverwrite)
|
||||
overwritePopup.open();
|
||||
else
|
||||
save_request(manager, settings);
|
||||
save_request(manager, settings, manager.selected, {}, true);
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "canvas.hpp"
|
||||
#include "dialog.hpp"
|
||||
#include "imgui_.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
@@ -27,6 +27,7 @@ namespace anm2ed::imgui
|
||||
bool isOpen{};
|
||||
bool disableReminder{};
|
||||
bool autoBakeFrames{};
|
||||
bool isQueued{};
|
||||
};
|
||||
|
||||
wizard::ChangeAllFrameProperties changeAllFrameProperties{};
|
||||
@@ -49,8 +50,9 @@ namespace anm2ed::imgui
|
||||
PendingSave pendingSave{};
|
||||
|
||||
bool save_requires_special_prompt(Manager&, Settings&, int = -1) const;
|
||||
void save_execute(Manager&, Settings&, const PendingSave&, bool);
|
||||
bool save_request(Manager&, Settings&, int = -1, const std::filesystem::path& = {});
|
||||
bool save_execute(Manager&, Settings&, const PendingSave&, bool);
|
||||
void save_enqueue(Manager&, Settings&, const PendingSave&, bool);
|
||||
bool save_request(Manager&, Settings&, int = -1, const std::filesystem::path& = {}, bool = false);
|
||||
|
||||
public:
|
||||
float height{};
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
#include <format>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <system_error>
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "actions.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
#include "log.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "math.hpp"
|
||||
#include "path.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "tool.hpp"
|
||||
@@ -103,7 +103,7 @@ namespace anm2ed::imgui
|
||||
pixels[index + 2] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 2] / alphaUnit), 0.0f, 255.0f);
|
||||
}
|
||||
}
|
||||
bool render_audio_stream_generate(AudioStream& audioStream, std::map<int, anm2::Sound>& sounds,
|
||||
bool render_audio_stream_generate(AudioStream& audioStream, std::map<int, Audio>& sounds,
|
||||
const std::vector<int>& frameSoundIDs, int fps)
|
||||
{
|
||||
audioStream.stream.clear();
|
||||
@@ -122,7 +122,7 @@ namespace anm2ed::imgui
|
||||
|
||||
for (auto soundID : frameSoundIDs)
|
||||
{
|
||||
if (soundID != -1 && sounds.contains(soundID)) sounds.at(soundID).audio.play(false, mixer);
|
||||
if (soundID != -1 && sounds.contains(soundID)) sounds.at(soundID).play(false, mixer);
|
||||
|
||||
sampleFrameAccumulator += framesPerStep;
|
||||
auto sampleFramesToGenerate = (int)std::floor(sampleFrameAccumulator);
|
||||
@@ -133,7 +133,7 @@ namespace anm2ed::imgui
|
||||
if (!MIX_Generate(mixer, frameBuffer.data(), (int)(frameBuffer.size() * sizeof(float))))
|
||||
{
|
||||
for (auto& [_, sound] : sounds)
|
||||
sound.audio.track_detach(mixer);
|
||||
sound.track_detach(mixer);
|
||||
MIX_DestroyMixer(mixer);
|
||||
audioStream.stream.clear();
|
||||
return false;
|
||||
@@ -143,7 +143,7 @@ namespace anm2ed::imgui
|
||||
}
|
||||
|
||||
for (auto& [_, sound] : sounds)
|
||||
sound.audio.track_detach(mixer);
|
||||
sound.track_detach(mixer);
|
||||
MIX_DestroyMixer(mixer);
|
||||
return true;
|
||||
}
|
||||
@@ -151,6 +151,8 @@ namespace anm2ed::imgui
|
||||
|
||||
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
|
||||
|
||||
bool AnimationPreview::is_focused_get() const { return isFocused; }
|
||||
|
||||
void AnimationPreview::tick(Manager& manager, Settings& settings, float deltaSeconds)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
@@ -164,8 +166,24 @@ namespace anm2ed::imgui
|
||||
|
||||
auto stop_all_sounds = [&]()
|
||||
{
|
||||
for (auto& sound : anm2.content.sounds | std::views::values)
|
||||
sound.audio.stop(mixer);
|
||||
for (auto& [_, sound] : document.sounds)
|
||||
sound.stop(mixer);
|
||||
};
|
||||
|
||||
auto trigger_sound_id_get = [&](Element* animation, float time)
|
||||
{
|
||||
if (!animation) return -1;
|
||||
auto triggers = animation_item_get(*animation, ItemType::TRIGGER);
|
||||
if (!triggers || !triggers->isVisible) return -1;
|
||||
|
||||
auto trigger = frame_generate(*triggers, time);
|
||||
if (!trigger.isVisible || trigger.soundIds.empty()) return -1;
|
||||
|
||||
auto soundIndex =
|
||||
trigger.soundIds.size() > 1 ? (size_t)math::random_in_range(0.0f, (float)trigger.soundIds.size()) : (size_t)0;
|
||||
soundIndex = std::min(soundIndex, trigger.soundIds.size() - 1);
|
||||
auto soundID = trigger.soundIds[soundIndex];
|
||||
return document.sound_get(soundID) ? soundID : -1;
|
||||
};
|
||||
|
||||
if (manager.isRecording)
|
||||
@@ -261,7 +279,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (settings.timelineIsSound && type != render::GIF)
|
||||
{
|
||||
if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, renderFrameRate))
|
||||
if (!render_audio_stream_generate(audioStream, document.sounds, renderFrameSoundIDs, renderFrameRate))
|
||||
{
|
||||
toasts.push(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED));
|
||||
logger.error("Failed to generate deterministic render audio stream; exporting without audio.");
|
||||
@@ -322,26 +340,13 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (settings.timelineIsSound && renderTempFrames.empty()) audioStream.capture_begin(mixer);
|
||||
auto frameSoundID = -1;
|
||||
if (settings.timelineIsSound && !anm2.content.sounds.empty())
|
||||
if (settings.timelineIsSound && !document.sounds.empty())
|
||||
{
|
||||
auto soundTime = (int)std::floor(playback.time);
|
||||
if (auto animation = document.animation_get(); soundTime != renderFrameSoundTimePrev && animation &&
|
||||
animation->triggers.isVisible &&
|
||||
(!settings.timelineIsOnlyShowLayers || manager.isRecording))
|
||||
{
|
||||
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible)
|
||||
{
|
||||
if (!trigger.soundIDs.empty())
|
||||
{
|
||||
auto soundIndex = trigger.soundIDs.size() > 1
|
||||
? (size_t)math::random_in_range(0.0f, (float)trigger.soundIDs.size())
|
||||
: (size_t)0;
|
||||
soundIndex = std::min(soundIndex, trigger.soundIDs.size() - 1);
|
||||
auto soundID = trigger.soundIDs[soundIndex];
|
||||
if (anm2.content.sounds.contains(soundID)) frameSoundID = soundID;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto animation = anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex);
|
||||
if (soundTime != renderFrameSoundTimePrev && animation &&
|
||||
(!settings.timelineIsOnlyShowLayers || manager.isRecording))
|
||||
frameSoundID = trigger_sound_id_get(animation, playback.time);
|
||||
renderFrameSoundTimePrev = soundTime;
|
||||
}
|
||||
renderFrameSoundIDs.push_back(frameSoundID);
|
||||
@@ -376,7 +381,7 @@ namespace anm2ed::imgui
|
||||
|
||||
if (playback.isPlaying)
|
||||
{
|
||||
auto animation = document.animation_get();
|
||||
auto animation = anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex);
|
||||
auto& isSound = settings.timelineIsSound;
|
||||
auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers;
|
||||
|
||||
@@ -388,27 +393,15 @@ namespace anm2ed::imgui
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!manager.isRecording && !anm2.content.sounds.empty() && isSound)
|
||||
if (!manager.isRecording && !document.sounds.empty() && isSound)
|
||||
{
|
||||
if (animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording))
|
||||
{
|
||||
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible)
|
||||
{
|
||||
if (!trigger.soundIDs.empty())
|
||||
{
|
||||
auto soundIndex = trigger.soundIDs.size() > 1
|
||||
? (size_t)math::random_in_range(0.0f, (float)trigger.soundIDs.size())
|
||||
: (size_t)0;
|
||||
soundIndex = std::min(soundIndex, trigger.soundIDs.size() - 1);
|
||||
auto soundID = trigger.soundIDs[soundIndex];
|
||||
|
||||
if (anm2.content.sounds.contains(soundID)) anm2.content.sounds[soundID].audio.play(false, mixer);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isOnlyShowLayers || manager.isRecording)
|
||||
if (auto soundID = trigger_sound_id_get(animation, playback.time); soundID != -1)
|
||||
if (auto sound = document.sound_get(soundID)) sound->play(false, mixer);
|
||||
}
|
||||
|
||||
auto fps = std::max(anm2.info.fps, 1);
|
||||
auto info = element_first_get(anm2.root, ElementType::INFO);
|
||||
auto fps = std::max(info ? info->fps : 30, 1);
|
||||
playback.tick(fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording,
|
||||
deltaSeconds);
|
||||
|
||||
@@ -422,11 +415,13 @@ namespace anm2ed::imgui
|
||||
|
||||
void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources)
|
||||
{
|
||||
isFocused = false;
|
||||
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& playback = document.playback;
|
||||
auto& reference = document.reference;
|
||||
auto animation = document.animation_get();
|
||||
auto animation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex);
|
||||
auto& pan = document.previewPan;
|
||||
auto& zoom = document.previewZoom;
|
||||
auto& backgroundColor = settings.previewBackgroundColor;
|
||||
@@ -502,6 +497,7 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_ANIMATION_PREVIEW_WINDOW), &settings.windowIsAnimationPreview))
|
||||
{
|
||||
isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
||||
manager.isAbleToRecord = true;
|
||||
|
||||
auto childSize = ImVec2(row_widget_width_get(4),
|
||||
@@ -636,11 +632,12 @@ namespace anm2ed::imgui
|
||||
savedZoom = zoom;
|
||||
savedPan = pan;
|
||||
|
||||
if (auto rect = anm2.animation_rect(*document.animation_get(), isRootTransform); rect != vec4(-1.0f))
|
||||
{
|
||||
size_set(vec2(rect.z, rect.w) * settings.renderScale);
|
||||
set_to_rect(zoom, pan, rect);
|
||||
}
|
||||
if (animation)
|
||||
if (auto rect = anm2.animation_rect(*animation, isRootTransform); rect != vec4(-1.0f))
|
||||
{
|
||||
size_set(vec2(rect.z, rect.w) * settings.renderScale);
|
||||
set_to_rect(zoom, pan, rect);
|
||||
}
|
||||
|
||||
isSizeTrySet = false;
|
||||
}
|
||||
@@ -738,43 +735,56 @@ namespace anm2ed::imgui
|
||||
add_samples(settings.onionskinAfterCount, 1, settings.onionskinAfterColor);
|
||||
}
|
||||
|
||||
auto render = [&](anm2::Animation* animation, float time, vec3 colorOffset = {}, float alphaOffset = {},
|
||||
auto referenceItemType = static_cast<ItemType>(reference.itemType);
|
||||
|
||||
auto render = [&](Element* animation, float time, vec3 colorOffset = {}, float alphaOffset = {},
|
||||
const std::vector<OnionskinSample>* layeredOnions = nullptr, bool isIndexMode = false)
|
||||
{
|
||||
auto sample_time_for_item = [&](anm2::Item& item, const OnionskinSample& sample) -> std::optional<float>
|
||||
auto sample_time_for_item = [&](Element& item, const OnionskinSample& sample) -> std::optional<float>
|
||||
{
|
||||
if (!isIndexMode)
|
||||
{
|
||||
if (sample.time < 0.0f || sample.time > animation->frameNum) return std::nullopt;
|
||||
return sample.time;
|
||||
}
|
||||
if (item.frames.empty()) return std::nullopt;
|
||||
int baseIndex = item.frame_index_from_time_get(frameTime);
|
||||
if (item.children.empty()) return std::nullopt;
|
||||
int baseIndex = frame_index_from_time_get(item, frameTime);
|
||||
if (baseIndex < 0) return std::nullopt;
|
||||
int sampleIndex = baseIndex + sample.indexOffset;
|
||||
if (sampleIndex < 0 || sampleIndex >= (int)item.frames.size()) return std::nullopt;
|
||||
return item.frame_time_from_index_get(sampleIndex);
|
||||
if (!track_frame_get(item, sampleIndex)) return std::nullopt;
|
||||
return frame_time_from_index_get(item, sampleIndex);
|
||||
};
|
||||
|
||||
auto transform_for_time = [&](anm2::Animation* anim, float t)
|
||||
auto root = animation_item_get(*animation, ItemType::ROOT);
|
||||
|
||||
auto transform_for_time = [&](float t)
|
||||
{
|
||||
auto sampleTransform = baseTransform;
|
||||
if (isRootTransform)
|
||||
if (isRootTransform && root)
|
||||
{
|
||||
auto rootFrame = anim->rootAnimation.frame_generate(t, anm2::ROOT);
|
||||
auto rootFrame = frame_generate(*root, t);
|
||||
sampleTransform *= math::quad_model_parent_get(rootFrame.position, {},
|
||||
math::percent_to_unit(rootFrame.scale), rootFrame.rotation);
|
||||
}
|
||||
return sampleTransform;
|
||||
};
|
||||
|
||||
auto transform = transform_for_time(animation, time);
|
||||
auto transform = transform_for_time(time);
|
||||
|
||||
auto is_track_group_visible = [](const Element& container, const Element& track)
|
||||
{
|
||||
if (track.groupId == -1) return true;
|
||||
for (const auto& child : container.children)
|
||||
if (child.type == ElementType::GROUP && child.id == track.groupId) return child.isVisible;
|
||||
return true;
|
||||
};
|
||||
|
||||
auto draw_root =
|
||||
[&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion)
|
||||
{
|
||||
auto rootFrame = animation->rootAnimation.frame_generate(sampleTime, anm2::ROOT);
|
||||
if (isOnlyShowLayers || !rootFrame.isVisible || !animation->rootAnimation.isVisible) return;
|
||||
if (!root) return;
|
||||
auto rootFrame = frame_generate(*root, sampleTime);
|
||||
if (isOnlyShowLayers || !rootFrame.isVisible || !root->isVisible) return;
|
||||
|
||||
auto rootModel = isRootTransform
|
||||
? math::quad_model_get(TARGET_SIZE, {}, TARGET_SIZE * 0.5f)
|
||||
@@ -788,34 +798,45 @@ namespace anm2ed::imgui
|
||||
texture_render(shaderTexture, resources.icons[icon].id, rootTransform, color);
|
||||
};
|
||||
|
||||
if (layeredOnions)
|
||||
if (layeredOnions && root)
|
||||
for (auto& sample : *layeredOnions)
|
||||
if (auto sampleTime = sample_time_for_item(animation->rootAnimation, sample))
|
||||
if (auto sampleTime = sample_time_for_item(*root, sample))
|
||||
{
|
||||
auto sampleTransform = transform_for_time(animation, *sampleTime);
|
||||
auto sampleTransform = transform_for_time(*sampleTime);
|
||||
draw_root(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||
}
|
||||
|
||||
draw_root(time, transform, {}, 0.0f, false);
|
||||
|
||||
for (auto& id : animation->layerOrder)
|
||||
if (auto layerAnimations = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS))
|
||||
{
|
||||
if (!animation->layerAnimations.contains(id)) continue;
|
||||
auto& layerAnimation = animation->layerAnimations[id];
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
if (!anm2.content.layers.contains(id)) continue;
|
||||
auto& layer = anm2.content.layers.at(id);
|
||||
|
||||
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
|
||||
if (!spritesheet || !spritesheet->is_valid()) continue;
|
||||
|
||||
auto draw_layer =
|
||||
[&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion)
|
||||
auto layer_animation_draw = [&](auto&& self, Element& layerAnimation, bool isParentVisible = true) -> void
|
||||
{
|
||||
if (auto frame = layerAnimation.frame_generate(sampleTime, anm2::LAYER); frame.isVisible)
|
||||
if (layerAnimation.type == ElementType::GROUP)
|
||||
{
|
||||
auto& texture = spritesheet->texture;
|
||||
for (auto& child : layerAnimation.children)
|
||||
self(self, child, isParentVisible && layerAnimation.isVisible);
|
||||
return;
|
||||
}
|
||||
if (layerAnimation.type != ElementType::LAYER_ANIMATION || !isParentVisible ||
|
||||
!layerAnimation.isVisible || !is_track_group_visible(*layerAnimations, layerAnimation))
|
||||
return;
|
||||
|
||||
auto id = layerAnimation.layerId;
|
||||
auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, id);
|
||||
if (!layer) return;
|
||||
|
||||
auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, layer->spritesheetId);
|
||||
auto textureInfo = document.texture_get(layer->spritesheetId);
|
||||
if (!spritesheet || !textureInfo || !textureInfo->is_valid()) return;
|
||||
|
||||
auto draw_layer = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor,
|
||||
float sampleAlpha, bool isOnion)
|
||||
{
|
||||
auto frame = frame_generate(layerAnimation, sampleTime);
|
||||
if (!frame.isVisible) return;
|
||||
|
||||
auto& texture = *textureInfo;
|
||||
|
||||
auto texSize = vec2(texture.size);
|
||||
if (texSize.x <= 0.0f || texSize.y <= 0.0f) return;
|
||||
@@ -835,9 +856,9 @@ namespace anm2ed::imgui
|
||||
vec3 frameColorOffset = frame.colorOffset + colorOffset + sampleColor;
|
||||
vec4 frameTint = frame.tint;
|
||||
|
||||
if (isRootTransform)
|
||||
if (isRootTransform && root)
|
||||
{
|
||||
auto rootFrame = animation->rootAnimation.frame_generate(sampleTime, anm2::ROOT);
|
||||
auto rootFrame = frame_generate(*root, sampleTime);
|
||||
frameColorOffset += rootFrame.colorOffset;
|
||||
frameTint *= rootFrame.tint;
|
||||
}
|
||||
@@ -860,39 +881,54 @@ namespace anm2ed::imgui
|
||||
|
||||
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (layeredOnions)
|
||||
for (auto& sample : *layeredOnions)
|
||||
if (auto sampleTime = sample_time_for_item(layerAnimation, sample))
|
||||
{
|
||||
auto sampleTransform = transform_for_time(*sampleTime);
|
||||
draw_layer(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||
}
|
||||
|
||||
draw_layer(time, transform, {}, 0.0f, false);
|
||||
};
|
||||
|
||||
if (layeredOnions)
|
||||
for (auto& sample : *layeredOnions)
|
||||
if (auto sampleTime = sample_time_for_item(layerAnimation, sample))
|
||||
{
|
||||
auto sampleTransform = transform_for_time(animation, *sampleTime);
|
||||
draw_layer(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||
}
|
||||
|
||||
draw_layer(time, transform, {}, 0.0f, false);
|
||||
for (auto& layerAnimation : layerAnimations->children)
|
||||
layer_animation_draw(layer_animation_draw, layerAnimation);
|
||||
}
|
||||
|
||||
for (auto& [id, nullAnimation] : animation->nullAnimations)
|
||||
if (auto nullAnimations = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS))
|
||||
{
|
||||
if (!nullAnimation.isVisible || isOnlyShowLayers) continue;
|
||||
|
||||
auto nullInfo = anm2.content.nulls.find(id);
|
||||
if (nullInfo == anm2.content.nulls.end()) continue;
|
||||
auto& isShowRect = nullInfo->second.isShowRect;
|
||||
|
||||
auto draw_null =
|
||||
[&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion)
|
||||
auto null_animation_draw = [&](auto&& self, Element& nullAnimation, bool isParentVisible = true) -> void
|
||||
{
|
||||
if (auto frame = nullAnimation.frame_generate(sampleTime, anm2::NULL_); frame.isVisible)
|
||||
if (nullAnimation.type == ElementType::GROUP)
|
||||
{
|
||||
for (auto& child : nullAnimation.children)
|
||||
self(self, child, isParentVisible && nullAnimation.isVisible);
|
||||
return;
|
||||
}
|
||||
if (nullAnimation.type != ElementType::NULL_ANIMATION || !isParentVisible || !nullAnimation.isVisible ||
|
||||
!is_track_group_visible(*nullAnimations, nullAnimation) || isOnlyShowLayers)
|
||||
return;
|
||||
|
||||
auto id = nullAnimation.nullId;
|
||||
auto nulls = anm2.element_get(ElementType::NULLS);
|
||||
auto nullInfo = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr;
|
||||
if (!nullInfo) return;
|
||||
auto isShowRect = nullInfo->isShowRect;
|
||||
|
||||
auto draw_null = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor,
|
||||
float sampleAlpha, bool isOnion)
|
||||
{
|
||||
auto frame = frame_generate(nullAnimation, sampleTime);
|
||||
if (!frame.isVisible) return;
|
||||
|
||||
auto icon = isShowRect ? icon::POINT : isAltIcons ? icon::TARGET_ALT : icon::TARGET;
|
||||
|
||||
auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE;
|
||||
auto color = isOnion ? vec4(sampleColor, 1.0f - sampleAlpha)
|
||||
: id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED
|
||||
: NULL_COLOR;
|
||||
: id == reference.itemID && referenceItemType == ItemType::NULL_ ? color::RED
|
||||
: NULL_COLOR;
|
||||
|
||||
auto nullModel = math::quad_model_get(size, frame.position, size * 0.5f,
|
||||
math::percent_to_unit(frame.scale), frame.rotation);
|
||||
@@ -908,18 +944,20 @@ namespace anm2ed::imgui
|
||||
|
||||
rect_render(shaderLine, rectTransform, rectModel, color);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (layeredOnions)
|
||||
for (auto& sample : *layeredOnions)
|
||||
if (auto sampleTime = sample_time_for_item(nullAnimation, sample))
|
||||
{
|
||||
auto sampleTransform = transform_for_time(*sampleTime);
|
||||
draw_null(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||
}
|
||||
|
||||
draw_null(time, transform, {}, 0.0f, false);
|
||||
};
|
||||
|
||||
if (layeredOnions)
|
||||
for (auto& sample : *layeredOnions)
|
||||
if (auto sampleTime = sample_time_for_item(nullAnimation, sample))
|
||||
{
|
||||
auto sampleTransform = transform_for_time(animation, *sampleTime);
|
||||
draw_null(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true);
|
||||
}
|
||||
|
||||
draw_null(time, transform, {}, 0.0f, false);
|
||||
for (auto& nullAnimation : nullAnimations->children)
|
||||
null_animation_draw(null_animation_draw, nullAnimation);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -929,7 +967,7 @@ namespace anm2ed::imgui
|
||||
|
||||
render(animation, frameTime, {}, 0.0f, layeredOnions, settings.onionskinMode == (int)OnionskinMode::INDEX);
|
||||
|
||||
if (auto overlayAnimation = anm2.animation_get(overlayIndex))
|
||||
if (auto overlayAnimation = anm2.element_get(ElementType::ANIMATION, overlayIndex))
|
||||
render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency), layeredOnions,
|
||||
settings.onionskinMode == (int)OnionskinMode::INDEX);
|
||||
}
|
||||
@@ -945,10 +983,10 @@ namespace anm2ed::imgui
|
||||
|
||||
isPreviewHovered = ImGui::IsItemHovered();
|
||||
|
||||
if (animation && animation->triggers.isVisible && !isOnlyShowLayers && !manager.isRecording)
|
||||
auto triggers = animation ? animation_item_get(*animation, ItemType::TRIGGER) : nullptr;
|
||||
if (animation && triggers && triggers->isVisible && !isOnlyShowLayers && !manager.isRecording)
|
||||
{
|
||||
if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER);
|
||||
trigger.isVisible && trigger.eventID > -1)
|
||||
if (auto trigger = frame_generate(*triggers, frameTime); trigger.isVisible && trigger.eventId > -1)
|
||||
{
|
||||
auto clipMin = ImGui::GetItemRectMin();
|
||||
auto clipMax = ImGui::GetItemRectMax();
|
||||
@@ -958,9 +996,9 @@ namespace anm2ed::imgui
|
||||
drawList->PushClipRect(clipMin, clipMax);
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE);
|
||||
auto triggerTextColor = isLightTheme ? TRIGGER_TEXT_COLOR_LIGHT : TRIGGER_TEXT_COLOR_DARK;
|
||||
if (anm2.content.events.contains(trigger.eventID))
|
||||
drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor),
|
||||
anm2.content.events.at(trigger.eventID).name.c_str());
|
||||
auto events = anm2.element_get(ElementType::EVENTS);
|
||||
auto event = events ? element_child_id_get(*events, ElementType::EVENT_ELEMENT, trigger.eventId) : nullptr;
|
||||
if (event) drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor), event->name.c_str());
|
||||
ImGui::PopFont();
|
||||
drawList->PopClipRect();
|
||||
}
|
||||
@@ -999,20 +1037,23 @@ namespace anm2ed::imgui
|
||||
auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown;
|
||||
auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased;
|
||||
|
||||
auto isZoomIn = shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL);
|
||||
auto isZoomOut = shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL);
|
||||
auto isZoomIn = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL);
|
||||
auto isZoomOut = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL);
|
||||
|
||||
auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift);
|
||||
|
||||
auto frame = document.frame_get();
|
||||
auto item = document.item_get();
|
||||
auto frame =
|
||||
anm2.element_get(reference.animationIndex, referenceItemType, reference.frameIndex, reference.itemID);
|
||||
auto item = anm2.element_get(reference.animationIndex, referenceItemType, reference.itemID);
|
||||
auto useTool = tool;
|
||||
auto step = isMod ? STEP_FAST : STEP;
|
||||
mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos));
|
||||
auto selectedNullIt = reference.itemType == anm2::NULL_ ? anm2.content.nulls.find(reference.itemID)
|
||||
: anm2.content.nulls.end();
|
||||
bool isSelectedNullRect = selectedNullIt != anm2.content.nulls.end() && selectedNullIt->second.isShowRect;
|
||||
auto null_rect_top_left = [](const anm2::Frame& frame) { return frame.position - (frame.scale * 0.5f); };
|
||||
auto nulls = anm2.element_get(ElementType::NULLS);
|
||||
auto selectedNull = referenceItemType == ItemType::NULL_ && nulls
|
||||
? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, reference.itemID)
|
||||
: nullptr;
|
||||
bool isSelectedNullRect = selectedNull && selectedNull->isShowRect;
|
||||
auto null_rect_top_left = [](const Element& frame) { return frame.position - (frame.scale * 0.5f); };
|
||||
|
||||
if (isMouseMiddleDown) useTool = tool::PAN;
|
||||
if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE;
|
||||
@@ -1033,8 +1074,31 @@ namespace anm2ed::imgui
|
||||
auto isToolDuring = isToolMouseDown || isKeyDown;
|
||||
auto isToolEnd = isToolMouseReleased || isKeyReleased;
|
||||
|
||||
auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST)
|
||||
{ item->frames_change(frameChange, reference.itemType, changeType, frames); };
|
||||
auto frame_snapshot = [&](auto message)
|
||||
{
|
||||
manager.command_push({manager.selected,
|
||||
[message](Manager&, Document& document) { document.snapshot(localize.get(message)); }});
|
||||
};
|
||||
auto frame_change_apply = [&](FrameChange frameChange, ChangeType changeType = ChangeType::ADJUST)
|
||||
{
|
||||
auto queuedReference = reference;
|
||||
auto queuedReferenceItemType = referenceItemType;
|
||||
auto queuedFrames = frames;
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto item = document.anm2.element_get(queuedReference.animationIndex,
|
||||
queuedReferenceItemType,
|
||||
queuedReference.itemID);
|
||||
if (!item) return;
|
||||
frames_change(*item, frameChange, queuedReferenceItemType, changeType, queuedFrames);
|
||||
}});
|
||||
};
|
||||
auto frames_changed = [&]()
|
||||
{
|
||||
manager.command_push(
|
||||
{manager.selected, [](Manager&, Document& document) { document.anm2_change(Document::FRAMES); }});
|
||||
};
|
||||
auto null_rect_change = [&](vec2 topLeft, vec2 rectSize)
|
||||
{
|
||||
topLeft = vec2(ivec2(topLeft));
|
||||
@@ -1064,7 +1128,7 @@ namespace anm2ed::imgui
|
||||
if (!item || !frame || frames.empty()) break;
|
||||
if (isToolBegin)
|
||||
{
|
||||
document.snapshot(localize.get(EDIT_FRAME_POSITION));
|
||||
frame_snapshot(EDIT_FRAME_POSITION);
|
||||
if (isToolMouseClicked)
|
||||
{
|
||||
auto origin = isSelectedNullRect ? null_rect_top_left(*frame) : frame->position;
|
||||
@@ -1082,13 +1146,13 @@ namespace anm2ed::imgui
|
||||
frame_change_apply({.positionX = (int)position.x, .positionY = (int)position.y});
|
||||
}
|
||||
|
||||
if (isLeftPressed) frame_change_apply({.positionX = step}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.positionX = step}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.positionY = step}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.positionY = step}, anm2::ADD);
|
||||
if (isLeftPressed) frame_change_apply({.positionX = step}, ChangeType::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.positionX = step}, ChangeType::ADD);
|
||||
if (isUpPressed) frame_change_apply({.positionY = step}, ChangeType::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.positionY = step}, ChangeType::ADD);
|
||||
|
||||
if (isToolMouseReleased) isMoveDragging = false;
|
||||
if (isToolEnd) document.change(Document::FRAMES);
|
||||
if (isToolEnd) frames_changed();
|
||||
if (isToolDuring)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
@@ -1104,7 +1168,7 @@ namespace anm2ed::imgui
|
||||
if (!item || !frame || frames.empty()) break;
|
||||
if (isToolBegin)
|
||||
{
|
||||
document.snapshot(localize.get(EDIT_FRAME_SCALE));
|
||||
frame_snapshot(EDIT_FRAME_SCALE);
|
||||
if (isToolMouseClicked && isSelectedNullRect) nullRectScaleAnchor = null_rect_top_left(*frame);
|
||||
}
|
||||
if (isToolMouseDown)
|
||||
@@ -1134,21 +1198,17 @@ namespace anm2ed::imgui
|
||||
|
||||
if (isSelectedNullRect)
|
||||
{
|
||||
if (isLeftPressed)
|
||||
frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, anm2::SUBTRACT);
|
||||
if (isRightPressed)
|
||||
frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, anm2::ADD);
|
||||
if (isUpPressed)
|
||||
frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, anm2::SUBTRACT);
|
||||
if (isDownPressed)
|
||||
frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, anm2::ADD);
|
||||
if (isLeftPressed) frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, ChangeType::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, ChangeType::ADD);
|
||||
if (isUpPressed) frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, ChangeType::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, ChangeType::ADD);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isLeftPressed) frame_change_apply({.scaleX = step}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.scaleX = step}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.scaleY = step}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.scaleY = step}, anm2::ADD);
|
||||
if (isLeftPressed) frame_change_apply({.scaleX = step}, ChangeType::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.scaleX = step}, ChangeType::ADD);
|
||||
if (isUpPressed) frame_change_apply({.scaleY = step}, ChangeType::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.scaleY = step}, ChangeType::ADD);
|
||||
}
|
||||
|
||||
if (isToolDuring)
|
||||
@@ -1173,15 +1233,15 @@ namespace anm2ed::imgui
|
||||
maxPoint = vec2(ivec2(maxPoint));
|
||||
null_rect_change(minPoint, maxPoint - minPoint);
|
||||
}
|
||||
document.change(Document::FRAMES);
|
||||
frames_changed();
|
||||
}
|
||||
break;
|
||||
case tool::ROTATE:
|
||||
if (!item || !frame || frames.empty()) break;
|
||||
if (isToolBegin) document.snapshot(localize.get(EDIT_FRAME_ROTATION));
|
||||
if (isToolMouseDown) frame_change_apply({.rotation = (int)mouseDelta.x}, anm2::ADD);
|
||||
if (isLeftPressed || isDownPressed) frame_change_apply({.rotation = step}, anm2::SUBTRACT);
|
||||
if (isUpPressed || isRightPressed) frame_change_apply({.rotation = step}, anm2::ADD);
|
||||
if (isToolBegin) frame_snapshot(EDIT_FRAME_ROTATION);
|
||||
if (isToolMouseDown) frame_change_apply({.rotation = (int)mouseDelta.x}, ChangeType::ADD);
|
||||
if (isLeftPressed || isDownPressed) frame_change_apply({.rotation = step}, ChangeType::SUBTRACT);
|
||||
if (isUpPressed || isRightPressed) frame_change_apply({.rotation = step}, ChangeType::ADD);
|
||||
|
||||
if (isToolDuring)
|
||||
{
|
||||
@@ -1193,7 +1253,7 @@ namespace anm2ed::imgui
|
||||
}
|
||||
}
|
||||
|
||||
if (isToolEnd) document.change(Document::FRAMES);
|
||||
if (isToolEnd) frames_changed();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -1228,27 +1288,17 @@ namespace anm2ed::imgui
|
||||
}
|
||||
}
|
||||
|
||||
if (tool == tool::PAN && ImGui::BeginPopupContextWindow("##Animation Preview Context Menu", ImGuiMouseButton_Right))
|
||||
if (tool == tool::PAN)
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_CENTER_VIEW), settings.shortcutCenterView.c_str())) center_view();
|
||||
if (ImGui::MenuItem(localize.get(LABEL_FIT), settings.shortcutFit.c_str(), false, animation)) fit_view();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_IN), settings.shortcutZoomIn.c_str())) zoom_in();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_OUT), settings.shortcutZoomOut.c_str())) zoom_out();
|
||||
|
||||
ImGui::EndPopup();
|
||||
Actions actions{};
|
||||
actions_undo_redo_add(actions, manager, document);
|
||||
actions.separator();
|
||||
actions.add(ACTION_CENTER_VIEW, []() { return true; }, center_view);
|
||||
actions.add(ACTION_FIT_VIEW, [&]() { return animation; }, fit_view);
|
||||
actions.separator();
|
||||
actions.add(ACTION_ZOOM_IN, []() { return true; }, zoom_in);
|
||||
actions.add(ACTION_ZOOM_OUT, []() { return true; }, zoom_out);
|
||||
actions_context_window_draw("##Animation Preview Context Menu", actions, settings);
|
||||
}
|
||||
|
||||
manager.progressPopup.trigger();
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace anm2ed::imgui
|
||||
MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
|
||||
AudioStream audioStream = AudioStream(mixer);
|
||||
bool wasPlaybackPlaying{};
|
||||
bool isFocused{};
|
||||
bool isPreviewHovered{};
|
||||
bool isSizeTrySet{true};
|
||||
Settings savedSettings{};
|
||||
@@ -38,6 +39,7 @@ namespace anm2ed::imgui
|
||||
|
||||
public:
|
||||
AnimationPreview();
|
||||
bool is_focused_get() const;
|
||||
void tick(Manager&, Settings&, float);
|
||||
void update(Manager&, Settings&, Resources&);
|
||||
};
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
#include "animations.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "vector_.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
void Animations::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.reference;
|
||||
auto& selection = document.animation.selection;
|
||||
auto& mergeSelection = document.merge.selection;
|
||||
auto& mergeReference = document.merge.reference;
|
||||
auto& overlayIndex = document.overlayIndex;
|
||||
|
||||
auto rename_format_get = [&](int index)
|
||||
{ return std::format("###Document #{} Animation #{}", manager.selected, index); };
|
||||
|
||||
auto rename = [&]()
|
||||
{
|
||||
if (!selection.empty()) renameQueued = *selection.begin();
|
||||
};
|
||||
|
||||
auto add = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
anm2::Animation animation;
|
||||
animation.name = localize.get(TEXT_NEW_ANIMATION);
|
||||
if (anm2::Animation* referenceAnimation = document.animation_get())
|
||||
{
|
||||
for (auto [id, layerAnimation] : referenceAnimation->layerAnimations)
|
||||
animation.layerAnimations[id] = anm2::Item();
|
||||
animation.layerOrder = referenceAnimation->layerOrder;
|
||||
for (auto [id, nullAnimation] : referenceAnimation->nullAnimations)
|
||||
animation.nullAnimations[id] = anm2::Item();
|
||||
}
|
||||
animation.rootAnimation.frames.emplace_back(anm2::Frame());
|
||||
|
||||
auto index = (int)anm2.animations.items.size();
|
||||
if (!selection.empty())
|
||||
{
|
||||
index = *selection.rbegin() + 1;
|
||||
index = std::min(index, (int)anm2.animations.items.size());
|
||||
}
|
||||
|
||||
if (anm2.animations.items.empty()) anm2.animations.defaultAnimation = animation.name;
|
||||
|
||||
anm2.animations.items.insert(anm2.animations.items.begin() + index, animation);
|
||||
selection = {index};
|
||||
reference = {index};
|
||||
newAnimationSelectedIndex = index;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_ANIMATION), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto remove = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
if (!selection.empty())
|
||||
{
|
||||
for (auto it = selection.rbegin(); it != selection.rend(); ++it)
|
||||
{
|
||||
auto i = *it;
|
||||
if (overlayIndex == i) overlayIndex = -1;
|
||||
if (reference.animationIndex == i) reference.animationIndex = -1;
|
||||
anm2.animations.items.erase(anm2.animations.items.begin() + i);
|
||||
}
|
||||
selection.clear();
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto duplicate = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto duplicated = selection;
|
||||
auto end = std::ranges::max(duplicated);
|
||||
for (auto& id : duplicated)
|
||||
{
|
||||
anm2.animations.items.insert(anm2.animations.items.begin() + end, anm2.animations.items[id]);
|
||||
selection.insert(++end);
|
||||
selection.erase(id);
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto merge = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
if (mergeSelection.contains(overlayIndex)) overlayIndex = -1;
|
||||
auto merged = anm2.animations_merge(mergeReference, mergeSelection, (merge::Type)settings.mergeType,
|
||||
settings.mergeIsDeleteAnimationsAfter);
|
||||
|
||||
selection = {merged};
|
||||
reference = {merged};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto merge_popup_open = [&]()
|
||||
{
|
||||
mergePopup.open();
|
||||
mergeSelection.clear();
|
||||
mergeReference = *selection.begin();
|
||||
};
|
||||
|
||||
auto merge_quick = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
int merged{};
|
||||
if (selection.contains(overlayIndex)) overlayIndex = -1;
|
||||
|
||||
if (selection.size() > 1)
|
||||
merged = anm2.animations_merge(*selection.begin(), selection);
|
||||
else if (selection.size() == 1 && *selection.begin() != (int)anm2.animations.items.size() - 1)
|
||||
{
|
||||
auto start = *selection.begin();
|
||||
auto next = *selection.begin() + 1;
|
||||
std::set<int> animationSet{};
|
||||
animationSet.insert(start);
|
||||
animationSet.insert(next);
|
||||
|
||||
merged = anm2.animations_merge(start, animationSet);
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
selection = {merged};
|
||||
reference = {merged};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
auto default_set = [&]()
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_DEFAULT_ANIMATION), Document::ANIMATIONS,
|
||||
anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name);
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& i : selection)
|
||||
clipboardText += anm2.animations.items[i].to_string();
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto cut = [&]()
|
||||
{
|
||||
copy();
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_CUT_ANIMATIONS), Document::ANIMATIONS, remove());
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto clipboardText = clipboard.get();
|
||||
auto start = selection.empty() ? anm2.animations.items.size() : *selection.rbegin() + 1;
|
||||
std::set<int> indices{};
|
||||
std::string errorString{};
|
||||
if (anm2.animations_deserialize(clipboardText, start, indices, &errorString))
|
||||
{
|
||||
if (!indices.empty())
|
||||
{
|
||||
auto index = *indices.rbegin();
|
||||
selection = {index};
|
||||
reference = {index};
|
||||
newAnimationSelectedIndex = index;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_ANIMATIONS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_ANIMATIONS), Document::ANIMATIONS, behavior());
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_ANIMATIONS_WINDOW), &settings.windowIsAnimations))
|
||||
{
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::IsKeyPressed(ImGuiKey_Escape))
|
||||
reference = {};
|
||||
|
||||
auto childSize = size_without_footer_get();
|
||||
|
||||
if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
|
||||
selection.start(anm2.animations.items.size());
|
||||
|
||||
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
|
||||
{
|
||||
ImGui::PushID((int)i);
|
||||
|
||||
auto isDefault = anm2.animations.defaultAnimation == animation.name;
|
||||
auto isReferenced = reference.animationIndex == (int)i;
|
||||
auto isNewAnimation = newAnimationSelectedIndex == (int)i;
|
||||
|
||||
auto font = isDefault && isReferenced ? font::BOLD_ITALICS
|
||||
: isDefault ? font::BOLD
|
||||
: isReferenced ? font::ITALICS
|
||||
: font::REGULAR;
|
||||
|
||||
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
|
||||
ImGui::SetNextItemSelectionUserData((int)i);
|
||||
|
||||
if (isNewAnimation || renameQueued == i)
|
||||
{
|
||||
renameState = RENAME_FORCE_EDIT;
|
||||
renameQueued = -1;
|
||||
}
|
||||
if (selectable_input_text(animation.name, rename_format_get(i), animation.name, selection.contains((int)i),
|
||||
ImGuiSelectableFlags_None, renameState))
|
||||
{
|
||||
reference = {(int)i};
|
||||
document.frames.clear();
|
||||
|
||||
if (renameState == RENAME_BEGIN)
|
||||
document.snapshot(localize.get(SNAPSHOT_RENAME_ANIMATION));
|
||||
else if (renameState == RENAME_FINISHED)
|
||||
{
|
||||
if (anm2.animations.items.size() == 1) anm2.animations.defaultAnimation = animation.name;
|
||||
document.change(Document::ANIMATIONS);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewAnimation)
|
||||
{
|
||||
isUpdateScroll = true;
|
||||
newAnimationSelectedIndex = -1;
|
||||
}
|
||||
ImGui::PopFont();
|
||||
|
||||
if (isUpdateScroll && isReferenced)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
isUpdateScroll = false;
|
||||
}
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(animation.name.c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
if (isDefault)
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(localize.get(BASIC_DEFAULT));
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_LENGTH), std::make_format_args(animation.frameNum)).c_str());
|
||||
auto loopLabel = animation.isLoop ? localize.get(BASIC_YES) : localize.get(BASIC_NO);
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_LOOP), std::make_format_args(loopLabel)).c_str());
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropSource())
|
||||
{
|
||||
static std::vector<int> dragDropSelection{};
|
||||
dragDropSelection.assign(selection.begin(), selection.end());
|
||||
ImGui::SetDragDropPayload("Animation Drag Drop", dragDropSelection.data(),
|
||||
dragDropSelection.size() * sizeof(int));
|
||||
for (auto& i : dragDropSelection)
|
||||
ImGui::Text("%s", anm2.animations.items[(int)i].name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (auto payload = ImGui::AcceptDragDropPayload("Animation Drag Drop"))
|
||||
{
|
||||
auto payloadIndices = (int*)(payload->Data);
|
||||
auto payloadCount = payload->DataSize / sizeof(int);
|
||||
std::vector<int> indices(payloadIndices, payloadIndices + payloadCount);
|
||||
std::sort(indices.begin(), indices.end());
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_ANIMATIONS), Document::ANIMATIONS,
|
||||
selection = vector::move_indices(anm2.animations.items, indices, i));
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
selection.finish();
|
||||
|
||||
if (shortcut(manager.chords[SHORTCUT_RENAME], shortcut::FOCUSED)) rename();
|
||||
if (shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED)) merge_quick();
|
||||
if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
|
||||
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RENAME), settings.shortcutRename.c_str(), false,
|
||||
selection.size() == 1))
|
||||
rename();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_DUPLICATE), settings.shortcutDuplicate.c_str())) duplicate();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_MERGE), settings.shortcutMerge.c_str())) merge_quick();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REMOVE), settings.shortcutRemove.c_str())) remove();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_DEFAULT), settings.shortcutDefault.c_str(), false,
|
||||
selection.size() == 1))
|
||||
default_set();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_CUT), settings.shortcutCut.c_str(), false, !selection.empty())) cut();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
copy();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(5);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_ANIMATION), settings.shortcutAdd);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
shortcut(manager.chords[SHORTCUT_DUPLICATE]);
|
||||
if (ImGui::Button(localize.get(BASIC_DUPLICATE), widgetSize)) duplicate();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_DUPLICATE_ANIMATION), settings.shortcutDuplicate);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize)) merge_popup_open();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_OPEN_MERGE_POPUP), settings.shortcutMerge);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize)) remove();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_ANIMATION), settings.shortcutRemove);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
shortcut(manager.chords[SHORTCUT_DEFAULT]);
|
||||
if (ImGui::Button(localize.get(BASIC_DEFAULT), widgetSize)) default_set();
|
||||
ImGui::EndDisabled();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_SET_DEFAULT_ANIMATION), settings.shortcutDefault);
|
||||
|
||||
mergePopup.trigger();
|
||||
|
||||
if (ImGui::BeginPopupModal(mergePopup.label(), &mergePopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
auto close = [&]()
|
||||
{
|
||||
mergeSelection.clear();
|
||||
mergePopup.close();
|
||||
};
|
||||
|
||||
auto& type = settings.mergeType;
|
||||
auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter;
|
||||
|
||||
auto footerSize = footer_size_get();
|
||||
auto optionsSize = child_size_get(2);
|
||||
auto deleteAfterSize = child_size_get();
|
||||
auto animationsSize =
|
||||
ImVec2(0, ImGui::GetContentRegionAvail().y -
|
||||
(optionsSize.y + deleteAfterSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 3));
|
||||
|
||||
if (ImGui::BeginChild(localize.get(LABEL_ANIMATIONS_CHILD), animationsSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
mergeSelection.start(anm2.animations.items.size());
|
||||
|
||||
for (int i = 0; i < (int)anm2.animations.items.size(); i++)
|
||||
{
|
||||
if (i == mergeReference) continue;
|
||||
|
||||
auto& animation = anm2.animations.items[i];
|
||||
|
||||
ImGui::PushID(i);
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(i);
|
||||
ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i));
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
mergeSelection.finish();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (ImGui::BeginChild("##Merge Options", optionsSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto size = ImVec2(optionsSize.x * 0.5f, optionsSize.y - ImGui::GetStyle().WindowPadding.y * 2);
|
||||
|
||||
if (ImGui::BeginChild("##Merge Options 1", size))
|
||||
{
|
||||
ImGui::RadioButton(localize.get(LABEL_APPEND_FRAMES), &type, merge::APPEND);
|
||||
ImGui::RadioButton(localize.get(LABEL_PREPEND_FRAMES), &type, merge::PREPEND);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::BeginChild("##Merge Options 2", size))
|
||||
{
|
||||
ImGui::RadioButton(localize.get(LABEL_REPLACE_FRAMES), &type, merge::REPLACE);
|
||||
ImGui::RadioButton(localize.get(LABEL_IGNORE_FRAMES), &type, merge::IGNORE);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (ImGui::BeginChild("##Merge Delete After", deleteAfterSize, ImGuiChildFlags_Borders))
|
||||
ImGui::Checkbox(localize.get(LABEL_DELETE_ANIMATIONS_AFTER), &isDeleteAnimationsAfter);
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
ImGui::BeginDisabled(mergeSelection.empty());
|
||||
if (ImGui::Button(localize.get(LABEL_MERGE), widgetSize))
|
||||
{
|
||||
merge();
|
||||
close();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
auto isNextAnimation = shortcut(manager.chords[SHORTCUT_NEXT_ANIMATION], shortcut::GLOBAL);
|
||||
auto isPreviousAnimation = shortcut(manager.chords[SHORTCUT_PREVIOUS_ANIMATION], shortcut::GLOBAL);
|
||||
|
||||
if ((isPreviousAnimation || isNextAnimation) && !anm2.animations.items.empty())
|
||||
{
|
||||
if (isPreviousAnimation)
|
||||
reference.animationIndex = glm::clamp(--reference.animationIndex, 0, (int)anm2.animations.items.size() - 1);
|
||||
if (isNextAnimation)
|
||||
reference.animationIndex = glm::clamp(++reference.animationIndex, 0, (int)anm2.animations.items.size() - 1);
|
||||
|
||||
selection = {reference.animationIndex};
|
||||
isUpdateScroll = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Animations
|
||||
{
|
||||
PopupHelper mergePopup{PopupHelper(LABEL_ANIMATIONS_MERGE_POPUP)};
|
||||
int newAnimationSelectedIndex{-1};
|
||||
int renameQueued{-1};
|
||||
bool isInContextMenu{};
|
||||
bool isUpdateScroll{};
|
||||
RenameState renameState{RENAME_SELECTABLE};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||
};
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
#include "events.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
void Events::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.event.reference;
|
||||
auto& selection = document.event.selection;
|
||||
|
||||
auto rename_format_get = [&](int id) { return std::format("###Document #{} Event #{}", manager.selected, id); };
|
||||
auto rename = [&]()
|
||||
{
|
||||
if (!selection.empty()) renameQueued = *selection.begin();
|
||||
};
|
||||
|
||||
auto add = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto id = map::next_id_get(anm2.content.events);
|
||||
anm2::Event event{};
|
||||
event.name = localize.get(TEXT_NEW_EVENT);
|
||||
anm2.content.events[id] = event;
|
||||
selection = {id};
|
||||
reference = {id};
|
||||
newEventId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_EVENT), Document::EVENTS, behavior());
|
||||
};
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.events_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
{
|
||||
for (auto& animation : anm2.animations.items)
|
||||
for (auto& trigger : animation.triggers.frames)
|
||||
if (trigger.eventID == id) trigger.eventID = -1;
|
||||
|
||||
anm2.content.events.erase(id);
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_EVENTS), Document::EVENTS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.events[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxEventIdBefore = anm2.content.events.empty() ? -1 : anm2.content.events.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_EVENTS));
|
||||
if (anm2.events_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.events.empty())
|
||||
{
|
||||
auto maxEventIdAfter = anm2.content.events.rbegin()->first;
|
||||
if (maxEventIdAfter > maxEventIdBefore)
|
||||
{
|
||||
newEventId = maxEventIdAfter;
|
||||
selection = {maxEventIdAfter};
|
||||
reference = maxEventIdAfter;
|
||||
}
|
||||
}
|
||||
document.change(Document::EVENTS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_EVENTS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_EVENTS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_EVENTS), Document::EVENTS, behavior());
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_EVENTS_WINDOW), &settings.windowIsEvents))
|
||||
{
|
||||
auto childSize = size_without_footer_get();
|
||||
|
||||
if (ImGui::BeginChild("##Events Child", childSize, true))
|
||||
{
|
||||
selection.start(anm2.content.events.size());
|
||||
|
||||
for (auto& [id, event] : anm2.content.events)
|
||||
{
|
||||
auto isNewEvent = (newEventId == id);
|
||||
|
||||
ImGui::PushID(id);
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
if (isNewEvent || renameQueued == id)
|
||||
{
|
||||
renameState = RENAME_FORCE_EDIT;
|
||||
renameQueued = -1;
|
||||
}
|
||||
if (selectable_input_text(event.name, rename_format_get(id), event.name, selection.contains(id),
|
||||
ImGuiSelectableFlags_None, renameState))
|
||||
{
|
||||
if (renameState == RENAME_BEGIN)
|
||||
document.snapshot(localize.get(EDIT_RENAME_EVENT));
|
||||
else if (renameState == RENAME_FINISHED)
|
||||
document.change(Document::EVENTS);
|
||||
}
|
||||
|
||||
if (isNewEvent)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newEventId = -1;
|
||||
}
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(event.name.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
selection.finish();
|
||||
|
||||
if (shortcut(manager.chords[SHORTCUT_RENAME], shortcut::FOCUSED)) rename();
|
||||
if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
|
||||
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RENAME), settings.shortcutRename.c_str(), false,
|
||||
selection.size() == 1))
|
||||
rename();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
copy();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_EVENT), settings.shortcutAdd);
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_EVENTS), settings.shortcutRemove);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Events
|
||||
{
|
||||
int newEventId{-1};
|
||||
int renameQueued{-1};
|
||||
RenameState renameState{RENAME_SELECTABLE};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "frame_properties.hpp"
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <limits>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "math_.hpp"
|
||||
#include "math.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "types.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
using namespace anm2ed::util::math;
|
||||
using namespace anm2ed::types;
|
||||
@@ -17,11 +18,70 @@ namespace anm2ed::imgui
|
||||
{
|
||||
void FrameProperties::update(Manager& manager, Settings& settings)
|
||||
{
|
||||
if (ImGui::Begin(localize.get(LABEL_FRAME_PROPERTIES_WINDOW), &settings.windowIsFrameProperties))
|
||||
auto& document = *manager.get();
|
||||
auto frameSelectionCount =
|
||||
document.frames.references.empty() ? document.frames.selection.size() : document.frames.references.size();
|
||||
auto isSingleFrameSelection = frameSelectionCount == 1;
|
||||
auto isMultiFrameSelection = frameSelectionCount > 1;
|
||||
auto isSingleFrameBatchMode =
|
||||
isSingleFrameSelection && document.reference.itemType != TRIGGER && isBatchMode;
|
||||
auto isBatchFrameProperties = isMultiFrameSelection || isSingleFrameBatchMode;
|
||||
auto windowLabel = std::string(localize.get(isBatchFrameProperties ? LABEL_CHANGE_ALL_FRAME_PROPERTIES
|
||||
: LABEL_FRAME_PROPERTIES_WINDOW));
|
||||
if (isBatchFrameProperties) windowLabel += "###Frame Properties";
|
||||
|
||||
if (ImGui::Begin(windowLabel.c_str(), &settings.windowIsFrameProperties))
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& frames = document.frames.selection;
|
||||
auto& type = document.reference.itemType;
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.reference;
|
||||
auto& type = reference.itemType;
|
||||
auto itemType = static_cast<ItemType>(type);
|
||||
auto frame = anm2.element_get(reference.animationIndex, itemType, reference.frameIndex, reference.itemID);
|
||||
|
||||
auto frame_edit = [&](auto message, auto behavior)
|
||||
{
|
||||
auto queuedReference = reference;
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document) mutable
|
||||
{
|
||||
auto itemType = static_cast<ItemType>(queuedReference.itemType);
|
||||
auto frame = document.anm2.element_get(queuedReference.animationIndex, itemType,
|
||||
queuedReference.frameIndex,
|
||||
queuedReference.itemID);
|
||||
auto item =
|
||||
document.anm2.element_get(queuedReference.animationIndex, itemType,
|
||||
queuedReference.itemID);
|
||||
if (!frame) return;
|
||||
|
||||
document.snapshot(localize.get(message));
|
||||
behavior(document, *frame, item, queuedReference);
|
||||
document.anm2_change(Document::FRAMES);
|
||||
}});
|
||||
};
|
||||
|
||||
auto persistent_edit = [&](auto state, auto message, auto behavior)
|
||||
{
|
||||
if (state == edit::NONE) return;
|
||||
|
||||
auto queuedReference = reference;
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document) mutable
|
||||
{
|
||||
auto itemType = static_cast<ItemType>(queuedReference.itemType);
|
||||
auto frame = document.anm2.element_get(queuedReference.animationIndex, itemType,
|
||||
queuedReference.frameIndex,
|
||||
queuedReference.itemID);
|
||||
auto item =
|
||||
document.anm2.element_get(queuedReference.animationIndex, itemType,
|
||||
queuedReference.itemID);
|
||||
if (!frame) return;
|
||||
|
||||
if (state == edit::START) document.snapshot(localize.get(message));
|
||||
behavior(document, *frame, item, queuedReference);
|
||||
if (state == edit::END) document.anm2_change(Document::FRAMES);
|
||||
}});
|
||||
};
|
||||
|
||||
auto regionLabelsString = std::vector<std::string>{localize.get(BASIC_NONE)};
|
||||
auto regionLabels = std::vector<const char*>{regionLabelsString[0].c_str()};
|
||||
auto regionIds = std::vector<int>{-1};
|
||||
@@ -33,17 +93,14 @@ namespace anm2ed::imgui
|
||||
interpolationLabelsString[2].c_str(), interpolationLabelsString[3].c_str(),
|
||||
interpolationLabelsString[4].c_str()};
|
||||
auto interpolationValues =
|
||||
std::vector<int>{anm2::Frame::Interpolation::NONE, anm2::Frame::Interpolation::LINEAR,
|
||||
anm2::Frame::Interpolation::EASE_IN, anm2::Frame::Interpolation::EASE_OUT,
|
||||
anm2::Frame::Interpolation::EASE_IN_OUT};
|
||||
std::vector<int>{(int)Interpolation::NONE, (int)Interpolation::LINEAR, (int)Interpolation::EASE_IN,
|
||||
(int)Interpolation::EASE_OUT, (int)Interpolation::EASE_IN_OUT};
|
||||
|
||||
if (type == anm2::LAYER && document.reference.itemID != -1)
|
||||
if (type == LAYER && reference.itemID != -1)
|
||||
{
|
||||
if (auto layerIt = document.anm2.content.layers.find(document.reference.itemID);
|
||||
layerIt != document.anm2.content.layers.end())
|
||||
if (auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID))
|
||||
{
|
||||
auto spritesheetID = layerIt->second.spritesheetID;
|
||||
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
|
||||
auto regionIt = document.regionBySpritesheet.find(layer->spritesheetId);
|
||||
if (regionIt != document.regionBySpritesheet.end() && !regionIt->second.ids.empty() &&
|
||||
!regionIt->second.labels.empty())
|
||||
{
|
||||
@@ -53,37 +110,70 @@ namespace anm2ed::imgui
|
||||
}
|
||||
}
|
||||
|
||||
if (frames.size() <= 1)
|
||||
auto mode_selector_draw = [&]()
|
||||
{
|
||||
auto frame = document.frame_get();
|
||||
auto useFrame = frame ? *frame : anm2::Frame();
|
||||
auto displayFrame = frame && type == anm2::LAYER && document.reference.itemID != -1
|
||||
? document.anm2.frame_effective(document.reference.itemID, *frame)
|
||||
if (!isSingleFrameSelection || type == TRIGGER) return;
|
||||
ImGui::SeparatorText(localize.get(BASIC_MODE));
|
||||
int mode = isBatchMode ? 1 : 0;
|
||||
ImGui::RadioButton(localize.get(BASIC_SINGLE), &mode, 0);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(localize.get(BASIC_BATCH), &mode, 1);
|
||||
isBatchMode = mode == 1;
|
||||
};
|
||||
|
||||
if (isSingleFrameBatchMode)
|
||||
{
|
||||
changeAllFrameProperties.update(manager, document, settings);
|
||||
mode_selector_draw();
|
||||
}
|
||||
else if (!isMultiFrameSelection)
|
||||
{
|
||||
auto useFrame = frame ? *frame : Element();
|
||||
auto displayFrame = frame && type == LAYER && reference.itemID != -1
|
||||
? anm2.frame_effective(reference.itemID, *frame)
|
||||
: useFrame;
|
||||
|
||||
ImGui::BeginDisabled(!frame);
|
||||
{
|
||||
if (type == anm2::TRIGGER)
|
||||
if (type == TRIGGER)
|
||||
{
|
||||
if (combo_id_mapped(localize.get(BASIC_EVENT), frame ? &useFrame.eventID : &dummy_value_negative<int>(),
|
||||
if (combo_id_mapped(localize.get(BASIC_EVENT), frame ? &useFrame.eventId : &dummy_value_negative<int>(),
|
||||
document.event.ids, document.event.labels) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_EVENT), Document::FRAMES,
|
||||
frame->eventID = useFrame.eventID);
|
||||
{
|
||||
auto eventId = useFrame.eventId;
|
||||
frame_edit(EDIT_TRIGGER_EVENT,
|
||||
[eventId](Document&, Element& frame, Element*, const Reference&) { frame.eventId = eventId; });
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_EVENT));
|
||||
|
||||
if (input_int_range(localize.get(BASIC_AT_FRAME), frame ? useFrame.atFrame : dummy_value<int>(), 0,
|
||||
std::numeric_limits<int>::max(), STEP, STEP_FAST,
|
||||
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_AT_FRAME), Document::FRAMES,
|
||||
frame->atFrame = useFrame.atFrame);
|
||||
{
|
||||
auto atFrame = useFrame.atFrame;
|
||||
frame_edit(EDIT_TRIGGER_AT_FRAME,
|
||||
[atFrame](Document& document, Element& frame, Element* item, const Reference&)
|
||||
{
|
||||
frame.atFrame = atFrame;
|
||||
if (!item) return;
|
||||
frames_sort_by_at_frame(*item);
|
||||
document.reference.frameIndex = frame_index_from_at_frame_get(*item, atFrame);
|
||||
document.frames.selection = {document.reference.frameIndex};
|
||||
document.frames.references = {document.reference};
|
||||
});
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_AT_FRAME));
|
||||
|
||||
if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value<bool>()) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_VISIBILITY), Document::FRAMES,
|
||||
frame->isVisible = useFrame.isVisible);
|
||||
{
|
||||
auto isVisible = useFrame.isVisible;
|
||||
frame_edit(EDIT_TRIGGER_VISIBILITY,
|
||||
[isVisible](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.isVisible = isVisible; });
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_VISIBILITY));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_SOUNDS));
|
||||
@@ -92,16 +182,23 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::BeginChild("##Sounds Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
if (!useFrame.soundIDs.empty())
|
||||
if (!useFrame.soundIds.empty())
|
||||
{
|
||||
for (auto [i, id] : std::views::enumerate(useFrame.soundIDs))
|
||||
for (auto [i, id] : std::views::enumerate(useFrame.soundIds))
|
||||
{
|
||||
ImGui::PushID(i);
|
||||
if (combo_id_mapped("##Sound", frame ? &id : &dummy_value_negative<int>(), document.sound.ids,
|
||||
document.sound.labels) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIGGER_SOUND), Document::FRAMES,
|
||||
frame->soundIDs[i] = id);
|
||||
{
|
||||
auto soundIndex = (std::size_t)i;
|
||||
auto soundId = id;
|
||||
frame_edit(EDIT_TRIGGER_SOUND,
|
||||
[soundIndex, soundId](Document&, Element& frame, Element*, const Reference&)
|
||||
{
|
||||
if (soundIndex < frame.soundIds.size()) frame.soundIds[soundIndex] = soundId;
|
||||
});
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIGGER_SOUND));
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -112,125 +209,127 @@ namespace anm2ed::imgui
|
||||
auto widgetSize = imgui::widget_size_with_row_get(2);
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize) && frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_TRIGGER_SOUND), Document::FRAMES,
|
||||
frame->soundIDs.push_back(-1));
|
||||
frame_edit(EDIT_ADD_TRIGGER_SOUND,
|
||||
[](Document&, Element& frame, Element*, const Reference&) { frame.soundIds.push_back(-1); });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_TRIGGER_SOUND));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(useFrame.soundIDs.empty());
|
||||
ImGui::BeginDisabled(useFrame.soundIds.empty());
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize) && frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_TRIGGER_SOUND), Document::FRAMES,
|
||||
frame->soundIDs.pop_back());
|
||||
frame_edit(EDIT_REMOVE_TRIGGER_SOUND,
|
||||
[](Document&, Element& frame, Element*, const Reference&)
|
||||
{
|
||||
if (!frame.soundIds.empty()) frame.soundIds.pop_back();
|
||||
});
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REMOVE_TRIGGER_SOUND));
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
bool isRegionSet = frame && displayFrame.regionID != -1 && displayFrame.crop == frame->crop &&
|
||||
bool isRegionSet = frame && displayFrame.regionId != -1 && displayFrame.crop == frame->crop &&
|
||||
displayFrame.size == frame->size && displayFrame.pivot == frame->pivot;
|
||||
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet);
|
||||
ImGui::BeginDisabled(type == ROOT || type == NULL_ || isRegionSet);
|
||||
{
|
||||
auto cropDisplay = frame ? displayFrame.crop : vec2();
|
||||
auto cropEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_CROP), frame ? &cropDisplay : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.crop) : "");
|
||||
if (cropEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_CROP));
|
||||
if (frame && cropEdit != edit::NONE) frame->crop = cropDisplay;
|
||||
if (cropEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
persistent_edit(cropEdit, EDIT_FRAME_CROP,
|
||||
[cropDisplay](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.crop = cropDisplay; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CROP));
|
||||
|
||||
auto sizeDisplay = frame ? displayFrame.size : vec2();
|
||||
auto sizeEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_SIZE), frame ? &sizeDisplay : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.size) : "");
|
||||
if (sizeEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_SIZE));
|
||||
if (frame && sizeEdit != edit::NONE) frame->size = sizeDisplay;
|
||||
if (sizeEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
persistent_edit(sizeEdit, EDIT_FRAME_SIZE,
|
||||
[sizeDisplay](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.size = sizeDisplay; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SIZE));
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
auto positionEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_POSITION), frame ? &frame->position : &dummy_value<vec2>(),
|
||||
drag_float2_persistent(localize.get(BASIC_POSITION),
|
||||
frame ? &useFrame.position : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->position) : "");
|
||||
if (positionEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_POSITION));
|
||||
else if (positionEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
persistent_edit(positionEdit, EDIT_FRAME_POSITION,
|
||||
[position = useFrame.position](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.position = position; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_POSITION));
|
||||
|
||||
ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_ || isRegionSet);
|
||||
ImGui::BeginDisabled(type == ROOT || type == NULL_ || isRegionSet);
|
||||
{
|
||||
auto pivotDisplay = frame ? displayFrame.pivot : vec2();
|
||||
auto pivotEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_PIVOT), frame ? &pivotDisplay : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(displayFrame.pivot) : "");
|
||||
if (pivotEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_PIVOT));
|
||||
if (frame && pivotEdit != edit::NONE) frame->pivot = pivotDisplay;
|
||||
if (pivotEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
persistent_edit(pivotEdit, EDIT_FRAME_PIVOT,
|
||||
[pivotDisplay](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.pivot = pivotDisplay; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PIVOT));
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
auto scaleEdit =
|
||||
drag_float2_persistent(localize.get(BASIC_SCALE), frame ? &frame->scale : &dummy_value<vec2>(),
|
||||
drag_float2_persistent(localize.get(BASIC_SCALE), frame ? &useFrame.scale : &dummy_value<vec2>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? vec2_format_get(frame->scale) : "");
|
||||
persistent_edit(scaleEdit, EDIT_FRAME_SCALE,
|
||||
[scale = useFrame.scale](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.scale = scale; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE));
|
||||
if (scaleEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_SCALE));
|
||||
else if (scaleEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
auto rotationEdit =
|
||||
drag_float_persistent(localize.get(BASIC_ROTATION), frame ? &frame->rotation : &dummy_value<float>(),
|
||||
drag_float_persistent(localize.get(BASIC_ROTATION), frame ? &useFrame.rotation : &dummy_value<float>(),
|
||||
DRAG_SPEED, 0.0f, 0.0f, frame ? float_format_get(frame->rotation) : "");
|
||||
persistent_edit(rotationEdit, EDIT_FRAME_ROTATION,
|
||||
[rotation = useFrame.rotation](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.rotation = rotation; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROTATION));
|
||||
if (rotationEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_ROTATION));
|
||||
else if (rotationEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
if (input_int_range(localize.get(BASIC_DURATION), frame ? useFrame.duration : dummy_value<int>(),
|
||||
frame ? anm2::FRAME_DURATION_MIN : 0, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST,
|
||||
frame ? FRAME_DURATION_MIN : 0, FRAME_DURATION_MAX, STEP, STEP_FAST,
|
||||
!frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_DURATION), Document::FRAMES,
|
||||
frame->duration = useFrame.duration);
|
||||
{
|
||||
auto duration = useFrame.duration;
|
||||
frame_edit(EDIT_FRAME_DURATION,
|
||||
[duration](Document&, Element& frame, Element*, const Reference&) { frame.duration = duration; });
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DURATION));
|
||||
|
||||
auto tintEdit =
|
||||
color_edit4_persistent(localize.get(BASIC_TINT), frame ? &frame->tint : &dummy_value<vec4>());
|
||||
color_edit4_persistent(localize.get(BASIC_TINT), frame ? &useFrame.tint : &dummy_value<vec4>());
|
||||
persistent_edit(tintEdit, EDIT_FRAME_TINT,
|
||||
[tint = useFrame.tint](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.tint = tint; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TINT));
|
||||
if (tintEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_TINT));
|
||||
else if (tintEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
auto colorOffsetEdit = color_edit3_persistent(localize.get(BASIC_COLOR_OFFSET),
|
||||
frame ? &frame->colorOffset : &dummy_value<vec3>());
|
||||
frame ? &useFrame.colorOffset : &dummy_value<vec3>());
|
||||
persistent_edit(colorOffsetEdit, EDIT_FRAME_COLOR_OFFSET,
|
||||
[colorOffset = useFrame.colorOffset](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.colorOffset = colorOffset; });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COLOR_OFFSET));
|
||||
if (colorOffsetEdit == edit::START)
|
||||
document.snapshot(localize.get(EDIT_FRAME_COLOR_OFFSET));
|
||||
else if (colorOffsetEdit == edit::END)
|
||||
document.change(Document::FRAMES);
|
||||
|
||||
ImGui::BeginDisabled(type != anm2::LAYER);
|
||||
if (combo_id_mapped(localize.get(BASIC_REGION), frame ? &useFrame.regionID : &dummy_value_negative<int>(),
|
||||
regionIds, regionLabels) &&
|
||||
ImGui::BeginDisabled(type != LAYER);
|
||||
if (combo_id_mapped(localize.get(BASIC_REGION), frame ? &useFrame.regionId : &dummy_value_negative<int>(),
|
||||
regionIds, regionLabels) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_REGION_PROPERTIES), Document::FRAMES,
|
||||
frame->regionID = useFrame.regionID;
|
||||
auto effectiveFrame = document.anm2.frame_effective(document.reference.itemID, *frame);
|
||||
frame->crop = effectiveFrame.crop;
|
||||
frame->size = effectiveFrame.size;
|
||||
frame->pivot = effectiveFrame.pivot);
|
||||
{
|
||||
auto regionId = useFrame.regionId;
|
||||
frame_edit(EDIT_SET_REGION_PROPERTIES,
|
||||
[regionId](Document& document, Element& frame, Element*, const Reference& reference)
|
||||
{
|
||||
frame.regionId = regionId;
|
||||
auto effectiveFrame = document.anm2.frame_effective(reference.itemID, frame);
|
||||
frame.crop = effectiveFrame.crop;
|
||||
frame.size = effectiveFrame.size;
|
||||
frame.pivot = effectiveFrame.pivot;
|
||||
});
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
@@ -238,14 +337,19 @@ namespace anm2ed::imgui
|
||||
if (combo_id_mapped(localize.get(BASIC_INTERPOLATED), &interpolationValue, interpolationValues,
|
||||
interpolationLabels) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_INTERPOLATION), Document::FRAMES,
|
||||
frame->interpolation = static_cast<anm2::Frame::Interpolation>(interpolationValue));
|
||||
frame_edit(EDIT_FRAME_INTERPOLATION,
|
||||
[interpolationValue](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.interpolation = static_cast<Interpolation>(interpolationValue); });
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FRAME_INTERPOLATION));
|
||||
|
||||
if (ImGui::Checkbox(localize.get(BASIC_VISIBLE), frame ? &useFrame.isVisible : &dummy_value<bool>()) &&
|
||||
frame)
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_VISIBILITY), Document::FRAMES,
|
||||
frame->isVisible = useFrame.isVisible);
|
||||
{
|
||||
auto isVisible = useFrame.isVisible;
|
||||
frame_edit(EDIT_FRAME_VISIBILITY,
|
||||
[isVisible](Document&, Element& frame, Element*, const Reference&)
|
||||
{ frame.isVisible = isVisible; });
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FRAME_VISIBILITY));
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
@@ -253,40 +357,40 @@ namespace anm2ed::imgui
|
||||
if (ImGui::Button(localize.get(LABEL_FLIP_X), widgetSize) && frame)
|
||||
{
|
||||
if (ImGui::IsKeyDown(ImGuiMod_Ctrl))
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_X), Document::FRAMES,
|
||||
frame->scale.x = -frame->scale.x;
|
||||
frame->position.x = -frame->position.x);
|
||||
}
|
||||
frame_edit(EDIT_FRAME_FLIP_X,
|
||||
[](Document&, Element& frame, Element*, const Reference&)
|
||||
{
|
||||
frame.scale.x = -frame.scale.x;
|
||||
frame.position.x = -frame.position.x;
|
||||
});
|
||||
else
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_X), Document::FRAMES,
|
||||
frame->scale.x = -frame->scale.x);
|
||||
}
|
||||
frame_edit(EDIT_FRAME_FLIP_X,
|
||||
[](Document&, Element& frame, Element*, const Reference&) { frame.scale.x = -frame.scale.x; });
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FLIP_X));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(localize.get(LABEL_FLIP_Y), widgetSize) && frame)
|
||||
{
|
||||
if (ImGui::IsKeyDown(ImGuiMod_Ctrl))
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_Y), Document::FRAMES,
|
||||
frame->scale.y = -frame->scale.y;
|
||||
frame->position.y = -frame->position.y);
|
||||
}
|
||||
frame_edit(EDIT_FRAME_FLIP_Y,
|
||||
[](Document&, Element& frame, Element*, const Reference&)
|
||||
{
|
||||
frame.scale.y = -frame.scale.y;
|
||||
frame.position.y = -frame.position.y;
|
||||
});
|
||||
else
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_FLIP_Y), Document::FRAMES,
|
||||
frame->scale.y = -frame->scale.y);
|
||||
}
|
||||
frame_edit(EDIT_FRAME_FLIP_Y,
|
||||
[](Document&, Element& frame, Element*, const Reference&) { frame.scale.y = -frame.scale.y; });
|
||||
}
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FLIP_Y));
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
mode_selector_draw();
|
||||
}
|
||||
else
|
||||
changeAllFrameProperties.update(document, settings);
|
||||
changeAllFrameProperties.update(manager, document, settings);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace anm2ed::imgui
|
||||
class FrameProperties
|
||||
{
|
||||
wizard::ChangeAllFrameProperties changeAllFrameProperties{};
|
||||
bool isBatchMode{};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&);
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
#include "layers.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
void Layers::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.layer.reference;
|
||||
auto& selection = document.layer.selection;
|
||||
auto& propertiesPopup = manager.layerPropertiesPopup;
|
||||
|
||||
auto add = [&]() { manager.layer_properties_open(); };
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.layers_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
anm2.content.layers.erase(id);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_LAYERS), Document::LAYERS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.layers[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxLayerIdBefore = anm2.content.layers.empty() ? -1 : anm2.content.layers.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_LAYERS));
|
||||
if (anm2.layers_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.layers.empty())
|
||||
{
|
||||
auto maxLayerIdAfter = anm2.content.layers.rbegin()->first;
|
||||
if (maxLayerIdAfter > maxLayerIdBefore)
|
||||
{
|
||||
newLayerId = maxLayerIdAfter;
|
||||
selection = {maxLayerIdAfter};
|
||||
reference = maxLayerIdAfter;
|
||||
}
|
||||
}
|
||||
document.change(Document::NULLS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_LAYERS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_LAYERS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_LAYERS), Document::LAYERS, behavior());
|
||||
};
|
||||
|
||||
auto properties = [&](int id) { manager.layer_properties_open(id); };
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_LAYERS_WINDOW), &settings.windowIsLayers))
|
||||
{
|
||||
auto childSize = size_without_footer_get();
|
||||
|
||||
if (ImGui::BeginChild("##Layers Child", childSize, true))
|
||||
{
|
||||
selection.start(anm2.content.layers.size());
|
||||
|
||||
for (auto& [id, layer] : anm2.content.layers)
|
||||
{
|
||||
auto isSelected = selection.contains(id);
|
||||
|
||||
ImGui::PushID(id);
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
ImGui::Selectable(
|
||||
std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, layer.name, layer.spritesheetID))
|
||||
.c_str(),
|
||||
isSelected);
|
||||
if (newLayerId == id)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newLayerId = -1;
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) properties(id);
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(layer.name.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer.spritesheetID)).c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
selection.finish();
|
||||
|
||||
if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
|
||||
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1))
|
||||
properties(*selection.begin());
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
copy();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_LAYER), settings.shortcutAdd);
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_LAYERS), settings.shortcutRemove);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
manager.layer_properties_trigger();
|
||||
|
||||
if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
auto childSize = child_size_get(2);
|
||||
auto& layer = manager.editLayer;
|
||||
|
||||
if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
|
||||
input_text_string(localize.get(BASIC_NAME), &layer.name);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME));
|
||||
|
||||
combo_id_mapped(localize.get(LABEL_SPRITESHEET), &layer.spritesheetID, document.spritesheet.ids,
|
||||
document.spritesheet.labels);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LAYER_SPRITESHEET));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize))
|
||||
{
|
||||
if (reference == -1)
|
||||
{
|
||||
auto layer_add = [&]()
|
||||
{
|
||||
auto id = map::next_id_get(anm2.content.layers);
|
||||
anm2.content.layers[id] = layer;
|
||||
selection = {id};
|
||||
newLayerId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_LAYER), Document::LAYERS, layer_add());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto layer_set = [&]()
|
||||
{
|
||||
anm2.content.layers[reference] = layer;
|
||||
selection = {reference};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_LAYER_PROPERTIES), Document::LAYERS, layer_set());
|
||||
}
|
||||
|
||||
manager.layer_properties_close();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.layer_properties_close();
|
||||
|
||||
manager.layer_properties_end();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Layers
|
||||
{
|
||||
int newLayerId{-1};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||
};
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
#include "nulls.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::types;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
void Nulls::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.null.reference;
|
||||
auto& selection = document.null.selection;
|
||||
auto& propertiesPopup = manager.nullPropertiesPopup;
|
||||
|
||||
auto add = [&]() { manager.null_properties_open(); };
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.nulls_unused();
|
||||
if (unused.empty()) return;
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
anm2.content.nulls.erase(id);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_NULLS), Document::NULLS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.nulls[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxNullIdBefore = anm2.content.nulls.empty() ? -1 : anm2.content.nulls.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_NULLS));
|
||||
if (anm2.nulls_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.nulls.empty())
|
||||
{
|
||||
auto maxNullIdAfter = anm2.content.nulls.rbegin()->first;
|
||||
if (maxNullIdAfter > maxNullIdBefore)
|
||||
{
|
||||
newNullId = maxNullIdAfter;
|
||||
selection = {maxNullIdAfter};
|
||||
reference = maxNullIdAfter;
|
||||
}
|
||||
}
|
||||
document.change(Document::NULLS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_NULLS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_NULLS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_NULLS), Document::NULLS, behavior());
|
||||
};
|
||||
|
||||
auto properties = [&](int id) { manager.null_properties_open(id); };
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_NULLS_WINDOW), &settings.windowIsNulls))
|
||||
{
|
||||
auto childSize = size_without_footer_get();
|
||||
|
||||
if (ImGui::BeginChild("##Nulls Child", childSize, true))
|
||||
{
|
||||
selection.start(anm2.content.nulls.size());
|
||||
|
||||
for (auto& [id, null] : anm2.content.nulls)
|
||||
{
|
||||
auto isSelected = selection.contains(id);
|
||||
auto isReferenced = reference == id;
|
||||
|
||||
ImGui::PushID(id);
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
|
||||
ImGui::Selectable(std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null.name)).c_str(),
|
||||
isSelected);
|
||||
if (newNullId == id)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newNullId = -1;
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) properties(id);
|
||||
|
||||
if (isReferenced) ImGui::PopFont();
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(null.name.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
selection.finish();
|
||||
|
||||
if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
|
||||
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1))
|
||||
properties(*selection.begin());
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty()))
|
||||
copy();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_NULL), settings.shortcutAdd);
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_NULLS), settings.shortcutRemove);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
manager.null_properties_trigger();
|
||||
|
||||
if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
auto childSize = child_size_get(2);
|
||||
auto& null = manager.editNull;
|
||||
|
||||
if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
|
||||
input_text_string(localize.get(BASIC_NAME), &null.name);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_NAME));
|
||||
|
||||
ImGui::Checkbox(localize.get(LABEL_RECT), &null.isShowRect);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_NULL_RECT));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize))
|
||||
{
|
||||
if (reference == -1)
|
||||
{
|
||||
auto null_add = [&]()
|
||||
{
|
||||
auto id = map::next_id_get(anm2.content.nulls);
|
||||
anm2.content.nulls[id] = null;
|
||||
selection = {id};
|
||||
newNullId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_NULL), Document::NULLS, null_add());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto null_set = [&]()
|
||||
{
|
||||
anm2.content.nulls[reference] = null;
|
||||
selection = {reference};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_NULL_PROPERTIES), Document::NULLS, null_set());
|
||||
}
|
||||
|
||||
manager.null_properties_close();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) manager.null_properties_close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
manager.null_properties_end();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Nulls
|
||||
{
|
||||
int newNullId{-1};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
|
||||
@@ -1,605 +0,0 @@
|
||||
#include "regions.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "document.hpp"
|
||||
#include "log.hpp"
|
||||
#include "map_.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "vector_.hpp"
|
||||
|
||||
#include "../../util/map_.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
void Regions::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& selection = document.region.selection;
|
||||
auto& frame = document.frames;
|
||||
auto& spritesheetReference = document.spritesheet.reference;
|
||||
auto& reference = document.region.reference;
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
auto spritesheet = map::find(anm2.content.spritesheets, spritesheetReference);
|
||||
|
||||
if (manager.isMakeRegionRequested)
|
||||
{
|
||||
spritesheetReference = manager.makeRegionSpritesheetId;
|
||||
reference = -1;
|
||||
editRegion = manager.makeRegion;
|
||||
isPreserveEditRegionOnOpen = true;
|
||||
propertiesPopup.open();
|
||||
manager.isMakeRegionRequested = false;
|
||||
spritesheet = map::find(anm2.content.spritesheets, spritesheetReference);
|
||||
}
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
if (!spritesheet) return;
|
||||
|
||||
auto unused = anm2.regions_unused(*spritesheet);
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
{
|
||||
for (auto& animation : anm2.animations.items)
|
||||
for (auto& layerAnimation : animation.layerAnimations | std::views::values)
|
||||
for (auto& frame : layerAnimation.frames)
|
||||
if (frame.regionID == id) frame.regionID = -1;
|
||||
|
||||
spritesheet->regions.erase(id);
|
||||
auto& order = spritesheet->regionOrder;
|
||||
order.erase(std::remove(order.begin(), order.end(), id), order.end());
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_REGIONS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto trim = [&]()
|
||||
{
|
||||
if (!spritesheet || selection.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
if (anm2.regions_trim(spritesheetReference, selection))
|
||||
{
|
||||
if (reference != -1 && !selection.contains(reference)) reference = *selection.begin();
|
||||
document.reference = {document.reference.animationIndex};
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_TRIM_REGIONS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (!spritesheet || selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += spritesheet->region_to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (!spritesheet || clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxRegionIdBefore = spritesheet->regions.empty() ? -1 : spritesheet->regions.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_REGIONS));
|
||||
if (spritesheet->regions_deserialize(clipboard.get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!spritesheet->regions.empty())
|
||||
{
|
||||
auto maxRegionIdAfter = spritesheet->regions.rbegin()->first;
|
||||
if (maxRegionIdAfter > maxRegionIdBefore)
|
||||
{
|
||||
newRegionId = maxRegionIdAfter;
|
||||
selection = {maxRegionIdAfter};
|
||||
reference = maxRegionIdAfter;
|
||||
}
|
||||
}
|
||||
document.change(Document::SPRITESHEETS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_REGIONS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_REGIONS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto add_open = [&]()
|
||||
{
|
||||
reference = -1;
|
||||
editRegion = anm2::Spritesheet::Region{};
|
||||
propertiesPopup.open();
|
||||
};
|
||||
|
||||
auto properties_open = [&](int id)
|
||||
{
|
||||
if (!spritesheet || !spritesheet->regions.contains(id)) return;
|
||||
reference = id;
|
||||
propertiesPopup.open();
|
||||
};
|
||||
|
||||
auto context_menu = [&]()
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right))
|
||||
ImGui::OpenPopup("##Region Context Menu");
|
||||
|
||||
if (ImGui::BeginPopup("##Region Context Menu"))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PROPERTIES), nullptr, false, selection.size() == 1))
|
||||
properties_open(*selection.begin());
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_TRIM), nullptr, false, !selection.empty())) trim();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TRIM_REGIONS));
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_REGIONS_WINDOW), &settings.windowIsRegions))
|
||||
{
|
||||
if (!spritesheet)
|
||||
{
|
||||
ImGui::TextUnformatted(localize.get(TEXT_SELECT_SPRITESHEET));
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
auto childSize = size_without_footer_get();
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
if (ImGui::BeginChild("##Regions Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto regionChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
|
||||
|
||||
auto rebuild_order = [&]()
|
||||
{
|
||||
spritesheet->regionOrder.clear();
|
||||
spritesheet->regionOrder.reserve(spritesheet->regions.size());
|
||||
for (auto id : spritesheet->regions | std::views::keys)
|
||||
spritesheet->regionOrder.push_back(id);
|
||||
};
|
||||
if (spritesheet->regionOrder.size() != spritesheet->regions.size())
|
||||
rebuild_order();
|
||||
else
|
||||
{
|
||||
bool isOrderValid = true;
|
||||
for (auto id : spritesheet->regionOrder)
|
||||
if (!spritesheet->regions.contains(id))
|
||||
{
|
||||
isOrderValid = false;
|
||||
break;
|
||||
}
|
||||
if (!isOrderValid) rebuild_order();
|
||||
}
|
||||
|
||||
selection.set_index_map(&spritesheet->regionOrder);
|
||||
selection.start(spritesheet->regionOrder.size());
|
||||
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused))
|
||||
{
|
||||
selection.clear();
|
||||
for (auto& id : spritesheet->regionOrder)
|
||||
selection.insert(id);
|
||||
}
|
||||
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
|
||||
auto scroll_to_item = [&](float itemHeight, bool isTarget)
|
||||
{
|
||||
if (!isTarget) return;
|
||||
auto windowHeight = ImGui::GetWindowHeight();
|
||||
auto targetTop = ImGui::GetCursorPosY();
|
||||
auto targetBottom = targetTop + itemHeight;
|
||||
auto visibleTop = ImGui::GetScrollY();
|
||||
auto visibleBottom = visibleTop + windowHeight;
|
||||
if (targetTop < visibleTop)
|
||||
ImGui::SetScrollY(targetTop);
|
||||
else if (targetBottom > visibleBottom)
|
||||
ImGui::SetScrollY(targetBottom - windowHeight);
|
||||
};
|
||||
int scrollTargetId = -1;
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
|
||||
{
|
||||
auto& order = spritesheet->regionOrder;
|
||||
if (!order.empty())
|
||||
{
|
||||
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
|
||||
int current = reference;
|
||||
if (current == -1 && !selection.empty()) current = *selection.begin();
|
||||
auto it = std::find(order.begin(), order.end(), current);
|
||||
int index = it == order.end() ? 0 : (int)std::distance(order.begin(), it);
|
||||
index = std::clamp(index + delta, 0, (int)order.size() - 1);
|
||||
int nextId = order[index];
|
||||
selection = {nextId};
|
||||
reference = nextId;
|
||||
document.reference = {document.reference.animationIndex};
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
scrollTargetId = nextId;
|
||||
}
|
||||
}
|
||||
bool isValid = spritesheet->is_valid();
|
||||
auto& texture = isValid ? spritesheet->texture : resources.icons[icon::NONE];
|
||||
auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
static std::vector<int> dragDropSelection{};
|
||||
bool isRegionDragDropSourceSubmitted = false;
|
||||
|
||||
for (int i = 0; i < (int)spritesheet->regionOrder.size(); i++)
|
||||
{
|
||||
int id = spritesheet->regionOrder[i];
|
||||
auto regionIt = spritesheet->regions.find(id);
|
||||
if (regionIt == spritesheet->regions.end()) continue;
|
||||
auto& region = regionIt->second;
|
||||
auto isNewRegion = newRegionId == id;
|
||||
auto nameCStr = region.name.c_str();
|
||||
auto isSelected = selection.contains(id);
|
||||
auto isReferenced = id == reference;
|
||||
|
||||
ImGui::PushID(id);
|
||||
|
||||
scroll_to_item(regionChildSize.y, scrollTargetId == id);
|
||||
|
||||
if (ImGui::BeginChild("##Region Child", regionChildSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(i);
|
||||
ImGui::SetNextItemStorageID(id);
|
||||
if (ImGui::Selectable("##Region Selectable", isSelected, 0, regionChildSize))
|
||||
{
|
||||
reference = id;
|
||||
document.reference = {document.reference.animationIndex};
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
}
|
||||
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) propertiesPopup.open();
|
||||
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f;
|
||||
vec2 regionSize = glm::max(region.size, vec2(1.0f));
|
||||
vec2 previewSize = regionSize;
|
||||
if (previewSize.x > maxPreviewSize.x || previewSize.y > maxPreviewSize.y)
|
||||
{
|
||||
auto scale = glm::min(maxPreviewSize.x / previewSize.x, maxPreviewSize.y / previewSize.y);
|
||||
previewSize *= scale;
|
||||
}
|
||||
vec2 uvMin{};
|
||||
vec2 uvMax{1.0f, 1.0f};
|
||||
if (isValid)
|
||||
{
|
||||
uvMin = region.crop / vec2(texture.size);
|
||||
uvMax = (region.crop + region.size) / vec2(texture.size);
|
||||
}
|
||||
|
||||
auto textWidth = ImGui::CalcTextSize(nameCStr).x;
|
||||
auto tooltipPadding = style.WindowPadding.x * 4.0f;
|
||||
auto minWidth = previewSize.x + style.ItemSpacing.x + textWidth + tooltipPadding;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && !ImGui::IsMouseDragging(ImGuiMouseButton_Left))
|
||||
{
|
||||
ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
auto childFlags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY;
|
||||
auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
|
||||
if (ImGui::BeginChild("##Region Tooltip Image Child", to_imvec2(previewSize), childFlags,
|
||||
noScrollFlags))
|
||||
ImGui::ImageWithBg(texture.id, to_imvec2(previewSize), to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(),
|
||||
tintColor);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
auto infoChildFlags = ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY;
|
||||
if (ImGui::BeginChild("##Region Info Tooltip Child", ImVec2(), infoChildFlags, noScrollFlags))
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(nameCStr);
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region.crop.x, region.crop.y))
|
||||
.c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y))
|
||||
.c_str());
|
||||
if (region.origin == anm2::Spritesheet::Region::CUSTOM)
|
||||
{
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y))
|
||||
.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
StringType originString = LABEL_REGION_ORIGIN_CENTER;
|
||||
if (region.origin == anm2::Spritesheet::Region::TOP_LEFT)
|
||||
originString = LABEL_REGION_ORIGIN_TOP_LEFT;
|
||||
auto originLabel = localize.get(originString);
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str());
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip))
|
||||
{
|
||||
isRegionDragDropSourceSubmitted = true;
|
||||
if (selection.contains(id))
|
||||
dragDropSelection.assign(selection.begin(), selection.end());
|
||||
else
|
||||
dragDropSelection = {id};
|
||||
|
||||
ImGui::SetDragDropPayload("Region Drag Drop", dragDropSelection.data(),
|
||||
dragDropSelection.size() * sizeof(int));
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (auto payload = ImGui::AcceptDragDropPayload("Region Drag Drop"))
|
||||
{
|
||||
auto payloadIds = (int*)(payload->Data);
|
||||
int payloadCount = (int)(payload->DataSize / sizeof(int));
|
||||
std::vector<int> indices{};
|
||||
indices.reserve(payloadCount);
|
||||
for (int payloadIndex = 0; payloadIndex < payloadCount; payloadIndex++)
|
||||
{
|
||||
int payloadId = payloadIds[payloadIndex];
|
||||
int index = vector::find_index(spritesheet->regionOrder, payloadId);
|
||||
if (index != -1) indices.push_back(index);
|
||||
}
|
||||
if (!indices.empty())
|
||||
{
|
||||
std::sort(indices.begin(), indices.end());
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MOVE_REGIONS), Document::SPRITESHEETS,
|
||||
vector::move_indices(spritesheet->regionOrder, indices, i));
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
auto imageSize = to_imvec2(vec2(regionChildSize.y));
|
||||
auto aspectRatio = region.size.y != 0.0f ? (float)region.size.x / region.size.y : 1.0f;
|
||||
|
||||
if (imageSize.x / imageSize.y > aspectRatio)
|
||||
imageSize.x = imageSize.y * aspectRatio;
|
||||
else
|
||||
imageSize.y = imageSize.x / aspectRatio;
|
||||
|
||||
ImGui::SetCursorPos(cursorPos);
|
||||
ImGui::ImageWithBg(texture.id, imageSize, to_imvec2(uvMin), to_imvec2(uvMax), ImVec4(), tintColor);
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(regionChildSize.y + style.ItemSpacing.x,
|
||||
regionChildSize.y - regionChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
|
||||
|
||||
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(nameCStr);
|
||||
if (isReferenced) ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (isNewRegion)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newRegionId = -1;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
auto regionDragPayload = ImGui::GetDragDropPayload();
|
||||
bool isRegionDragActive = regionDragPayload && regionDragPayload->IsDataType("Region Drag Drop");
|
||||
if (isRegionDragActive && !isRegionDragDropSourceSubmitted && !dragDropSelection.empty() &&
|
||||
ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceExtern | ImGuiDragDropFlags_SourceNoPreviewTooltip))
|
||||
{
|
||||
ImGui::SetDragDropPayload("Region Drag Drop", dragDropSelection.data(),
|
||||
dragDropSelection.size() * sizeof(int));
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
if (isRegionDragActive && !dragDropSelection.empty())
|
||||
{
|
||||
auto mousePos = ImGui::GetIO().MousePos;
|
||||
auto tooltipOffset = ImVec2(16.0f, 16.0f);
|
||||
ImGui::SetNextWindowPos(ImVec2(mousePos.x + tooltipOffset.x, mousePos.y + tooltipOffset.y));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
for (auto regionId : dragDropSelection)
|
||||
{
|
||||
auto dragIt = spritesheet->regions.find(regionId);
|
||||
if (dragIt == spritesheet->regions.end()) continue;
|
||||
ImGui::TextUnformatted(dragIt->second.name.c_str());
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
selection.finish();
|
||||
|
||||
if (shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open();
|
||||
if (shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
context_menu();
|
||||
|
||||
auto rowOneWidgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) add_open();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_REGION), settings.shortcutAdd);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowOneWidgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_REGIONS), settings.shortcutAdd);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
propertiesPopup.trigger();
|
||||
|
||||
if (ImGui::BeginPopupModal(propertiesPopup.label(), &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
if (!spritesheet || (reference != -1 && !spritesheet->regions.contains(reference)))
|
||||
{
|
||||
propertiesPopup.close();
|
||||
ImGui::EndPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
auto childSize = child_size_get(5);
|
||||
auto& region = reference == -1 ? editRegion : spritesheet->regions.at(reference);
|
||||
|
||||
if (propertiesPopup.isJustOpened)
|
||||
{
|
||||
if (!isPreserveEditRegionOnOpen)
|
||||
editRegion = anm2::Spritesheet::Region{};
|
||||
isPreserveEditRegionOnOpen = false;
|
||||
}
|
||||
|
||||
if (ImGui::BeginChild("##Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
const char* originOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT),
|
||||
localize.get(LABEL_REGION_ORIGIN_CENTER),
|
||||
localize.get(LABEL_REGION_ORIGIN_CUSTOM)};
|
||||
|
||||
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
|
||||
input_text_string(localize.get(BASIC_NAME), ®ion.name);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME));
|
||||
ImGui::DragFloat2(localize.get(BASIC_CROP), value_ptr(region.crop), DRAG_SPEED, 0.0f, 0.0f,
|
||||
math::vec2_format_get(region.crop));
|
||||
ImGui::DragFloat2(localize.get(BASIC_SIZE), value_ptr(region.size), DRAG_SPEED, 0.0f, 0.0f,
|
||||
math::vec2_format_get(region.size));
|
||||
ImGui::BeginDisabled(region.origin != anm2::Spritesheet::Region::CUSTOM);
|
||||
ImGui::DragFloat2(localize.get(BASIC_PIVOT), value_ptr(region.pivot), DRAG_SPEED, 0.0f, 0.0f,
|
||||
math::vec2_format_get(region.pivot));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), (int*)®ion.origin, originOptions,
|
||||
IM_ARRAYSIZE(originOptions)))
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REGION_PROPERTIES_ORIGIN));
|
||||
|
||||
if (region.origin == anm2::Spritesheet::Region::TOP_LEFT)
|
||||
region.pivot = {};
|
||||
else if (region.origin == anm2::Spritesheet::Region::ORIGIN_CENTER)
|
||||
region.pivot = {(int)(region.size.x / 2.0f), (int)(region.size.y / 2.0f)};
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
if (ImGui::Button(reference == -1 ? localize.get(BASIC_ADD) : localize.get(BASIC_CONFIRM), widgetSize))
|
||||
{
|
||||
if (reference == -1)
|
||||
{
|
||||
auto add = [&]()
|
||||
{
|
||||
auto id = map::next_id_get(spritesheet->regions);
|
||||
spritesheet->regions[id] = region;
|
||||
spritesheet->regionOrder.push_back(id);
|
||||
selection = {id};
|
||||
newRegionId = id;
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_REGION), Document::SPRITESHEETS, add());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto set = [&]()
|
||||
{
|
||||
spritesheet->regions.at(reference) = region;
|
||||
selection = {reference};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_REGION_PROPERTIES), Document::SPRITESHEETS, set());
|
||||
}
|
||||
|
||||
frame.reference = -1;
|
||||
frame.selection.clear();
|
||||
|
||||
propertiesPopup.close();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) propertiesPopup.close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
propertiesPopup.end();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Regions
|
||||
{
|
||||
public:
|
||||
anm2::Spritesheet::Region editRegion{};
|
||||
int newRegionId{-1};
|
||||
bool isPreserveEditRegionOnOpen{};
|
||||
imgui::PopupHelper propertiesPopup{imgui::PopupHelper(LABEL_REGION_PROPERTIES, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
|
||||
void update(Manager&, Settings&, Resources&, Clipboard&);
|
||||
};
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
#include "sounds.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
void Sounds::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.sound.reference;
|
||||
auto& selection = document.sound.selection;
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
auto add_open = [&]() { dialog.file_open(Dialog::SOUND_OPEN); };
|
||||
auto replace_open = [&]() { dialog.file_open(Dialog::SOUND_REPLACE); };
|
||||
|
||||
auto play = [&](anm2::Sound& sound) { sound.play(); };
|
||||
|
||||
auto add = [&](const std::filesystem::path& path)
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
int id{};
|
||||
auto pathString = path::to_utf8(path);
|
||||
if (anm2.sound_add(document.directory_get(), path, id))
|
||||
{
|
||||
selection = {id};
|
||||
newSoundId = id;
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(pathString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_ADD_SOUND), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.sounds_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
anm2.content.sounds.erase(id);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SOUNDS), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto reload = [&]()
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : selection)
|
||||
{
|
||||
anm2::Sound& sound = anm2.content.sounds[id];
|
||||
sound.reload(document.directory_get());
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_RELOAD_SOUND), std::make_format_args(id, pathString)));
|
||||
logger.info(
|
||||
std::vformat(localize.get(TOAST_RELOAD_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_RELOAD_SOUNDS), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto replace = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (selection.size() != 1 || path.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto& id = *selection.begin();
|
||||
anm2::Sound& sound = anm2.content.sounds[id];
|
||||
sound = anm2::Sound(document.directory_get(), path);
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_REPLACE_SOUND), std::make_format_args(id, pathString)));
|
||||
logger.info(
|
||||
std::vformat(localize.get(TOAST_REPLACE_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString)));
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SOUND), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto open_directory = [&](anm2::Sound& sound)
|
||||
{
|
||||
if (sound.path.empty()) return;
|
||||
std::error_code ec{};
|
||||
auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / sound.path, ec);
|
||||
if (ec) absolutePath = document.directory_get() / sound.path;
|
||||
auto target = std::filesystem::is_directory(absolutePath) ? absolutePath
|
||||
: std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path()
|
||||
: document.directory_get();
|
||||
dialog.file_explorer_open(target);
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.sounds[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxSoundIdBefore = anm2.content.sounds.empty() ? -1 : anm2.content.sounds.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(TOAST_SOUNDS_PASTE));
|
||||
if (anm2.sounds_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.sounds.empty())
|
||||
{
|
||||
auto maxSoundIdAfter = anm2.content.sounds.rbegin()->first;
|
||||
if (maxSoundIdAfter > maxSoundIdBefore)
|
||||
{
|
||||
newSoundId = maxSoundIdAfter;
|
||||
selection = {maxSoundIdAfter};
|
||||
reference = maxSoundIdAfter;
|
||||
}
|
||||
}
|
||||
document.change(Document::SOUNDS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SOUNDS_DESERIALIZE_ERROR, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_SOUNDS), Document::SOUNDS, behavior());
|
||||
};
|
||||
|
||||
auto context_menu = [&]()
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right))
|
||||
ImGui::OpenPopup("##Sound Context Menu");
|
||||
|
||||
if (ImGui::BeginPopup("##Sound Context Menu"))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_PLAY), nullptr, false, selection.size() == 1))
|
||||
play(anm2.content.sounds[*selection.begin()]);
|
||||
if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1))
|
||||
open_directory(anm2.content.sounds[*selection.begin()]);
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RELOAD), nullptr, false, !selection.empty())) reload();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REPLACE), nullptr, false, selection.size() == 1)) replace_open();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_SOUNDS_WINDOW), &settings.windowIsSounds))
|
||||
{
|
||||
auto childSize = imgui::size_without_footer_get();
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
if (ImGui::BeginChild("##Sounds Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto soundChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 2);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
|
||||
|
||||
selection.start(anm2.content.sounds.size());
|
||||
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused))
|
||||
{
|
||||
selection.clear();
|
||||
for (auto& id : anm2.content.sounds | std::views::keys)
|
||||
selection.insert(id);
|
||||
}
|
||||
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
|
||||
auto scroll_to_item = [&](float itemHeight, bool isTarget)
|
||||
{
|
||||
if (!isTarget) return;
|
||||
auto windowHeight = ImGui::GetWindowHeight();
|
||||
auto targetTop = ImGui::GetCursorPosY();
|
||||
auto targetBottom = targetTop + itemHeight;
|
||||
auto visibleTop = ImGui::GetScrollY();
|
||||
auto visibleBottom = visibleTop + windowHeight;
|
||||
if (targetTop < visibleTop)
|
||||
ImGui::SetScrollY(targetTop);
|
||||
else if (targetBottom > visibleBottom)
|
||||
ImGui::SetScrollY(targetBottom - windowHeight);
|
||||
};
|
||||
int scrollTargetId = -1;
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.reserve(anm2.content.sounds.size());
|
||||
for (auto& [id, sound] : anm2.content.sounds)
|
||||
ids.push_back(id);
|
||||
if (!ids.empty())
|
||||
{
|
||||
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
|
||||
int current = reference;
|
||||
if (current == -1 && !selection.empty()) current = *selection.begin();
|
||||
auto it = std::find(ids.begin(), ids.end(), current);
|
||||
int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it);
|
||||
index = std::clamp(index + delta, 0, (int)ids.size() - 1);
|
||||
int nextId = ids[index];
|
||||
selection = {nextId};
|
||||
reference = nextId;
|
||||
scrollTargetId = nextId;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [id, sound] : anm2.content.sounds)
|
||||
{
|
||||
auto isNewSound = newSoundId == id;
|
||||
ImGui::PushID(id);
|
||||
|
||||
scroll_to_item(soundChildSize.y, scrollTargetId == id);
|
||||
|
||||
if (ImGui::BeginChild("##Sound Child", soundChildSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto isSelected = selection.contains(id);
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
bool isValid = sound.is_valid();
|
||||
auto& soundIcon = isValid ? resources.icons[icon::SOUND] : resources.icons[icon::NONE];
|
||||
auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
auto pathString = path::to_utf8(sound.path);
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
ImGui::SetNextItemStorageID(id);
|
||||
if (ImGui::Selectable("##Sound Selectable", isSelected, 0, soundChildSize))
|
||||
{
|
||||
reference = id;
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) play(sound);
|
||||
}
|
||||
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) open_directory(sound);
|
||||
|
||||
auto textWidth = ImGui::CalcTextSize(pathString.c_str()).x;
|
||||
auto tooltipPadding = style.WindowPadding.x * 4.0f;
|
||||
auto minWidth = textWidth + style.ItemSpacing.x + tooltipPadding;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(pathString.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::Text("%s: %d", localize.get(BASIC_ID), id);
|
||||
if (!isValid)
|
||||
{
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", localize.get(TOOLTIP_SOUND_INVALID));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text("%s", localize.get(TEXT_SOUND_PLAY));
|
||||
ImGui::Text("%s", localize.get(TEXT_OPEN_DIRECTORY));
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
ImGui::SetCursorPos(cursorPos);
|
||||
auto imageSize = to_imvec2(vec2(soundChildSize.y));
|
||||
ImGui::ImageWithBg(soundIcon.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(soundChildSize.y + style.ItemSpacing.x,
|
||||
soundChildSize.y - soundChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
|
||||
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString)).c_str());
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (isNewSound)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newSoundId = -1;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
selection.finish();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
context_menu();
|
||||
|
||||
auto widgetSize = imgui::widget_size_with_row_get(4);
|
||||
|
||||
imgui::shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add_open();
|
||||
imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_SOUND_ADD), settings.shortcutAdd);
|
||||
|
||||
if (dialog.is_selected(Dialog::SOUND_OPEN))
|
||||
{
|
||||
add(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
imgui::shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), widgetSize)) remove_unused();
|
||||
imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SOUNDS), settings.shortcutRemove);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
if (ImGui::Button(localize.get(BASIC_RELOAD), widgetSize)) reload();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SOUNDS));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
if (ImGui::Button(localize.get(BASIC_REPLACE), widgetSize)) replace_open();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SOUND));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (dialog.is_selected(Dialog::SOUND_REPLACE))
|
||||
{
|
||||
replace(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "dialog.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Sounds
|
||||
{
|
||||
int newSoundId{-1};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Dialog&, Clipboard&);
|
||||
};
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "actions.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
#include "imgui_internal.h"
|
||||
#include "math_.hpp"
|
||||
#include "math.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "tool.hpp"
|
||||
#include "types.hpp"
|
||||
@@ -20,12 +22,17 @@ namespace anm2ed::imgui
|
||||
{
|
||||
SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) {}
|
||||
|
||||
bool SpritesheetEditor::is_focused_get() const { return isFocused; }
|
||||
|
||||
void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources)
|
||||
{
|
||||
isFocused = false;
|
||||
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.reference;
|
||||
auto& referenceSpritesheet = document.spritesheet.reference;
|
||||
auto referenceItemType = static_cast<ItemType>(reference.itemType);
|
||||
auto& pan = document.editorPan;
|
||||
auto& zoom = document.editorZoom;
|
||||
auto& backgroundColor = settings.editorBackgroundColor;
|
||||
@@ -38,7 +45,8 @@ namespace anm2ed::imgui
|
||||
auto& zoomStep = settings.inputZoomStep;
|
||||
auto& isBorder = settings.editorIsBorder;
|
||||
auto& isTransparent = settings.editorIsTransparent;
|
||||
auto spritesheet = document.spritesheet_get();
|
||||
auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, referenceSpritesheet);
|
||||
auto texture = document.texture_get(referenceSpritesheet);
|
||||
auto& tool = settings.tool;
|
||||
auto& shaderGrid = resources.shaders[shader::GRID];
|
||||
auto& shaderTexture = resources.shaders[shader::TEXTURE];
|
||||
@@ -79,8 +87,8 @@ namespace anm2ed::imgui
|
||||
|
||||
auto fit_view = [&]()
|
||||
{
|
||||
if (spritesheet && spritesheet->texture.is_valid())
|
||||
set_to_rect(zoom, pan, {0, 0, (float)spritesheet->texture.size.x, (float)spritesheet->texture.size.y});
|
||||
if (texture && texture->is_valid())
|
||||
set_to_rect(zoom, pan, {0, 0, (float)texture->size.x, (float)texture->size.y});
|
||||
};
|
||||
|
||||
auto zoom_adjust = [&](float delta)
|
||||
@@ -94,8 +102,12 @@ namespace anm2ed::imgui
|
||||
auto zoom_in = [&]() { zoom_adjust(zoomStep); };
|
||||
auto zoom_out = [&]() { zoom_adjust(-zoomStep); };
|
||||
|
||||
auto region_get = [&](int id)
|
||||
{ return spritesheet ? element_child_id_get(*spritesheet, ElementType::REGION, id) : nullptr; };
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_SPRITESHEET_EDITOR_WINDOW), &settings.windowIsSpritesheetEditor))
|
||||
{
|
||||
isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
||||
|
||||
auto childSize = ImVec2(imgui::row_widget_width_get(3),
|
||||
(ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2));
|
||||
@@ -196,18 +208,17 @@ namespace anm2ed::imgui
|
||||
viewport_set();
|
||||
clear(isTransparent ? vec4(0) : vec4(backgroundColor, 1.0f));
|
||||
|
||||
auto frame = document.frame_get();
|
||||
auto item = document.item_get();
|
||||
auto frame =
|
||||
anm2.element_get(reference.animationIndex, referenceItemType, reference.frameIndex, reference.itemID);
|
||||
|
||||
if (spritesheet && spritesheet->texture.is_valid())
|
||||
if (spritesheet && texture && texture->is_valid())
|
||||
{
|
||||
auto& texture = spritesheet->texture;
|
||||
auto transform = transform_get(zoom, pan);
|
||||
|
||||
auto spritesheetModel = math::quad_model_get(texture.size);
|
||||
auto spritesheetModel = math::quad_model_get(texture->size);
|
||||
auto spritesheetTransform = transform * spritesheetModel;
|
||||
|
||||
texture_render(shaderTexture, texture.id, spritesheetTransform);
|
||||
texture_render(shaderTexture, texture->id, spritesheetTransform);
|
||||
|
||||
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
|
||||
|
||||
@@ -217,32 +228,29 @@ namespace anm2ed::imgui
|
||||
|
||||
if (hoveredRegionId != -1)
|
||||
{
|
||||
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
if (auto region = region_get(hoveredRegionId))
|
||||
{
|
||||
auto& region = regionIt->second;
|
||||
auto cropModel = math::quad_model_get(region.size, region.crop);
|
||||
auto cropModel = math::quad_model_get(region->size, region->crop);
|
||||
auto cropTransform = transform * cropModel;
|
||||
rect_fill_render(shaderLine, cropTransform, cropModel, vec4(1.0f, 1.0f, 1.0f, 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
auto layerIt = anm2.content.layers.find(reference.itemID);
|
||||
auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID);
|
||||
bool isReferenceLayerOnSpritesheet =
|
||||
frame && reference.itemID > -1 && layerIt != anm2.content.layers.end() &&
|
||||
layerIt->second.spritesheetID == referenceSpritesheet;
|
||||
frame && reference.itemID > -1 && layer && layer->spritesheetId == referenceSpritesheet;
|
||||
|
||||
int highlightedRegionId = -1;
|
||||
if (isReferenceLayerOnSpritesheet && frame->regionID != -1 && spritesheet->regions.contains(frame->regionID))
|
||||
if (isReferenceLayerOnSpritesheet && frame->regionId != -1 && region_get(frame->regionId))
|
||||
{
|
||||
highlightedRegionId = frame->regionID;
|
||||
highlightedRegionId = frame->regionId;
|
||||
}
|
||||
else if (regionReference != -1 && spritesheet->regions.contains(regionReference))
|
||||
else if (regionReference != -1 && region_get(regionReference))
|
||||
{
|
||||
highlightedRegionId = regionReference;
|
||||
}
|
||||
|
||||
auto draw_region_rect = [&](anm2::Spritesheet::Region& region, vec4 regionColor)
|
||||
auto draw_region_rect = [&](Element& region, vec4 regionColor)
|
||||
{
|
||||
auto cropModel = math::quad_model_get(region.size, region.crop);
|
||||
auto cropTransform = transform * cropModel;
|
||||
@@ -250,8 +258,10 @@ namespace anm2ed::imgui
|
||||
BORDER_DASH_OFFSET);
|
||||
};
|
||||
|
||||
for (auto& [id, region] : spritesheet->regions)
|
||||
for (auto& region : spritesheet->children)
|
||||
{
|
||||
if (region.type != ElementType::REGION) continue;
|
||||
auto id = region.id;
|
||||
if (id == highlightedRegionId) continue;
|
||||
draw_region_rect(region, color::WHITE);
|
||||
|
||||
@@ -262,26 +272,25 @@ namespace anm2ed::imgui
|
||||
|
||||
if (highlightedRegionId != -1)
|
||||
{
|
||||
auto regionIt = spritesheet->regions.find(highlightedRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
if (auto region = region_get(highlightedRegionId))
|
||||
{
|
||||
auto& region = regionIt->second;
|
||||
draw_region_rect(region, color::RED);
|
||||
draw_region_rect(*region, color::RED);
|
||||
|
||||
auto pivotTransform =
|
||||
transform * math::quad_model_get(PIVOT_SIZE, region.crop + region.pivot, PIVOT_SIZE * 0.5f);
|
||||
transform * math::quad_model_get(PIVOT_SIZE, region->crop + region->pivot, PIVOT_SIZE * 0.5f);
|
||||
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
bool isFrameOnSpritesheet = isReferenceLayerOnSpritesheet;
|
||||
if (isFrameOnSpritesheet && frame->regionID == -1)
|
||||
if (isFrameOnSpritesheet && frame->regionId == -1)
|
||||
{
|
||||
auto frameModel = math::quad_model_get(frame->size, frame->crop);
|
||||
auto frameTransform = transform * frameModel;
|
||||
rect_render(shaderLine, frameTransform, frameModel, color::RED);
|
||||
|
||||
auto pivotTransform = transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f);
|
||||
auto pivotTransform =
|
||||
transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f);
|
||||
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR);
|
||||
}
|
||||
}
|
||||
@@ -293,7 +302,7 @@ namespace anm2ed::imgui
|
||||
render_checker_background(drawList, min, max, -size * 0.5f - checkerPan, CHECKER_SIZE);
|
||||
else
|
||||
drawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(vec4(backgroundColor, 1.0f))));
|
||||
ImGui::Image(texture, to_imvec2(size));
|
||||
ImGui::Image(this->texture, to_imvec2(size));
|
||||
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
@@ -330,8 +339,8 @@ namespace anm2ed::imgui
|
||||
auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown;
|
||||
auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased;
|
||||
|
||||
auto isZoomIn = shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL);
|
||||
auto isZoomOut = shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL);
|
||||
auto isZoomIn = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL);
|
||||
auto isZoomOut = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL);
|
||||
|
||||
auto isBegin = isMouseClicked || isKeyJustPressed;
|
||||
auto isDuring = isMouseDown || isKeyDown;
|
||||
@@ -339,11 +348,12 @@ namespace anm2ed::imgui
|
||||
|
||||
auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift);
|
||||
|
||||
auto frame = document.frame_get();
|
||||
auto layerIt = anm2.content.layers.find(reference.itemID);
|
||||
auto frame =
|
||||
anm2.element_get(reference.animationIndex, referenceItemType, reference.frameIndex, reference.itemID);
|
||||
auto item = anm2.element_get(reference.animationIndex, referenceItemType, reference.itemID);
|
||||
auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID);
|
||||
bool isReferenceLayerOnSpritesheet =
|
||||
frame && reference.itemID > -1 && layerIt != anm2.content.layers.end() &&
|
||||
layerIt->second.spritesheetID == referenceSpritesheet;
|
||||
frame && reference.itemID > -1 && layer && layer->spritesheetId == referenceSpritesheet;
|
||||
auto useTool = tool;
|
||||
auto step = isMod ? STEP_FAST : STEP;
|
||||
auto stepX = isGridSnap ? step * gridSize.x : step;
|
||||
@@ -373,31 +383,212 @@ namespace anm2ed::imgui
|
||||
return std::pair{minPoint, maxPoint};
|
||||
};
|
||||
|
||||
auto frame_change_apply = [&](anm2::FrameChange frameChange, anm2::ChangeType changeType = anm2::ADJUST)
|
||||
{ item->frames_change(frameChange, reference.itemType, changeType, frames); };
|
||||
auto clamp_vec2_to_int = [](const vec2& value)
|
||||
{ return vec2(ivec2(value)); };
|
||||
auto clamp_vec2_to_int = [](const vec2& value) { return vec2(ivec2(value)); };
|
||||
auto snapshot_push = [&](StringType messageType)
|
||||
{
|
||||
auto message = std::string(localize.get(messageType));
|
||||
manager.command_push(
|
||||
{manager.selected, [message](Manager&, Document& document) { document.snapshot(message); }});
|
||||
};
|
||||
auto document_change_push = [&](Document::ChangeType changeType)
|
||||
{
|
||||
manager.command_push({manager.selected,
|
||||
[changeType](Manager&, Document& document) { document.anm2_change(changeType); }});
|
||||
};
|
||||
auto frame_change_apply = [&](FrameChange frameChange, ChangeType changeType = ChangeType::ADJUST)
|
||||
{
|
||||
auto queuedReference = reference;
|
||||
auto queuedReferenceItemType = referenceItemType;
|
||||
std::set<int> queuedFrames(frames.begin(), frames.end());
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto item = document.anm2.element_get(queuedReference.animationIndex,
|
||||
queuedReferenceItemType,
|
||||
queuedReference.itemID);
|
||||
if (!item) return;
|
||||
frames_change(*item, frameChange, queuedReferenceItemType, changeType, queuedFrames);
|
||||
}});
|
||||
};
|
||||
auto frame_change_from_current_apply = [&](auto frameChangeGet, ChangeType changeType = ChangeType::ADJUST)
|
||||
{
|
||||
auto queuedReference = reference;
|
||||
auto queuedReferenceItemType = referenceItemType;
|
||||
std::set<int> queuedFrames(frames.begin(), frames.end());
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto item = document.anm2.element_get(queuedReference.animationIndex,
|
||||
queuedReferenceItemType,
|
||||
queuedReference.itemID);
|
||||
auto frame = document.anm2.element_get(queuedReference.animationIndex,
|
||||
queuedReferenceItemType,
|
||||
queuedReference.frameIndex,
|
||||
queuedReference.itemID);
|
||||
if (!item || !frame) return;
|
||||
frames_change(*item, frameChangeGet(*frame), queuedReferenceItemType, changeType,
|
||||
queuedFrames);
|
||||
}});
|
||||
};
|
||||
auto frame_crop_normalize_apply = [&](bool isSnap, ivec2 snapGridSize, ivec2 snapGridOffset)
|
||||
{
|
||||
frame_change_from_current_apply(
|
||||
[=](const Element& frame)
|
||||
{
|
||||
auto minPoint = glm::min(frame.crop, frame.crop + frame.size);
|
||||
auto maxPoint = glm::max(frame.crop, frame.crop + frame.size);
|
||||
|
||||
if (isSnap)
|
||||
{
|
||||
if (snapGridSize.x != 0)
|
||||
{
|
||||
auto offsetX = (float)snapGridOffset.x;
|
||||
auto sizeX = (float)snapGridSize.x;
|
||||
minPoint.x = std::floor((minPoint.x - offsetX) / sizeX) * sizeX + offsetX;
|
||||
maxPoint.x = std::ceil((maxPoint.x - offsetX) / sizeX) * sizeX + offsetX;
|
||||
}
|
||||
if (snapGridSize.y != 0)
|
||||
{
|
||||
auto offsetY = (float)snapGridOffset.y;
|
||||
auto sizeY = (float)snapGridSize.y;
|
||||
minPoint.y = std::floor((minPoint.y - offsetY) / sizeY) * sizeY + offsetY;
|
||||
maxPoint.y = std::ceil((maxPoint.y - offsetY) / sizeY) * sizeY + offsetY;
|
||||
}
|
||||
}
|
||||
|
||||
return FrameChange{.cropX = minPoint.x,
|
||||
.cropY = minPoint.y,
|
||||
.sizeX = maxPoint.x - minPoint.x,
|
||||
.sizeY = maxPoint.y - minPoint.y};
|
||||
});
|
||||
};
|
||||
auto region_update = [&](int id, auto update)
|
||||
{
|
||||
auto queuedSpritesheet = referenceSpritesheet;
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET,
|
||||
queuedSpritesheet);
|
||||
if (!spritesheet) return;
|
||||
auto region = element_child_id_get(*spritesheet, ElementType::REGION, id);
|
||||
if (!region) return;
|
||||
update(*region);
|
||||
}});
|
||||
};
|
||||
auto region_update_all = [&](auto update)
|
||||
{
|
||||
auto queuedSpritesheet = referenceSpritesheet;
|
||||
std::set<int> queuedSelection(regionSelection.begin(), regionSelection.end());
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto spritesheet = document.anm2.element_get(ElementType::SPRITESHEET,
|
||||
queuedSpritesheet);
|
||||
if (!spritesheet) return;
|
||||
for (auto id : queuedSelection)
|
||||
{
|
||||
auto region = element_child_id_get(*spritesheet, ElementType::REGION, id);
|
||||
if (!region) continue;
|
||||
update(*region);
|
||||
}
|
||||
}});
|
||||
};
|
||||
auto region_set_all = [&](const vec2& crop, const vec2& size)
|
||||
{
|
||||
if (!spritesheet) return;
|
||||
for (auto id : regionSelection)
|
||||
{
|
||||
auto it = spritesheet->regions.find(id);
|
||||
if (it == spritesheet->regions.end()) continue;
|
||||
it->second.crop = clamp_vec2_to_int(crop);
|
||||
it->second.size = clamp_vec2_to_int(size);
|
||||
}
|
||||
auto queuedCrop = clamp_vec2_to_int(crop);
|
||||
auto queuedSize = clamp_vec2_to_int(size);
|
||||
region_update_all(
|
||||
[=](Element& region)
|
||||
{
|
||||
region.crop = queuedCrop;
|
||||
region.size = queuedSize;
|
||||
});
|
||||
};
|
||||
auto region_offset_all = [&](const vec2& delta)
|
||||
{
|
||||
if (!spritesheet) return;
|
||||
for (auto id : regionSelection)
|
||||
{
|
||||
auto it = spritesheet->regions.find(id);
|
||||
if (it == spritesheet->regions.end()) continue;
|
||||
it->second.crop = clamp_vec2_to_int(it->second.crop + delta);
|
||||
it->second.size = clamp_vec2_to_int(it->second.size);
|
||||
}
|
||||
region_update_all(
|
||||
[=](Element& region)
|
||||
{
|
||||
region.crop = clamp_vec2_to_int(region.crop + delta);
|
||||
region.size = clamp_vec2_to_int(region.size);
|
||||
});
|
||||
};
|
||||
auto region_crop_normalize_all = [&](bool isSnap, ivec2 snapGridSize, ivec2 snapGridOffset)
|
||||
{
|
||||
region_update_all(
|
||||
[=](Element& region)
|
||||
{
|
||||
auto minPoint = glm::min(region.crop, region.crop + region.size);
|
||||
auto maxPoint = glm::max(region.crop, region.crop + region.size);
|
||||
|
||||
if (isSnap)
|
||||
{
|
||||
if (snapGridSize.x != 0)
|
||||
{
|
||||
auto offsetX = (float)snapGridOffset.x;
|
||||
auto sizeX = (float)snapGridSize.x;
|
||||
minPoint.x = std::floor((minPoint.x - offsetX) / sizeX) * sizeX + offsetX;
|
||||
maxPoint.x = std::ceil((maxPoint.x - offsetX) / sizeX) * sizeX + offsetX;
|
||||
}
|
||||
if (snapGridSize.y != 0)
|
||||
{
|
||||
auto offsetY = (float)snapGridOffset.y;
|
||||
auto sizeY = (float)snapGridSize.y;
|
||||
minPoint.y = std::floor((minPoint.y - offsetY) / sizeY) * sizeY + offsetY;
|
||||
maxPoint.y = std::ceil((maxPoint.y - offsetY) / sizeY) * sizeY + offsetY;
|
||||
}
|
||||
}
|
||||
|
||||
region.crop = clamp_vec2_to_int(minPoint);
|
||||
region.size = clamp_vec2_to_int(maxPoint - minPoint);
|
||||
});
|
||||
};
|
||||
auto texture_line_apply = [&](ivec2 start, ivec2 end, vec4 color)
|
||||
{
|
||||
auto queuedSpritesheet = referenceSpritesheet;
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto texture = document.texture_get(queuedSpritesheet);
|
||||
if (!texture) return;
|
||||
texture->pixel_line(start, end, color);
|
||||
}});
|
||||
};
|
||||
auto texture_change_push = [&]()
|
||||
{
|
||||
auto queuedSpritesheet = referenceSpritesheet;
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document) { document.texture_change(queuedSpritesheet); }});
|
||||
};
|
||||
|
||||
auto region_selection_set = [&](int id)
|
||||
{
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
document.region.reference = id;
|
||||
document.region.selection = {id};
|
||||
}});
|
||||
};
|
||||
auto region_pivot_set = [&](int id, vec2 pivot)
|
||||
{
|
||||
auto queuedPivot = clamp_vec2_to_int(pivot);
|
||||
region_update(id,
|
||||
[=](Element& region)
|
||||
{
|
||||
region.origin = Origin::CUSTOM;
|
||||
region.pivot = queuedPivot;
|
||||
});
|
||||
};
|
||||
auto region_pivot_offset = [&](int id, vec2 delta)
|
||||
{
|
||||
region_update(id,
|
||||
[=](Element& region)
|
||||
{
|
||||
region.origin = Origin::CUSTOM;
|
||||
region.pivot = clamp_vec2_to_int(region.pivot + delta);
|
||||
});
|
||||
};
|
||||
|
||||
if (isMouseMiddleDown) useTool = tool::PAN;
|
||||
@@ -408,16 +599,17 @@ namespace anm2ed::imgui
|
||||
|
||||
hoveredRegionId = -1;
|
||||
|
||||
if (useTool == tool::PAN && spritesheet && spritesheet->texture.is_valid() && isMouseOverCanvas)
|
||||
if (useTool == tool::PAN && spritesheet && texture && texture->is_valid() && isMouseOverCanvas)
|
||||
{
|
||||
for (auto& [id, region] : spritesheet->regions)
|
||||
for (auto& region : spritesheet->children)
|
||||
{
|
||||
if (region.type != ElementType::REGION) continue;
|
||||
auto minPoint = glm::min(region.crop, region.crop + region.size);
|
||||
auto maxPoint = glm::max(region.crop, region.crop + region.size);
|
||||
if (hoverMousePos.x >= minPoint.x && hoverMousePos.x <= maxPoint.x && hoverMousePos.y >= minPoint.y &&
|
||||
hoverMousePos.y <= maxPoint.y)
|
||||
{
|
||||
hoveredRegionId = id;
|
||||
hoveredRegionId = region.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -428,12 +620,12 @@ namespace anm2ed::imgui
|
||||
bool isAreaAllowed = areaType == tool::ALL || areaType == tool::SPRITESHEET_EDITOR;
|
||||
bool isFrameRequired =
|
||||
!(useTool == tool::PAN || useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER);
|
||||
bool isRegionInUse = frame && frame->regionID != -1 && (useTool == tool::CROP || useTool == tool::MOVE);
|
||||
bool isRegionInUse = frame && frame->regionId != -1 && (useTool == tool::CROP || useTool == tool::MOVE);
|
||||
bool isFrameAvailable = !isFrameRequired || (frame && !isRegionInUse) ||
|
||||
(useTool == tool::CROP && !frame && !regionSelection.empty()) ||
|
||||
(useTool == tool::MOVE && !frame && regionReference != -1);
|
||||
bool isSpritesheetRequired = useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER;
|
||||
bool isSpritesheetAvailable = !isSpritesheetRequired || (spritesheet && spritesheet->texture.is_valid());
|
||||
bool isSpritesheetAvailable = !isSpritesheetRequired || (texture && texture->is_valid());
|
||||
auto cursor = (isAreaAllowed && isFrameAvailable && isSpritesheetAvailable) ? toolInfo.cursor
|
||||
: ImGuiMouseCursor_NotAllowed;
|
||||
ImGui::SetMouseCursor(cursor);
|
||||
@@ -444,29 +636,23 @@ namespace anm2ed::imgui
|
||||
case tool::PAN:
|
||||
if (isMouseLeftClicked && hoveredRegionId != -1)
|
||||
{
|
||||
regionReference = hoveredRegionId;
|
||||
regionSelection = {hoveredRegionId};
|
||||
region_selection_set(hoveredRegionId);
|
||||
if (isReferenceLayerOnSpritesheet)
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_FRAME_REGION), Document::FRAMES,
|
||||
{
|
||||
anm2::FrameChange change{};
|
||||
change.regionID = hoveredRegionId;
|
||||
if (spritesheet)
|
||||
{
|
||||
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
{
|
||||
change.cropX = regionIt->second.crop.x;
|
||||
change.cropY = regionIt->second.crop.y;
|
||||
change.sizeX = regionIt->second.size.x;
|
||||
change.sizeY = regionIt->second.size.y;
|
||||
change.pivotX = regionIt->second.pivot.x;
|
||||
change.pivotY = regionIt->second.pivot.y;
|
||||
}
|
||||
}
|
||||
frame_change_apply(change);
|
||||
});
|
||||
snapshot_push(EDIT_FRAME_REGION);
|
||||
FrameChange change{};
|
||||
change.regionId = hoveredRegionId;
|
||||
if (auto region = region_get(hoveredRegionId))
|
||||
{
|
||||
change.cropX = region->crop.x;
|
||||
change.cropY = region->crop.y;
|
||||
change.sizeX = region->size.x;
|
||||
change.sizeY = region->size.y;
|
||||
change.pivotX = region->pivot.x;
|
||||
change.pivotY = region->pivot.y;
|
||||
}
|
||||
frame_change_apply(change);
|
||||
document_change_push(Document::FRAMES);
|
||||
}
|
||||
}
|
||||
if (isMouseDown || isMouseMiddleDown) pan += mouseDelta;
|
||||
@@ -476,48 +662,48 @@ namespace anm2ed::imgui
|
||||
if (!frame && regionReference != -1)
|
||||
{
|
||||
if (!spritesheet) break;
|
||||
auto regionIt = spritesheet->regions.find(regionReference);
|
||||
if (regionIt == spritesheet->regions.end()) break;
|
||||
auto region = region_get(regionReference);
|
||||
if (!region) break;
|
||||
|
||||
auto& region = regionIt->second;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_REGION_MOVE));
|
||||
bool isPivotEdited = isMouseDown || isLeftPressed || isRightPressed || isUpPressed || isDownPressed;
|
||||
if (isPivotEdited) region.origin = anm2::Spritesheet::Region::CUSTOM;
|
||||
if (isMouseDown) region.pivot = ivec2(mousePos) - ivec2(region.crop);
|
||||
if (isLeftPressed) region.pivot.x -= step;
|
||||
if (isRightPressed) region.pivot.x += step;
|
||||
if (isUpPressed) region.pivot.y -= step;
|
||||
if (isDownPressed) region.pivot.y += step;
|
||||
if (isBegin) snapshot_push(EDIT_REGION_MOVE);
|
||||
if (isMouseDown) region_pivot_set(regionReference, ivec2(mousePos) - ivec2(region->crop));
|
||||
if (isLeftPressed) region_pivot_offset(regionReference, vec2(-step, 0));
|
||||
if (isRightPressed) region_pivot_offset(regionReference, vec2(step, 0));
|
||||
if (isUpPressed) region_pivot_offset(regionReference, vec2(0, -step));
|
||||
if (isDownPressed) region_pivot_offset(regionReference, vec2(0, step));
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y))
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region->pivot.x, region->pivot.y))
|
||||
.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnd) document.change(Document::SPRITESHEETS);
|
||||
if (isEnd) document_change_push(Document::SPRITESHEETS);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!item || frames.empty()) break;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_FRAME_PIVOT));
|
||||
if (isBegin) snapshot_push(EDIT_FRAME_PIVOT);
|
||||
if (isMouseDown)
|
||||
{
|
||||
frame->crop = ivec2(frame->crop);
|
||||
frame_change_apply(
|
||||
{.pivotX = (int)(mousePos.x - frame->crop.x), .pivotY = (int)(mousePos.y - frame->crop.y)});
|
||||
auto frameCrop = ivec2(frame->crop);
|
||||
frame_change_apply({.pivotX = (int)(mousePos.x - frameCrop.x),
|
||||
.pivotY = (int)(mousePos.y - frameCrop.y),
|
||||
.cropX = frameCrop.x,
|
||||
.cropY = frameCrop.y});
|
||||
}
|
||||
if (isLeftPressed) frame_change_apply({.pivotX = step}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.pivotX = step}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.pivotY = step}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.pivotY = step}, anm2::ADD);
|
||||
if (isLeftPressed) frame_change_apply({.pivotX = step}, ChangeType::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.pivotX = step}, ChangeType::ADD);
|
||||
if (isUpPressed) frame_change_apply({.pivotY = step}, ChangeType::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.pivotY = step}, ChangeType::ADD);
|
||||
|
||||
frame_change_apply({.pivotX = (int)frame->pivot.x, .pivotY = (int)frame->pivot.y});
|
||||
frame_change_from_current_apply(
|
||||
[](const Element& frame) { return FrameChange{.pivotX = (int)frame.pivot.x, .pivotY = (int)frame.pivot.y}; });
|
||||
|
||||
if (isDuring)
|
||||
{
|
||||
@@ -530,14 +716,14 @@ namespace anm2ed::imgui
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnd) document.change(Document::FRAMES);
|
||||
if (isEnd) document_change_push(Document::FRAMES);
|
||||
break;
|
||||
case tool::CROP:
|
||||
if (isRegionInUse) break;
|
||||
if (frames.empty())
|
||||
{
|
||||
if (!spritesheet || regionSelection.empty()) break;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_REGION_CROP));
|
||||
if (isBegin) snapshot_push(EDIT_REGION_CROP);
|
||||
|
||||
if (isMouseClicked)
|
||||
{
|
||||
@@ -557,49 +743,29 @@ namespace anm2ed::imgui
|
||||
if (isDuring)
|
||||
{
|
||||
if (!isMouseDown)
|
||||
{
|
||||
for (auto id : regionSelection)
|
||||
{
|
||||
auto it = spritesheet->regions.find(id);
|
||||
if (it == spritesheet->regions.end()) continue;
|
||||
|
||||
auto& region = it->second;
|
||||
auto minPoint = glm::min(region.crop, region.crop + region.size);
|
||||
auto maxPoint = glm::max(region.crop, region.crop + region.size);
|
||||
region.crop = clamp_vec2_to_int(minPoint);
|
||||
region.size = clamp_vec2_to_int(maxPoint - minPoint);
|
||||
|
||||
if (isGridSnap)
|
||||
{
|
||||
auto [snapMin, snapMax] = snap_rect(region.crop, region.crop + region.size);
|
||||
region.crop = clamp_vec2_to_int(snapMin);
|
||||
region.size = clamp_vec2_to_int(snapMax - snapMin);
|
||||
}
|
||||
}
|
||||
}
|
||||
region_crop_normalize_all(isGridSnap, gridSize, gridOffset);
|
||||
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
auto it = spritesheet->regions.find(*regionSelection.begin());
|
||||
if (it != spritesheet->regions.end())
|
||||
if (auto region = region_get(*regionSelection.begin()))
|
||||
{
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_CROP),
|
||||
std::make_format_args(it->second.crop.x, it->second.crop.y))
|
||||
.c_str());
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_SIZE),
|
||||
std::make_format_args(it->second.size.x, it->second.size.y))
|
||||
.c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region->crop.x, region->crop.y))
|
||||
.c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region->size.x, region->size.y))
|
||||
.c_str());
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnd) document.change(Document::SPRITESHEETS);
|
||||
if (isEnd) document_change_push(Document::SPRITESHEETS);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!item) break;
|
||||
if (isBegin) document.snapshot(localize.get(EDIT_FRAME_CROP));
|
||||
if (isBegin) snapshot_push(EDIT_FRAME_CROP);
|
||||
|
||||
if (isMouseClicked)
|
||||
{
|
||||
@@ -614,10 +780,10 @@ namespace anm2ed::imgui
|
||||
.sizeX = maxPoint.x - minPoint.x,
|
||||
.sizeY = maxPoint.y - minPoint.y});
|
||||
}
|
||||
if (isLeftPressed) frame_change_apply({.cropX = stepX}, anm2::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.cropX = stepX}, anm2::ADD);
|
||||
if (isUpPressed) frame_change_apply({.cropY = stepY}, anm2::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.cropY = stepY}, anm2::ADD);
|
||||
if (isLeftPressed) frame_change_apply({.cropX = stepX}, ChangeType::SUBTRACT);
|
||||
if (isRightPressed) frame_change_apply({.cropX = stepX}, ChangeType::ADD);
|
||||
if (isUpPressed) frame_change_apply({.cropY = stepY}, ChangeType::SUBTRACT);
|
||||
if (isDownPressed) frame_change_apply({.cropY = stepY}, ChangeType::ADD);
|
||||
|
||||
frame_change_apply(
|
||||
{.cropX = frame->crop.x, .cropY = frame->crop.y, .sizeX = frame->size.x, .sizeY = frame->size.y});
|
||||
@@ -626,23 +792,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
if (!isMouseDown)
|
||||
{
|
||||
auto minPoint = glm::min(frame->crop, frame->crop + frame->size);
|
||||
auto maxPoint = glm::max(frame->crop, frame->crop + frame->size);
|
||||
|
||||
frame_change_apply({.cropX = minPoint.x,
|
||||
.cropY = minPoint.y,
|
||||
.sizeX = maxPoint.x - minPoint.x,
|
||||
.sizeY = maxPoint.y - minPoint.y});
|
||||
|
||||
if (isGridSnap)
|
||||
{
|
||||
auto [snapMin, snapMax] = snap_rect(frame->crop, frame->crop + frame->size);
|
||||
|
||||
frame_change_apply({.cropX = snapMin.x,
|
||||
.cropY = snapMin.y,
|
||||
.sizeX = snapMax.x - snapMin.x,
|
||||
.sizeY = snapMax.y - snapMin.y});
|
||||
}
|
||||
frame_crop_normalize_apply(isGridSnap, gridSize, gridOffset);
|
||||
}
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
@@ -655,27 +805,24 @@ namespace anm2ed::imgui
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
if (isEnd) document.change(Document::FRAMES);
|
||||
if (isEnd) document_change_push(Document::FRAMES);
|
||||
break;
|
||||
case tool::DRAW:
|
||||
case tool::ERASE:
|
||||
{
|
||||
if (!spritesheet) break;
|
||||
if (!texture) break;
|
||||
auto color = useTool == tool::DRAW ? toolColor : vec4();
|
||||
if (isMouseClicked)
|
||||
document.snapshot(useTool == tool::DRAW ? localize.get(EDIT_DRAW) : localize.get(EDIT_ERASE));
|
||||
if (isMouseDown) spritesheet->texture.pixel_line(ivec2(previousMousePos), ivec2(mousePos), color);
|
||||
if (isMouseReleased)
|
||||
{
|
||||
document.change(Document::SPRITESHEETS);
|
||||
}
|
||||
snapshot_push(useTool == tool::DRAW ? EDIT_DRAW : EDIT_ERASE);
|
||||
if (isMouseDown) texture_line_apply(ivec2(previousMousePos), ivec2(mousePos), color);
|
||||
if (isMouseReleased) texture_change_push();
|
||||
break;
|
||||
}
|
||||
case tool::COLOR_PICKER:
|
||||
{
|
||||
if (spritesheet && isDuring)
|
||||
if (texture && isDuring)
|
||||
{
|
||||
toolColor = spritesheet->texture.pixel_read(mousePos);
|
||||
toolColor = texture->pixel_read(mousePos);
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
ImGui::ColorButton("##Color Picker Button", to_imvec4(toolColor));
|
||||
@@ -696,31 +843,31 @@ namespace anm2ed::imgui
|
||||
|
||||
if (tool == tool::PAN && hoveredRegionId != -1 && spritesheet)
|
||||
{
|
||||
auto regionIt = spritesheet->regions.find(hoveredRegionId);
|
||||
if (regionIt != spritesheet->regions.end())
|
||||
if (auto region = region_get(hoveredRegionId))
|
||||
{
|
||||
if (ImGui::BeginTooltip())
|
||||
{
|
||||
auto& region = regionIt->second;
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(region.name.c_str());
|
||||
ImGui::TextUnformatted(region->name.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_ID), std::make_format_args(hoveredRegionId)).c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region.crop.x, region.crop.y)).c_str());
|
||||
std::vformat(localize.get(FORMAT_CROP), std::make_format_args(region->crop.x, region->crop.y))
|
||||
.c_str());
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region.size.x, region.size.y)).c_str());
|
||||
if (region.origin == anm2::Spritesheet::Region::CUSTOM)
|
||||
std::vformat(localize.get(FORMAT_SIZE), std::make_format_args(region->size.x, region->size.y))
|
||||
.c_str());
|
||||
if (region->origin == Origin::CUSTOM)
|
||||
{
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region.pivot.x, region.pivot.y))
|
||||
std::vformat(localize.get(FORMAT_PIVOT), std::make_format_args(region->pivot.x, region->pivot.y))
|
||||
.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
StringType originString = LABEL_REGION_ORIGIN_CENTER;
|
||||
if (region.origin == anm2::Spritesheet::Region::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT;
|
||||
if (region->origin == Origin::TOP_LEFT) originString = LABEL_REGION_ORIGIN_TOP_LEFT;
|
||||
auto originLabel = localize.get(originString);
|
||||
ImGui::TextUnformatted(
|
||||
std::vformat(localize.get(FORMAT_ORIGIN), std::make_format_args(originLabel)).c_str());
|
||||
@@ -766,8 +913,7 @@ namespace anm2ed::imgui
|
||||
if (mouseWheel != 0 || isZoomIn || isZoomOut)
|
||||
{
|
||||
auto focus = mouseWheel != 0 ? vec2(mousePos) : vec2();
|
||||
if (auto spritesheet = document.spritesheet_get(); spritesheet && mouseWheel == 0)
|
||||
focus = spritesheet->texture.size / 2;
|
||||
if (texture && mouseWheel == 0) focus = texture->size / 2;
|
||||
|
||||
auto previousZoom = zoom;
|
||||
zoom_set(zoom, pan, focus, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep);
|
||||
@@ -776,31 +922,17 @@ namespace anm2ed::imgui
|
||||
}
|
||||
}
|
||||
|
||||
if (tool == tool::PAN &&
|
||||
ImGui::BeginPopupContextWindow("##Spritesheet Editor Context Menu", ImGuiMouseButton_Right))
|
||||
if (tool == tool::PAN)
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_CENTER_VIEW), settings.shortcutCenterView.c_str())) center_view();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(LABEL_FIT), settings.shortcutFit.c_str(), false,
|
||||
spritesheet && spritesheet->texture.is_valid()))
|
||||
fit_view();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_IN), settings.shortcutZoomIn.c_str())) zoom_in();
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_ZOOM_OUT), settings.shortcutZoomOut.c_str())) zoom_out();
|
||||
|
||||
ImGui::EndPopup();
|
||||
Actions actions{};
|
||||
actions_undo_redo_add(actions, manager, document);
|
||||
actions.separator();
|
||||
actions.add(ACTION_CENTER_VIEW, []() { return true; }, center_view);
|
||||
actions.add(ACTION_FIT_VIEW, [&]() { return texture && texture->is_valid(); }, fit_view);
|
||||
actions.separator();
|
||||
actions.add(ACTION_ZOOM_IN, []() { return true; }, zoom_in);
|
||||
actions.add(ACTION_ZOOM_OUT, []() { return true; }, zoom_out);
|
||||
actions_context_window_draw("##Spritesheet Editor Context Menu", actions, settings);
|
||||
}
|
||||
|
||||
if (!document.isSpritesheetEditorSet)
|
||||
|
||||
@@ -17,10 +17,12 @@ namespace anm2ed::imgui
|
||||
float checkerSyncZoom{};
|
||||
bool isCheckerPanInitialized{};
|
||||
bool hasPendingZoomPanAdjust{};
|
||||
bool isFocused{};
|
||||
int hoveredRegionId{-1};
|
||||
|
||||
public:
|
||||
SpritesheetEditor();
|
||||
bool is_focused_get() const;
|
||||
void update(Manager&, Settings&, Resources&);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,709 +0,0 @@
|
||||
#include "spritesheets.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
|
||||
#include "document.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "working_directory.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
static constexpr auto PADDING_MAX = 100;
|
||||
|
||||
void Spritesheets::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog,
|
||||
Clipboard& clipboard)
|
||||
{
|
||||
auto& document = *manager.get();
|
||||
auto& anm2 = document.anm2;
|
||||
auto& selection = document.spritesheet.selection;
|
||||
auto& reference = document.spritesheet.reference;
|
||||
auto& region = document.region;
|
||||
auto style = ImGui::GetStyle();
|
||||
std::function<void()> pack{};
|
||||
|
||||
auto add_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_OPEN); };
|
||||
auto replace_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_REPLACE); };
|
||||
auto set_file_path_open = [&]() { dialog.file_save(Dialog::SPRITESHEET_PATH_SET); };
|
||||
auto merge_open = [&]()
|
||||
{
|
||||
if (selection.size() <= 1) return;
|
||||
mergeSelection = selection;
|
||||
mergePopup.open();
|
||||
};
|
||||
auto pack_open = [&]()
|
||||
{
|
||||
if (selection.size() != 1) return;
|
||||
auto id = *selection.begin();
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
if (anm2.content.spritesheets.at(id).regions.empty()) return;
|
||||
packId = id;
|
||||
packPopup.open();
|
||||
};
|
||||
|
||||
auto add = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (path.empty()) return;
|
||||
document.spritesheet_add(path);
|
||||
newSpritesheetId = document.spritesheet.reference;
|
||||
};
|
||||
|
||||
auto remove_unused = [&]()
|
||||
{
|
||||
auto unused = anm2.spritesheets_unused();
|
||||
if (unused.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : unused)
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
anm2.content.spritesheets.erase(id);
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REMOVE_UNUSED_SPRITESHEETS), Document::ALL, behavior());
|
||||
};
|
||||
|
||||
auto reload = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto& id : selection)
|
||||
{
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
spritesheet.reload(document.directory_get());
|
||||
document.spritesheet_hash_set_saved(id);
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_RELOAD_SPRITESHEETS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto replace = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (selection.size() != 1 || path.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto& id = *selection.begin();
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
spritesheet.reload(document.directory_get(), path);
|
||||
document.spritesheet_hash_set_saved(id);
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
toasts.push(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_REPLACE_SPRITESHEET), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto set_file_path = [&](const std::filesystem::path& path)
|
||||
{
|
||||
if (selection.size() != 1 || path.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto id = *selection.begin();
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
WorkingDirectory workingDirectory(document.directory_get());
|
||||
anm2.content.spritesheets[id].path = path::make_relative(path);
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_SET_SPRITESHEET_FILE_PATH), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto save = [&](const std::set<int>& ids)
|
||||
{
|
||||
if (ids.empty()) return;
|
||||
|
||||
for (auto& id : ids)
|
||||
{
|
||||
if (!anm2.content.spritesheets.contains(id)) continue;
|
||||
anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
if (spritesheet.save(document.directory_get()))
|
||||
{
|
||||
document.spritesheet_hash_set_saved(id);
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString)));
|
||||
logger.info(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED), std::make_format_args(id, pathString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(id, pathString)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto save_open = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
if (settings.fileIsWarnOverwrite)
|
||||
{
|
||||
saveSelection = selection;
|
||||
overwritePopup.open();
|
||||
}
|
||||
else
|
||||
{
|
||||
save(selection);
|
||||
}
|
||||
};
|
||||
|
||||
auto merge = [&]()
|
||||
{
|
||||
if (mergeSelection.size() <= 1) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto baseID = *mergeSelection.begin();
|
||||
if (anm2.spritesheets_merge(mergeSelection, (anm2::SpritesheetMergeOrigin)settings.mergeSpritesheetsOrigin,
|
||||
settings.mergeSpritesheetsIsMakeRegions,
|
||||
settings.mergeSpritesheetsIsMakePrimaryRegion,
|
||||
(origin::Type)settings.mergeSpritesheetsRegionOrigin))
|
||||
{
|
||||
selection = {baseID};
|
||||
reference = baseID;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS));
|
||||
logger.info(localize.get(TOAST_MERGE_SPRITESHEETS, anm2ed::ENGLISH));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED));
|
||||
logger.error(localize.get(TOAST_MERGE_SPRITESHEETS_FAILED, anm2ed::ENGLISH));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_MERGE_SPRITESHEETS), Document::ALL, behavior());
|
||||
};
|
||||
pack = [&]()
|
||||
{
|
||||
int id = packId != -1 ? packId : (selection.size() == 1 ? *selection.begin() : -1);
|
||||
if (id == -1) return;
|
||||
if (!anm2.content.spritesheets.contains(id)) return;
|
||||
if (anm2.content.spritesheets.at(id).regions.empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto padding = std::max(0, settings.packPadding);
|
||||
if (anm2.spritesheet_pack(id, padding))
|
||||
{
|
||||
toasts.push(localize.get(TOAST_PACK_SPRITESHEET));
|
||||
logger.info(localize.get(TOAST_PACK_SPRITESHEET, anm2ed::ENGLISH));
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(localize.get(TOAST_PACK_SPRITESHEET_FAILED));
|
||||
logger.error(localize.get(TOAST_PACK_SPRITESHEET_FAILED, anm2ed::ENGLISH));
|
||||
}
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PACK_SPRITESHEET), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto open_directory = [&](anm2::Spritesheet& spritesheet)
|
||||
{
|
||||
if (spritesheet.path.empty()) return;
|
||||
std::error_code ec{};
|
||||
auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / spritesheet.path, ec);
|
||||
if (ec) absolutePath = document.directory_get() / spritesheet.path;
|
||||
auto target = std::filesystem::is_directory(absolutePath) ? absolutePath
|
||||
: std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path()
|
||||
: document.directory_get();
|
||||
dialog.file_explorer_open(target);
|
||||
};
|
||||
|
||||
auto copy = [&]()
|
||||
{
|
||||
if (selection.empty()) return;
|
||||
|
||||
std::string clipboardText{};
|
||||
for (auto& id : selection)
|
||||
clipboardText += anm2.content.spritesheets[id].to_string(id);
|
||||
clipboard.set(clipboardText);
|
||||
};
|
||||
|
||||
auto paste = [&]()
|
||||
{
|
||||
if (clipboard.is_empty()) return;
|
||||
|
||||
auto behavior = [&]()
|
||||
{
|
||||
auto maxSpritesheetIdBefore =
|
||||
anm2.content.spritesheets.empty() ? -1 : anm2.content.spritesheets.rbegin()->first;
|
||||
std::string errorString{};
|
||||
document.snapshot(localize.get(EDIT_PASTE_SPRITESHEETS));
|
||||
if (anm2.spritesheets_deserialize(clipboard.get(), document.directory_get(), merge::APPEND, &errorString))
|
||||
{
|
||||
if (!anm2.content.spritesheets.empty())
|
||||
{
|
||||
auto maxSpritesheetIdAfter = anm2.content.spritesheets.rbegin()->first;
|
||||
if (maxSpritesheetIdAfter > maxSpritesheetIdBefore)
|
||||
{
|
||||
newSpritesheetId = maxSpritesheetIdAfter;
|
||||
selection = {maxSpritesheetIdAfter};
|
||||
reference = maxSpritesheetIdAfter;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
}
|
||||
}
|
||||
document.change(Document::SPRITESHEETS);
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.push(
|
||||
std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED), std::make_format_args(errorString)));
|
||||
logger.error(std::vformat(localize.get(TOAST_DESERIALIZE_SPRITESHEETS_FAILED, anm2ed::ENGLISH),
|
||||
std::make_format_args(errorString)));
|
||||
};
|
||||
};
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_PASTE_SPRITESHEETS), Document::SPRITESHEETS, behavior());
|
||||
};
|
||||
|
||||
auto context_menu = [&]()
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Right))
|
||||
ImGui::OpenPopup("##Spritesheet Context Menu");
|
||||
|
||||
if (ImGui::BeginPopup("##Spritesheet Context Menu"))
|
||||
{
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_UNDO), settings.shortcutUndo.c_str(), false,
|
||||
document.is_able_to_undo()))
|
||||
document.undo();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(SHORTCUT_STRING_REDO), settings.shortcutRedo.c_str(), false,
|
||||
document.is_able_to_redo()))
|
||||
document.redo();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_OPEN_DIRECTORY), nullptr, false, selection.size() == 1))
|
||||
open_directory(anm2.content.spritesheets[*selection.begin()]);
|
||||
if (ImGui::MenuItem(localize.get(BASIC_SET_FILE_PATH), nullptr, false, selection.size() == 1))
|
||||
set_file_path_open();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_ADD), settings.shortcutAdd.c_str())) add_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REMOVE_UNUSED), settings.shortcutRemove.c_str())) remove_unused();
|
||||
|
||||
bool isPackable = selection.size() == 1 && anm2.content.spritesheets.contains(*selection.begin()) &&
|
||||
!anm2.content.spritesheets.at(*selection.begin()).regions.empty();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_RELOAD), nullptr, false, !selection.empty())) reload();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_REPLACE), nullptr, false, selection.size() == 1)) replace_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_MERGE), settings.shortcutMerge.c_str(), false, selection.size() > 1))
|
||||
merge_open();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PACK), nullptr, false, isPackable)) pack_open();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PACK_SPRITESHEET));
|
||||
if (ImGui::MenuItem(localize.get(BASIC_SAVE), nullptr, false, !selection.empty())) save_open();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(localize.get(BASIC_COPY), settings.shortcutCopy.c_str(), false, !selection.empty())) copy();
|
||||
if (ImGui::MenuItem(localize.get(BASIC_PASTE), settings.shortcutPaste.c_str(), false, !clipboard.is_empty()))
|
||||
paste();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
};
|
||||
|
||||
if (ImGui::Begin(localize.get(LABEL_SPRITESHEETS_WINDOW), &settings.windowIsSpritesheets))
|
||||
{
|
||||
auto childSize = size_without_footer_get(2);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
if (ImGui::BeginChild("##Spritesheets Child", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto spritesheetChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 4);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
|
||||
|
||||
selection.start(anm2.content.spritesheets.size());
|
||||
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused))
|
||||
{
|
||||
selection.clear();
|
||||
for (auto& id : anm2.content.spritesheets | std::views::keys)
|
||||
selection.insert(id);
|
||||
}
|
||||
if (ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) selection.clear();
|
||||
auto scroll_to_item = [&](float itemHeight, bool isTarget)
|
||||
{
|
||||
if (!isTarget) return;
|
||||
auto windowHeight = ImGui::GetWindowHeight();
|
||||
auto targetTop = ImGui::GetCursorPosY();
|
||||
auto targetBottom = targetTop + itemHeight;
|
||||
auto visibleTop = ImGui::GetScrollY();
|
||||
auto visibleBottom = visibleTop + windowHeight;
|
||||
if (targetTop < visibleTop)
|
||||
ImGui::SetScrollY(targetTop);
|
||||
else if (targetBottom > visibleBottom)
|
||||
ImGui::SetScrollY(targetBottom - windowHeight);
|
||||
};
|
||||
int scrollTargetId = -1;
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
(ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) || ImGui::IsKeyPressed(ImGuiKey_DownArrow, true)))
|
||||
{
|
||||
std::vector<int> ids{};
|
||||
ids.reserve(anm2.content.spritesheets.size());
|
||||
for (auto& [id, sheet] : anm2.content.spritesheets)
|
||||
ids.push_back(id);
|
||||
if (!ids.empty())
|
||||
{
|
||||
int delta = ImGui::IsKeyPressed(ImGuiKey_UpArrow, true) ? -1 : 1;
|
||||
int current = reference;
|
||||
if (current == -1 && !selection.empty()) current = *selection.begin();
|
||||
auto it = std::find(ids.begin(), ids.end(), current);
|
||||
int index = it == ids.end() ? 0 : (int)std::distance(ids.begin(), it);
|
||||
index = std::clamp(index + delta, 0, (int)ids.size() - 1);
|
||||
int nextId = ids[index];
|
||||
selection = {nextId};
|
||||
reference = nextId;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
scrollTargetId = nextId;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [id, spritesheet] : anm2.content.spritesheets)
|
||||
{
|
||||
auto isNewSpritesheet = newSpritesheetId == id;
|
||||
ImGui::PushID(id);
|
||||
|
||||
scroll_to_item(spritesheetChildSize.y, scrollTargetId == id);
|
||||
|
||||
if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto isSelected = selection.contains(id);
|
||||
auto isReferenced = id == reference;
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
bool isValid = spritesheet.texture.is_valid();
|
||||
auto& texture = isValid ? spritesheet.texture : resources.icons[icon::NONE];
|
||||
auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
auto pathString = path::to_utf8(spritesheet.path);
|
||||
auto pathCStr = pathString.c_str();
|
||||
|
||||
ImGui::SetNextItemSelectionUserData(id);
|
||||
ImGui::SetNextItemStorageID(id);
|
||||
if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize))
|
||||
{
|
||||
reference = id;
|
||||
region.reference = -1;
|
||||
region.selection.clear();
|
||||
}
|
||||
if (scrollTargetId == id) ImGui::SetItemDefaultFocus();
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
open_directory(spritesheet);
|
||||
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto maxPreviewSize = to_vec2(viewport->Size) * 0.5f;
|
||||
vec2 textureSize = vec2(glm::max(texture.size.x, 1), glm::max(texture.size.y, 1));
|
||||
if (textureSize.x > maxPreviewSize.x || textureSize.y > maxPreviewSize.y)
|
||||
{
|
||||
auto scale = glm::min(maxPreviewSize.x / textureSize.x, maxPreviewSize.y / textureSize.y);
|
||||
textureSize *= scale;
|
||||
}
|
||||
|
||||
auto textWidth = ImGui::CalcTextSize(pathCStr).x;
|
||||
auto tooltipPadding = style.WindowPadding.x * 4.0f;
|
||||
auto minWidth = textureSize.x + style.ItemSpacing.x + textWidth + tooltipPadding;
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(minWidth, 0), ImGuiCond_Appearing);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
auto childFlags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY;
|
||||
auto noScrollFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
if (ImGui::BeginChild("##Spritesheet Tooltip Image Child", to_imvec2(textureSize), childFlags,
|
||||
noScrollFlags))
|
||||
ImGui::ImageWithBg(texture.id, to_imvec2(textureSize), ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
auto infoChildFlags = ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY;
|
||||
if (ImGui::BeginChild("##Spritesheet Info Tooltip Child", ImVec2(), infoChildFlags, noScrollFlags))
|
||||
{
|
||||
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
|
||||
ImGui::TextUnformatted(pathCStr);
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str());
|
||||
|
||||
if (!isValid)
|
||||
ImGui::TextUnformatted(localize.get(TOOLTIP_SPRITESHEET_INVALID));
|
||||
else
|
||||
ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_TEXTURE_SIZE),
|
||||
std::make_format_args(texture.size.x, texture.size.y))
|
||||
.c_str());
|
||||
|
||||
ImGui::TextUnformatted(localize.get(TEXT_OPEN_DIRECTORY));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
auto imageSize = to_imvec2(vec2(spritesheetChildSize.y));
|
||||
auto aspectRatio = (float)texture.size.x / texture.size.y;
|
||||
|
||||
if (imageSize.x / imageSize.y > aspectRatio)
|
||||
imageSize.x = imageSize.y * aspectRatio;
|
||||
else
|
||||
imageSize.y = imageSize.x / aspectRatio;
|
||||
|
||||
ImGui::SetCursorPos(cursorPos);
|
||||
ImGui::ImageWithBg(texture.id, imageSize, ImVec2(), ImVec2(1, 1), ImVec4(), tintColor);
|
||||
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(spritesheetChildSize.y + style.ItemSpacing.x,
|
||||
spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
|
||||
|
||||
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
|
||||
auto spritesheetLabel = std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathCStr));
|
||||
if (document.spritesheet_is_dirty(id))
|
||||
spritesheetLabel =
|
||||
std::vformat(localize.get(FORMAT_SPRITESHEET_NOT_SAVED), std::make_format_args(spritesheetLabel));
|
||||
ImGui::TextUnformatted(spritesheetLabel.c_str());
|
||||
if (isReferenced) ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (isNewSpritesheet)
|
||||
{
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
newSpritesheetId = -1;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
selection.finish();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
context_menu();
|
||||
|
||||
auto rowOneWidgetSize = widget_size_with_row_get(3);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_ADD]);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) add_open();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_SPRITESHEET), settings.shortcutAdd);
|
||||
|
||||
if (dialog.is_selected(Dialog::SPRITESHEET_OPEN))
|
||||
{
|
||||
add(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
if (ImGui::Button(localize.get(BASIC_RELOAD), rowOneWidgetSize)) reload();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RELOAD_SPRITESHEETS));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.size() != 1);
|
||||
if (ImGui::Button(localize.get(BASIC_REPLACE), rowOneWidgetSize)) replace_open();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SPRITESHEET));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (dialog.is_selected(Dialog::SPRITESHEET_REPLACE))
|
||||
{
|
||||
replace(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
if (dialog.is_selected(Dialog::SPRITESHEET_PATH_SET))
|
||||
{
|
||||
set_file_path(dialog.path);
|
||||
dialog.reset();
|
||||
}
|
||||
|
||||
auto rowTwoWidgetSize = widget_size_with_row_get(2);
|
||||
|
||||
shortcut(manager.chords[SHORTCUT_REMOVE]);
|
||||
if (ImGui::Button(localize.get(BASIC_REMOVE_UNUSED), rowTwoWidgetSize)) remove_unused();
|
||||
set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_UNUSED_SPRITESHEETS), settings.shortcutRemove);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(selection.empty());
|
||||
if (ImGui::Button(localize.get(BASIC_SAVE), rowTwoWidgetSize)) save_open();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SAVE_SPRITESHEETS));
|
||||
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_ADD], shortcut::FOCUSED)) add_open();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_REMOVE], shortcut::FOCUSED)) remove_unused();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste();
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED) && selection.size() > 1) merge_open();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
mergePopup.trigger();
|
||||
if (ImGui::BeginPopupModal(mergePopup.label(), &mergePopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
settings.mergeSpritesheetsRegionOrigin =
|
||||
glm::clamp(settings.mergeSpritesheetsRegionOrigin, (int)origin::TOP_LEFT, (int)origin::ORIGIN_CENTER);
|
||||
|
||||
auto close = [&]()
|
||||
{
|
||||
mergeSelection.clear();
|
||||
mergePopup.close();
|
||||
};
|
||||
|
||||
auto optionsSize = child_size_get(6);
|
||||
if (ImGui::BeginChild("##Merge Spritesheets Options", optionsSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
ImGui::SeparatorText(localize.get(LABEL_REGION_PROPERTIES_ORIGIN));
|
||||
ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_BOTTOM), &settings.mergeSpritesheetsOrigin,
|
||||
anm2::APPEND_BOTTOM);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_BOTTOM_LEFT));
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(localize.get(LABEL_MERGE_SPRITESHEETS_APPEND_RIGHT), &settings.mergeSpritesheetsOrigin,
|
||||
anm2::APPEND_RIGHT);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_SPRITESHEETS_TOP_RIGHT));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_OPTIONS));
|
||||
ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_SPRITESHEET_REGIONS), &settings.mergeSpritesheetsIsMakeRegions);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_SPRITESHEET_REGIONS));
|
||||
|
||||
ImGui::BeginDisabled(!settings.mergeSpritesheetsIsMakeRegions);
|
||||
ImGui::Checkbox(localize.get(LABEL_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION),
|
||||
&settings.mergeSpritesheetsIsMakePrimaryRegion);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MERGE_MAKE_PRIMARY_SPRITESHEET_REGION));
|
||||
|
||||
const char* regionOriginOptions[] = {localize.get(LABEL_REGION_ORIGIN_TOP_LEFT),
|
||||
localize.get(LABEL_REGION_ORIGIN_CENTER)};
|
||||
ImGui::Combo(localize.get(LABEL_REGION_PROPERTIES_ORIGIN), &settings.mergeSpritesheetsRegionOrigin,
|
||||
regionOriginOptions, IM_ARRAYSIZE(regionOriginOptions));
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
ImGui::BeginDisabled(mergeSelection.size() <= 1);
|
||||
if (ImGui::Button(localize.get(BASIC_MERGE), widgetSize))
|
||||
{
|
||||
merge();
|
||||
close();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
mergePopup.end();
|
||||
|
||||
packPopup.trigger();
|
||||
if (ImGui::BeginPopupModal(packPopup.label(), &packPopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
settings.packPadding = std::max(0, settings.packPadding);
|
||||
|
||||
auto close = [&]()
|
||||
{
|
||||
packId = -1;
|
||||
packPopup.close();
|
||||
};
|
||||
|
||||
auto optionsSize = child_size_get(1);
|
||||
if (ImGui::BeginChild("##Pack Spritesheet Options", optionsSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
ImGui::DragInt(localize.get(LABEL_PACK_PADDING), &settings.packPadding, DRAG_SPEED, 0, PADDING_MAX);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
shortcut(manager.chords[SHORTCUT_CONFIRM]);
|
||||
bool isPackable = packId != -1 && anm2.content.spritesheets.contains(packId) &&
|
||||
!anm2.content.spritesheets.at(packId).regions.empty();
|
||||
ImGui::BeginDisabled(!isPackable);
|
||||
if (ImGui::Button(localize.get(BASIC_PACK), widgetSize))
|
||||
{
|
||||
if (pack) pack();
|
||||
close();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
shortcut(manager.chords[SHORTCUT_CANCEL]);
|
||||
if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) close();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
packPopup.end();
|
||||
|
||||
overwritePopup.trigger();
|
||||
if (ImGui::BeginPopupModal(overwritePopup.label(), &overwritePopup.isOpen, ImGuiWindowFlags_NoResize))
|
||||
{
|
||||
ImGui::TextUnformatted(localize.get(LABEL_OVERWRITE_CONFIRMATION));
|
||||
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_YES), widgetSize))
|
||||
{
|
||||
save(saveSelection);
|
||||
saveSelection.clear();
|
||||
overwritePopup.close();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_NO), widgetSize))
|
||||
{
|
||||
saveSelection.clear();
|
||||
overwritePopup.close();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
overwritePopup.end();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "dialog.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
class Spritesheets
|
||||
{
|
||||
int newSpritesheetId{-1};
|
||||
PopupHelper mergePopup{PopupHelper(LABEL_SPRITESHEETS_MERGE_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
PopupHelper packPopup{PopupHelper(LABEL_SPRITESHEETS_PACK_POPUP, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
PopupHelper overwritePopup{PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
std::set<int> mergeSelection{};
|
||||
int packId{-1};
|
||||
std::set<int> saveSelection{};
|
||||
|
||||
public:
|
||||
void update(Manager&, Settings&, Resources&, Dialog&, Clipboard& clipboard);
|
||||
};
|
||||
}
|
||||
+2091
-736
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "clipboard.hpp"
|
||||
@@ -8,17 +10,41 @@
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "util/imgui/popup.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
struct TimelineGroupReference
|
||||
{
|
||||
int documentIndex{-1};
|
||||
int animationIndex{-1};
|
||||
int type{NONE};
|
||||
int id{-1};
|
||||
|
||||
auto operator<=>(const TimelineGroupReference&) const = default;
|
||||
};
|
||||
|
||||
struct TimelineRowReference
|
||||
{
|
||||
int documentIndex{-1};
|
||||
int animationIndex{-1};
|
||||
int type{NONE};
|
||||
int id{-1};
|
||||
int index{-1};
|
||||
bool isGroup{};
|
||||
|
||||
auto operator<=>(const TimelineRowReference&) const = default;
|
||||
};
|
||||
|
||||
struct FrameMoveDrag
|
||||
{
|
||||
anm2::Type type{anm2::NONE};
|
||||
int type{NONE};
|
||||
int itemID{-1};
|
||||
int animationIndex{-1};
|
||||
int frameIndex{-1};
|
||||
int duration{1};
|
||||
std::vector<int> indices{};
|
||||
std::vector<Reference> references{};
|
||||
bool isActive{};
|
||||
};
|
||||
|
||||
@@ -30,8 +56,22 @@ namespace anm2ed::imgui
|
||||
popup::ItemProperties itemProperties{};
|
||||
PopupHelper bakePopup{PopupHelper(LABEL_TIMELINE_BAKE_POPUP, POPUP_SMALL_NO_HEIGHT)};
|
||||
int hoveredTime{};
|
||||
anm2::Frame* draggedFrame{};
|
||||
anm2::Type draggedFrameType{};
|
||||
bool isFrameBoxSelecting{};
|
||||
bool isFrameBoxAdditive{};
|
||||
ImVec2 frameBoxStart{};
|
||||
ImVec2 frameBoxEnd{};
|
||||
std::set<Reference> frameBoxSelection{};
|
||||
std::set<TimelineGroupReference> groupReferences{};
|
||||
TimelineRowReference rowSelectionAnchor{};
|
||||
bool isRowSelectionAnchorSet{};
|
||||
PopupHelper groupPropertiesPopup{PopupHelper(LABEL_GROUP_PROPERTIES, POPUP_SMALL_NO_HEIGHT)};
|
||||
std::string groupName{};
|
||||
int groupAnimationIndex{-1};
|
||||
int groupType{NONE};
|
||||
int groupId{-1};
|
||||
Reference draggedFrameReference{};
|
||||
bool isDraggedFrameActive{};
|
||||
int draggedFrameType{};
|
||||
int draggedFrameIndex{-1};
|
||||
int draggedFrameStart{-1};
|
||||
int draggedFrameStartDuration{-1};
|
||||
@@ -42,7 +82,10 @@ namespace anm2ed::imgui
|
||||
std::vector<int> frameSelectionSnapshot{};
|
||||
std::vector<int> frameSelectionLocked{};
|
||||
bool isFrameSelectionLocked{};
|
||||
anm2::Reference frameSelectionSnapshotReference{};
|
||||
Reference frameSelectionSnapshotReference{};
|
||||
Reference frameSelectionAnchor{};
|
||||
bool isFrameSelectionAnchorSet{};
|
||||
std::vector<TimelineRowReference> rowDragReferences{};
|
||||
glm::vec2 scroll{};
|
||||
ImDrawList* pickerLineDrawList{};
|
||||
ImGuiStyle style{};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "strings.hpp"
|
||||
#include "tool.hpp"
|
||||
#include "types.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
using namespace anm2ed::types;
|
||||
@@ -30,10 +31,12 @@ namespace anm2ed::imgui
|
||||
switch (type)
|
||||
{
|
||||
case tool::UNDO:
|
||||
if (document.is_able_to_undo()) document.undo();
|
||||
if (document.is_able_to_undo())
|
||||
manager.command_push({manager.selected, [](Manager&, Document& document) { document.undo(); }});
|
||||
break;
|
||||
case tool::REDO:
|
||||
if (document.is_able_to_redo()) document.redo();
|
||||
if (document.is_able_to_redo())
|
||||
manager.command_push({manager.selected, [](Manager&, Document& document) { document.redo(); }});
|
||||
break;
|
||||
case tool::COLOR:
|
||||
colorEditPopup.open();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "strings.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
@@ -15,8 +15,7 @@ namespace anm2ed::imgui
|
||||
{
|
||||
auto viewport = ImGui::GetMainViewport();
|
||||
auto windowHeight = viewport->Size.y - taskbar.height - documents.height;
|
||||
if (windowHeight < 1.0f)
|
||||
windowHeight = 1.0f;
|
||||
if (windowHeight < 1.0f) windowHeight = 1.0f;
|
||||
|
||||
ImGui::SetNextWindowViewport(viewport->ID);
|
||||
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height));
|
||||
@@ -37,10 +36,10 @@ namespace anm2ed::imgui
|
||||
auto widgetSize = widget_size_with_row_get(2);
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_NEW), widgetSize))
|
||||
dialog.file_save(Dialog::ANM2_NEW); // handled in taskbar.cpp
|
||||
dialog.file_save(Dialog::ANM2_CREATE); // handled in taskbar.cpp
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(localize.get(BASIC_OPEN), widgetSize))
|
||||
dialog.file_open(Dialog::ANM2_OPEN); // handled in taskbar.cpp
|
||||
dialog.file_open(Dialog::ANM2_OPEN, true); // handled in taskbar.cpp
|
||||
|
||||
if (ImGui::BeginChild("##Recent Files Child", {}, ImGuiChildFlags_Borders))
|
||||
{
|
||||
@@ -53,7 +52,7 @@ namespace anm2ed::imgui
|
||||
|
||||
if (ImGui::Selectable(label.c_str()))
|
||||
{
|
||||
manager.open(file);
|
||||
manager.command_push({.runManager = [file](Manager& manager) { manager.open(file); }});
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "clipboard.hpp"
|
||||
#include "dialog.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "storage.hpp"
|
||||
|
||||
namespace anm2ed::imgui
|
||||
{
|
||||
enum WindowFlag
|
||||
{
|
||||
WINDOW_ADD = 1 << 0,
|
||||
WINDOW_REMOVE = 1 << 1,
|
||||
WINDOW_REMOVE_UNUSED = 1 << 2,
|
||||
WINDOW_DUPLICATE = 1 << 3,
|
||||
WINDOW_MERGE = 1 << 4,
|
||||
WINDOW_DEFAULT = 1 << 5,
|
||||
WINDOW_CUT = 1 << 6,
|
||||
WINDOW_COPY = 1 << 7,
|
||||
WINDOW_PASTE = 1 << 8,
|
||||
WINDOW_RENAME = 1 << 9,
|
||||
WINDOW_PROPERTIES = 1 << 10,
|
||||
WINDOW_REFERENCE_ITALIC = 1 << 11
|
||||
};
|
||||
|
||||
using WindowFlags = int;
|
||||
|
||||
constexpr bool window_flag_has(WindowFlags flags, WindowFlag flag) { return (flags & flag) != 0; }
|
||||
|
||||
struct Window
|
||||
{
|
||||
using StorageGet = std::function<Storage&(Document&)>;
|
||||
using ElementGet = std::function<Element*(Anm2&, int)>;
|
||||
using ElementKeyGet = std::function<int(const Element&, int)>;
|
||||
using RowLabelGet = std::function<std::string(Document&, const Element&)>;
|
||||
using RowFontGet = std::function<resource::font::Type(Document&, const Element&, int)>;
|
||||
using RowSelect = std::function<void(Window&, Document&, int)>;
|
||||
using RenameFinish = std::function<void(Document&, Element&, int, int)>;
|
||||
using TooltipDraw = std::function<void(Document&, Resources&, const Element&)>;
|
||||
using RowDragDropUpdate = std::function<bool(Window&, Manager&, Document&, const Element&, int)>;
|
||||
using PropertiesOpen = std::function<void(Manager&, int)>;
|
||||
using Command = std::function<void(Window&, Manager&, Settings&, Document&, Clipboard&)>;
|
||||
using IsAvailable = std::function<bool(Document&)>;
|
||||
using Update = std::function<void(Window&, Manager&, Settings&, Resources&, Clipboard&, Document&)>;
|
||||
using RowsUpdate = std::function<void(Window&, Manager&, Settings&, Resources&, Clipboard&, Document&, ImVec2)>;
|
||||
|
||||
StringType title{};
|
||||
bool Settings::* isOpen{};
|
||||
Document::ChangeType changeType{};
|
||||
ElementType containerType{ElementType::UNKNOWN};
|
||||
ElementType elementType{ElementType::UNKNOWN};
|
||||
const char* childLabel{"##Window Child"};
|
||||
StringType addTooltip{};
|
||||
StringType duplicateTooltip{};
|
||||
StringType mergeTooltip{};
|
||||
StringType removeTooltip{};
|
||||
StringType removeUnusedTooltip{};
|
||||
StringType defaultTooltip{};
|
||||
StringType addEdit{};
|
||||
StringType renameEdit{};
|
||||
StringType pasteEdit{};
|
||||
StringType removeUnusedEdit{};
|
||||
StringType deserializeFailedToast{};
|
||||
StringType unavailableText{STRING_UNDEFINED};
|
||||
int newElementId{-1};
|
||||
int scrollQueued{-1};
|
||||
int renameQueued{-1};
|
||||
int renameId{-1};
|
||||
int editId{-1};
|
||||
int footerRows{-1};
|
||||
RenameState renameState{RENAME_SELECTABLE};
|
||||
std::string renameText{};
|
||||
PopupHelper popup{STRING_UNDEFINED};
|
||||
PopupHelper popup2{STRING_UNDEFINED};
|
||||
PopupHelper popup3{STRING_UNDEFINED};
|
||||
std::set<int> selection{};
|
||||
std::set<int> selection2{};
|
||||
std::vector<int> dragSelection{};
|
||||
std::vector<int> order{};
|
||||
Element editElement{};
|
||||
Dialog* dialog{};
|
||||
WindowFlags flags{WINDOW_COPY | WINDOW_PASTE};
|
||||
bool isChildPaddingZero{};
|
||||
bool isPreserveEditElementOnOpen{};
|
||||
ImVec2 tooltipWindowPadding{};
|
||||
ImVec2 tooltipItemSpacing{};
|
||||
StorageGet storage_get{};
|
||||
ElementGet element_get{};
|
||||
ElementKeyGet element_key_get{};
|
||||
RowLabelGet row_label_get{};
|
||||
RowFontGet row_font_get{};
|
||||
RowSelect row_select{};
|
||||
RenameFinish rename_finish{};
|
||||
TooltipDraw tooltip_draw{};
|
||||
RowDragDropUpdate row_drag_drop_update{};
|
||||
PropertiesOpen properties_open{};
|
||||
Command add{};
|
||||
Command remove{};
|
||||
Command duplicate{};
|
||||
Command merge{};
|
||||
Command merge_open{};
|
||||
Command default_set{};
|
||||
Command cut{};
|
||||
Command copy{};
|
||||
Command paste{};
|
||||
Command reload{};
|
||||
Command replace{};
|
||||
Command save{};
|
||||
Command pack{};
|
||||
Command trim{};
|
||||
Command properties{};
|
||||
Command open{};
|
||||
Command path_set{};
|
||||
IsAvailable is_available{};
|
||||
Update begin_update{};
|
||||
RowsUpdate rows_update{};
|
||||
Update context_update{};
|
||||
Update footer_update{};
|
||||
Update body_update{};
|
||||
Update popup_update{};
|
||||
Update post_update{};
|
||||
};
|
||||
|
||||
Window animations_window_register();
|
||||
Window regions_window_register();
|
||||
Window sounds_window_register();
|
||||
Window spritesheets_window_register();
|
||||
Window layers_window_register();
|
||||
Window nulls_window_register();
|
||||
Window events_window_register();
|
||||
void window_update(Window&, Manager&, Settings&, Resources&, Dialog&, Clipboard&);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ namespace anm2ed::imgui::wizard
|
||||
{""},
|
||||
{"Designer", font::BOLD},
|
||||
{"Shweet"},
|
||||
{"OpenAI Codex"},
|
||||
{""},
|
||||
{"Additional Help", font::BOLD},
|
||||
{"im-tem"},
|
||||
@@ -37,7 +38,7 @@ namespace anm2ed::imgui::wizard
|
||||
{"Music", font::BOLD},
|
||||
{"Soundbin"},
|
||||
{"\"Digital Antidepressant\""},
|
||||
{"https://soundbin.newgrounds.com/"},
|
||||
{"https://www.newgrounds.com/audio/listen/1565433"},
|
||||
{"License: CC0"},
|
||||
{""},
|
||||
{"Libraries", font::BOLD},
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
#include "change_all_frame_properties.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "math_.hpp"
|
||||
#include "math.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
using namespace anm2ed::util::math;
|
||||
using namespace glm;
|
||||
|
||||
namespace anm2ed::imgui::wizard
|
||||
{
|
||||
namespace
|
||||
enum ChangeDestination
|
||||
{
|
||||
enum ChangeDestination
|
||||
{
|
||||
CHANGE_DESTINATION_FRAMES,
|
||||
CHANGE_DESTINATION_ANIMATIONS,
|
||||
};
|
||||
}
|
||||
CHANGE_DESTINATION_FRAMES,
|
||||
CHANGE_DESTINATION_ANIMATIONS,
|
||||
CHANGE_DESTINATION_ITEMS,
|
||||
};
|
||||
|
||||
void ChangeAllFrameProperties::update(Document& document, Settings& settings, bool isFromWizard)
|
||||
void ChangeAllFrameProperties::update(Manager& manager, Document& document, Settings& settings, bool isFromWizard)
|
||||
{
|
||||
isChanged = false;
|
||||
|
||||
auto& anm2 = document.anm2;
|
||||
auto& frames = document.frames.selection;
|
||||
auto& frameReferences = document.frames.references;
|
||||
auto& itemReferences = document.items.references;
|
||||
auto& animations = document.animation.selection;
|
||||
auto& isCropX = settings.changeIsCropX;
|
||||
auto& isCropY = settings.changeIsCropY;
|
||||
@@ -71,19 +74,56 @@ namespace anm2ed::imgui::wizard
|
||||
auto& isNulls = settings.changeIsNulls;
|
||||
auto& itemType = document.reference.itemType;
|
||||
|
||||
auto is_frame_reference_changeable = [](const Reference& reference) { return reference.itemType != TRIGGER; };
|
||||
auto is_item_reference_changeable = [](const Reference& reference) { return reference.itemType != TRIGGER; };
|
||||
auto selected_frame_references_get = [&]()
|
||||
{
|
||||
std::set<Reference> result = frameReferences;
|
||||
if (result.empty())
|
||||
for (auto frameIndex : frames)
|
||||
result.insert({document.reference.animationIndex, itemType, document.reference.itemID, frameIndex});
|
||||
std::erase_if(result, [&](const Reference& reference) { return !is_frame_reference_changeable(reference); });
|
||||
return result;
|
||||
};
|
||||
auto selected_item_references_get = [&]()
|
||||
{
|
||||
std::set<Reference> result = itemReferences;
|
||||
if (result.empty() && itemType != NONE)
|
||||
result.insert({document.reference.animationIndex, itemType, document.reference.itemID});
|
||||
std::erase_if(result, [&](const Reference& reference) { return !is_item_reference_changeable(reference); });
|
||||
return result;
|
||||
};
|
||||
auto selectedFrameReferences = selected_frame_references_get();
|
||||
auto selectedItemReferences = selected_item_references_get();
|
||||
|
||||
bool isFramesDestination = !isFromWizard || destination == CHANGE_DESTINATION_FRAMES;
|
||||
bool isSelectedFramesAvailable = !frames.empty() && itemType != anm2::TRIGGER;
|
||||
bool isItemsDestination = isFromWizard && destination == CHANGE_DESTINATION_ITEMS;
|
||||
bool isSelectedFramesAvailable = !selectedFrameReferences.empty();
|
||||
bool isSelectedItemsAvailable = !selectedItemReferences.empty();
|
||||
bool isSelectedAnimationsAvailable = !animations.empty();
|
||||
if (isFromWizard)
|
||||
{
|
||||
if (destination == CHANGE_DESTINATION_FRAMES && !isSelectedFramesAvailable && isSelectedAnimationsAvailable)
|
||||
destination = CHANGE_DESTINATION_ANIMATIONS;
|
||||
if (destination == CHANGE_DESTINATION_FRAMES && !isSelectedFramesAvailable)
|
||||
destination = isSelectedItemsAvailable ? CHANGE_DESTINATION_ITEMS : CHANGE_DESTINATION_ANIMATIONS;
|
||||
if (destination == CHANGE_DESTINATION_ITEMS && !isSelectedItemsAvailable)
|
||||
destination = isSelectedFramesAvailable ? CHANGE_DESTINATION_FRAMES : CHANGE_DESTINATION_ANIMATIONS;
|
||||
if (destination == CHANGE_DESTINATION_ANIMATIONS && !isSelectedAnimationsAvailable && isSelectedItemsAvailable)
|
||||
destination = CHANGE_DESTINATION_ITEMS;
|
||||
if (destination == CHANGE_DESTINATION_ANIMATIONS && !isSelectedAnimationsAvailable && isSelectedFramesAvailable)
|
||||
destination = CHANGE_DESTINATION_FRAMES;
|
||||
isFramesDestination = destination == CHANGE_DESTINATION_FRAMES;
|
||||
isItemsDestination = destination == CHANGE_DESTINATION_ITEMS;
|
||||
}
|
||||
|
||||
bool isLayerPropertyAvailable = isFramesDestination ? itemType == anm2::LAYER : isLayers;
|
||||
auto isLayerPropertyAvailable = isFramesDestination
|
||||
? std::ranges::any_of(selectedFrameReferences,
|
||||
[](const Reference& reference)
|
||||
{ return reference.itemType == LAYER; })
|
||||
: isItemsDestination
|
||||
? std::ranges::any_of(selectedItemReferences,
|
||||
[](const Reference& reference)
|
||||
{ return reference.itemType == LAYER; })
|
||||
: isLayers;
|
||||
|
||||
#define PROPERTIES_WIDGET(body, checkboxLabel, isEnabled) \
|
||||
ImGui::Checkbox(checkboxLabel, &isEnabled); \
|
||||
@@ -212,9 +252,8 @@ namespace anm2ed::imgui::wizard
|
||||
|
||||
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),
|
||||
checkboxLabel, isEnabled);
|
||||
PROPERTIES_WIDGET(input_int_range(valueLabel, value, FRAME_DURATION_MIN, FRAME_DURATION_MAX, STEP, STEP_FAST),
|
||||
checkboxLabel, isEnabled);
|
||||
};
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImGui::GetStyle().ItemInnerSpacing);
|
||||
@@ -248,26 +287,22 @@ namespace anm2ed::imgui::wizard
|
||||
std::vector<int> fallbackIds{-1};
|
||||
std::vector<std::string> fallbackLabelsString{localize.get(BASIC_NONE)};
|
||||
std::vector<const char*> fallbackLabels{fallbackLabelsString[0].c_str()};
|
||||
std::vector<int> interpolationIds{anm2::Frame::Interpolation::NONE, anm2::Frame::Interpolation::LINEAR,
|
||||
anm2::Frame::Interpolation::EASE_IN, anm2::Frame::Interpolation::EASE_OUT,
|
||||
anm2::Frame::Interpolation::EASE_IN_OUT};
|
||||
std::vector<std::string> interpolationLabelsString{
|
||||
localize.get(BASIC_NONE), localize.get(BASIC_LINEAR), localize.get(BASIC_EASE_IN),
|
||||
localize.get(BASIC_EASE_OUT), localize.get(BASIC_EASE_IN_OUT)};
|
||||
std::vector<const char*> interpolationLabels{interpolationLabelsString[0].c_str(),
|
||||
interpolationLabelsString[1].c_str(),
|
||||
interpolationLabelsString[2].c_str(),
|
||||
interpolationLabelsString[3].c_str(),
|
||||
interpolationLabelsString[4].c_str()};
|
||||
std::vector<int> interpolationIds{(int)Interpolation::NONE, (int)Interpolation::LINEAR, (int)Interpolation::EASE_IN,
|
||||
(int)Interpolation::EASE_OUT, (int)Interpolation::EASE_IN_OUT};
|
||||
std::vector<std::string> interpolationLabelsString{localize.get(BASIC_NONE), localize.get(BASIC_LINEAR),
|
||||
localize.get(BASIC_EASE_IN), localize.get(BASIC_EASE_OUT),
|
||||
localize.get(BASIC_EASE_IN_OUT)};
|
||||
std::vector<const char*> interpolationLabels{
|
||||
interpolationLabelsString[0].c_str(), interpolationLabelsString[1].c_str(),
|
||||
interpolationLabelsString[2].c_str(), interpolationLabelsString[3].c_str(),
|
||||
interpolationLabelsString[4].c_str()};
|
||||
|
||||
const Storage* regionStorage = nullptr;
|
||||
if (itemType == anm2::LAYER && document.reference.itemID != -1)
|
||||
if (itemType == LAYER && document.reference.itemID != -1)
|
||||
{
|
||||
if (auto layerIt = document.anm2.content.layers.find(document.reference.itemID);
|
||||
layerIt != document.anm2.content.layers.end())
|
||||
if (auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, document.reference.itemID))
|
||||
{
|
||||
auto spritesheetID = layerIt->second.spritesheetID;
|
||||
auto regionIt = document.regionBySpritesheet.find(spritesheetID);
|
||||
auto regionIt = document.regionBySpritesheet.find(layer->spritesheetId);
|
||||
if (regionIt != document.regionBySpritesheet.end()) regionStorage = ®ionIt->second;
|
||||
}
|
||||
}
|
||||
@@ -294,9 +329,9 @@ namespace anm2ed::imgui::wizard
|
||||
|
||||
#undef PROPERTIES_WIDGET
|
||||
|
||||
auto frame_change = [&](anm2::ChangeType changeType)
|
||||
auto frame_change = [&](ChangeType changeType)
|
||||
{
|
||||
anm2::FrameChange frameChange;
|
||||
FrameChange frameChange;
|
||||
if (isCropX) frameChange.cropX = crop.x;
|
||||
if (isCropY) frameChange.cropY = crop.y;
|
||||
if (isSizeX) frameChange.sizeX = size.x;
|
||||
@@ -309,7 +344,7 @@ namespace anm2ed::imgui::wizard
|
||||
if (isScaleY) frameChange.scaleY = scale.y;
|
||||
if (isRotation) frameChange.rotation = std::make_optional(rotation);
|
||||
if (isDuration) frameChange.duration = std::make_optional(duration);
|
||||
if (isRegion) frameChange.regionID = std::make_optional(regionId);
|
||||
if (isRegion) frameChange.regionId = std::make_optional(regionId);
|
||||
if (isTintR) frameChange.tintR = tint.r;
|
||||
if (isTintG) frameChange.tintG = tint.g;
|
||||
if (isTintB) frameChange.tintB = tint.b;
|
||||
@@ -318,67 +353,148 @@ namespace anm2ed::imgui::wizard
|
||||
if (isColorOffsetG) frameChange.colorOffsetG = colorOffset.g;
|
||||
if (isColorOffsetB) frameChange.colorOffsetB = colorOffset.b;
|
||||
if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible);
|
||||
if (isInterpolationSet)
|
||||
frameChange.interpolation = std::make_optional(static_cast<anm2::Frame::Interpolation>(interpolation));
|
||||
if (isInterpolationSet) frameChange.interpolation = std::make_optional(static_cast<Interpolation>(interpolation));
|
||||
if (isFlipXSet) frameChange.isFlipX = std::make_optional(isFlipX);
|
||||
if (isFlipYSet) frameChange.isFlipY = std::make_optional(isFlipY);
|
||||
|
||||
auto all_frames_selection = [](anm2::Item& item)
|
||||
{
|
||||
std::set<int> selection{};
|
||||
for (int i = 0; i < (int)item.frames.size(); ++i)
|
||||
selection.insert(i);
|
||||
return selection;
|
||||
};
|
||||
if (isFramesDestination && selectedFrameReferences.empty()) return;
|
||||
if (isItemsDestination && selectedItemReferences.empty()) return;
|
||||
|
||||
if (isFramesDestination)
|
||||
{
|
||||
if (auto item = document.item_get())
|
||||
{
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES,
|
||||
item->frames_change(frameChange, itemType, changeType, frames));
|
||||
auto queuedFrameReferences = selectedFrameReferences;
|
||||
auto queuedItemReferences = selectedItemReferences;
|
||||
auto queuedAnimations = animations;
|
||||
auto queuedIsFramesDestination = isFramesDestination;
|
||||
auto queuedIsItemsDestination = isItemsDestination;
|
||||
auto queuedIsRoot = isRoot;
|
||||
auto queuedIsLayers = isLayers;
|
||||
auto queuedIsNulls = isNulls;
|
||||
|
||||
isChanged = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto behavior = [&]()
|
||||
{
|
||||
for (auto animationIndex : animations)
|
||||
{
|
||||
if (animationIndex < 0 || animationIndex >= (int)document.anm2.animations.items.size()) continue;
|
||||
auto& animation = document.anm2.animations.items[animationIndex];
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto all_frames_selection = [](const Element& item)
|
||||
{
|
||||
std::set<int> selection{};
|
||||
int index{};
|
||||
for (const auto& frame : item.children)
|
||||
if (frame.type == ElementType::FRAME)
|
||||
{
|
||||
selection.insert(index);
|
||||
++index;
|
||||
}
|
||||
return selection;
|
||||
};
|
||||
|
||||
if (isRoot)
|
||||
{
|
||||
auto selection = all_frames_selection(animation.rootAnimation);
|
||||
animation.rootAnimation.frames_change(frameChange, anm2::ROOT, changeType, selection);
|
||||
}
|
||||
auto& anm2 = document.anm2;
|
||||
|
||||
if (isLayers)
|
||||
{
|
||||
for (auto& [_, item] : animation.layerAnimations)
|
||||
{
|
||||
auto selection = all_frames_selection(item);
|
||||
item.frames_change(frameChange, anm2::LAYER, changeType, selection);
|
||||
}
|
||||
}
|
||||
if (queuedIsFramesDestination)
|
||||
{
|
||||
std::map<Reference, std::set<int>> groupedFrames{};
|
||||
for (auto frameReference : queuedFrameReferences)
|
||||
{
|
||||
auto itemReference = frameReference;
|
||||
itemReference.frameIndex = -1;
|
||||
groupedFrames[itemReference].insert(frameReference.frameIndex);
|
||||
}
|
||||
|
||||
if (isNulls)
|
||||
{
|
||||
for (auto& [_, item] : animation.nullAnimations)
|
||||
{
|
||||
auto selection = all_frames_selection(item);
|
||||
item.frames_change(frameChange, anm2::NULL_, changeType, selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
document.snapshot(localize.get(EDIT_CHANGE_FRAME_PROPERTIES));
|
||||
for (auto& [itemReference, selection] : groupedFrames)
|
||||
{
|
||||
auto item = anm2.element_get(itemReference.animationIndex,
|
||||
static_cast<ItemType>(itemReference.itemType),
|
||||
itemReference.itemID);
|
||||
if (!item) continue;
|
||||
frames_change(*item, frameChange, static_cast<ItemType>(itemReference.itemType),
|
||||
changeType, selection);
|
||||
}
|
||||
document.anm2_change(Document::FRAMES);
|
||||
return;
|
||||
}
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES, behavior());
|
||||
isChanged = true;
|
||||
}
|
||||
if (queuedIsItemsDestination)
|
||||
{
|
||||
document.snapshot(localize.get(EDIT_CHANGE_FRAME_PROPERTIES));
|
||||
for (auto itemReference : queuedItemReferences)
|
||||
{
|
||||
if (itemReference.itemType == TRIGGER) continue;
|
||||
auto item = anm2.element_get(itemReference.animationIndex,
|
||||
static_cast<ItemType>(itemReference.itemType),
|
||||
itemReference.itemID);
|
||||
if (!item) continue;
|
||||
auto selection = all_frames_selection(*item);
|
||||
frames_change(*item, frameChange, static_cast<ItemType>(itemReference.itemType),
|
||||
changeType, selection);
|
||||
}
|
||||
document.anm2_change(Document::FRAMES);
|
||||
return;
|
||||
}
|
||||
|
||||
document.snapshot(localize.get(EDIT_CHANGE_FRAME_PROPERTIES));
|
||||
for (auto animationIndex : queuedAnimations)
|
||||
{
|
||||
auto animation = anm2.element_get(ElementType::ANIMATION, animationIndex);
|
||||
if (!animation) continue;
|
||||
|
||||
if (queuedIsRoot)
|
||||
{
|
||||
if (auto item = animation_item_get(*animation, ItemType::ROOT))
|
||||
{
|
||||
auto selection = all_frames_selection(*item);
|
||||
frames_change(*item, frameChange, ItemType::ROOT, changeType, selection);
|
||||
}
|
||||
}
|
||||
|
||||
if (queuedIsLayers)
|
||||
{
|
||||
auto layerAnimations = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS);
|
||||
if (layerAnimations)
|
||||
{
|
||||
auto item_change = [&](auto&& self, Element& item) -> void
|
||||
{
|
||||
if (item.type == ElementType::GROUP)
|
||||
{
|
||||
for (auto& child : item.children)
|
||||
self(self, child);
|
||||
return;
|
||||
}
|
||||
if (item.type == ElementType::LAYER_ANIMATION)
|
||||
{
|
||||
auto selection = all_frames_selection(item);
|
||||
frames_change(item, frameChange, ItemType::LAYER, changeType, selection);
|
||||
}
|
||||
};
|
||||
for (auto& item : layerAnimations->children)
|
||||
item_change(item_change, item);
|
||||
}
|
||||
}
|
||||
|
||||
if (queuedIsNulls)
|
||||
{
|
||||
auto nullAnimations = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS);
|
||||
if (nullAnimations)
|
||||
{
|
||||
auto item_change = [&](auto&& self, Element& item) -> void
|
||||
{
|
||||
if (item.type == ElementType::GROUP)
|
||||
{
|
||||
for (auto& child : item.children)
|
||||
self(self, child);
|
||||
return;
|
||||
}
|
||||
if (item.type == ElementType::NULL_ANIMATION)
|
||||
{
|
||||
auto selection = all_frames_selection(item);
|
||||
frames_change(item, frameChange, ItemType::NULL_, changeType, selection);
|
||||
}
|
||||
};
|
||||
for (auto& item : nullAnimations->children)
|
||||
item_change(item_change, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
document.anm2_change(Document::FRAMES);
|
||||
}});
|
||||
isChanged = true;
|
||||
};
|
||||
|
||||
ImGui::Separator();
|
||||
@@ -394,12 +510,19 @@ namespace anm2ed::imgui::wizard
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(!isSelectedItemsAvailable);
|
||||
ImGui::RadioButton(localize.get(LABEL_ITEMS), &destination, CHANGE_DESTINATION_ITEMS);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CHANGE_ALL_DESTINATION_ITEMS));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(!isSelectedAnimationsAvailable);
|
||||
ImGui::RadioButton(localize.get(LABEL_ANIMATIONS_CHILD), &destination, CHANGE_DESTINATION_ANIMATIONS);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CHANGE_ALL_DESTINATION_ANIMATIONS));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::BeginDisabled(isFramesDestination);
|
||||
ImGui::BeginDisabled(isFramesDestination || isItemsDestination);
|
||||
ImGui::Checkbox(localize.get(LABEL_ROOT), &isRoot);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CHANGE_ALL_ROOT));
|
||||
ImGui::SameLine();
|
||||
@@ -418,33 +541,34 @@ namespace anm2ed::imgui::wizard
|
||||
isTintB || isTintA || isColorOffsetR || isColorOffsetG || isColorOffsetB || isRegion ||
|
||||
isVisibleSet || isInterpolationSet || isFlipXSet || isFlipYSet;
|
||||
bool isDestinationValid = isFramesDestination ? isSelectedFramesAvailable
|
||||
: isSelectedAnimationsAvailable && (isRoot || isLayers || isNulls);
|
||||
: isItemsDestination ? isSelectedItemsAvailable
|
||||
: isSelectedAnimationsAvailable && (isRoot || isLayers || isNulls);
|
||||
|
||||
auto rowWidgetSize = widget_size_with_row_get(5);
|
||||
|
||||
ImGui::BeginDisabled(!isAnyProperty || !isDestinationValid);
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(anm2::ADJUST);
|
||||
if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(ChangeType::ADJUST);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADJUST));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(anm2::ADD);
|
||||
if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(ChangeType::ADD);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_VALUES));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(anm2::SUBTRACT);
|
||||
if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(ChangeType::SUBTRACT);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SUBTRACT_VALUES));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(anm2::MULTIPLY);
|
||||
if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(ChangeType::MULTIPLY);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MULTIPLY_VALUES));
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(anm2::DIVIDE);
|
||||
if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(ChangeType::DIVIDE);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DIVIDE_VALUES));
|
||||
|
||||
ImGui::EndDisabled();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "document.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
namespace anm2ed::imgui::wizard
|
||||
@@ -10,6 +11,6 @@ namespace anm2ed::imgui::wizard
|
||||
public:
|
||||
bool isChanged{};
|
||||
|
||||
void update(Document&, Settings&, bool = false);
|
||||
void update(Manager&, Document&, Settings&, bool = false);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "configure.hpp"
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "sdl.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
|
||||
@@ -24,11 +24,9 @@ namespace anm2ed::imgui::wizard
|
||||
if (ImGui::BeginChild("##Tab Child", childSize, true))
|
||||
{
|
||||
ImGui::SeparatorText(localize.get(LABEL_WINDOW_MENU));
|
||||
input_float_range(localize.get(LABEL_UI_SCALE), temporary.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f");
|
||||
input_float_range(localize.get(LABEL_UI_SCALE), temporary.uiScale, UI_SCALE_MIN, UI_SCALE_MAX,
|
||||
UI_SCALE_STEP, UI_SCALE_STEP, "%.2fx");
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_UI_SCALE));
|
||||
input_float_range(localize.get(LABEL_ITEM_HEIGHT), temporary.timelineItemHeight, 0.75f, 1.25f, 0.05f, 0.05f,
|
||||
"%.2f");
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_HEIGHT));
|
||||
ImGui::Checkbox(localize.get(LABEL_VSYNC), &temporary.isVsync);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC));
|
||||
|
||||
@@ -70,13 +68,13 @@ namespace anm2ed::imgui::wizard
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_STACK_SIZE));
|
||||
|
||||
ImGui::SeparatorText(localize.get(LABEL_COMPATIBILITY));
|
||||
ImGui::RadioButton(localize.get(LABEL_ISAAC), &temporary.fileCompatibility, anm2::ISAAC);
|
||||
ImGui::RadioButton(localize.get(LABEL_ISAAC), &temporary.fileCompatibility, ISAAC);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ISAAC));
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(localize.get(LABEL_ANM2ED), &temporary.fileCompatibility, anm2::ANM2ED);
|
||||
ImGui::RadioButton(localize.get(LABEL_ANM2ED), &temporary.fileCompatibility, ANM2ED);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED));
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(localize.get(LABEL_ANM2ED_LIMITED), &temporary.fileCompatibility, anm2::ANM2ED_LIMITED);
|
||||
ImGui::RadioButton(localize.get(LABEL_ANM2ED_LIMITED), &temporary.fileCompatibility, ANM2ED_LIMITED);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COMPATIBILITY_ANM2ED_LIMITED));
|
||||
|
||||
ImGui::Checkbox(localize.get(LABEL_SPECIAL_INTERPOLATED_FRAMES_REMINDER_ON_SAVE),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "generate_animation_from_grid.hpp"
|
||||
|
||||
#include "math_.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include "math.hpp"
|
||||
#include "types.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
@@ -11,10 +14,12 @@ namespace anm2ed::imgui::wizard
|
||||
{
|
||||
GenerateAnimationFromGrid::GenerateAnimationFromGrid() : Canvas(vec2()) {}
|
||||
|
||||
void GenerateAnimationFromGrid::update(Document& document, Resources& resources, Settings& settings)
|
||||
void GenerateAnimationFromGrid::update(Manager& manager, Document& document, Resources& resources, Settings& settings)
|
||||
{
|
||||
isEnd = false;
|
||||
|
||||
auto& anm2 = document.anm2;
|
||||
auto& reference = document.reference;
|
||||
auto& startPosition = settings.generateStartPosition;
|
||||
auto& size = settings.generateSize;
|
||||
auto& pivot = settings.generatePivot;
|
||||
@@ -35,12 +40,17 @@ namespace anm2ed::imgui::wizard
|
||||
ImGui::InputInt(localize.get(LABEL_GENERATE_ROWS), &rows, STEP, STEP_FAST);
|
||||
ImGui::InputInt(localize.get(LABEL_GENERATE_COLUMNS), &columns, STEP, STEP_FAST);
|
||||
|
||||
input_int_range(localize.get(LABEL_GENERATE_COUNT), count, anm2::FRAME_NUM_MIN, rows * columns);
|
||||
input_int_range(localize.get(LABEL_GENERATE_COUNT), count, FRAME_NUM_MIN, rows * columns);
|
||||
|
||||
ImGui::InputInt(localize.get(BASIC_DURATION), &delay, STEP, STEP_FAST);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
rows = std::max(rows, 1);
|
||||
columns = std::max(columns, 1);
|
||||
count = std::clamp(count, FRAME_NUM_MIN, rows * columns);
|
||||
delay = std::max(delay, FRAME_DURATION_MIN);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::BeginChild("##Preview Child", childSize, ImGuiChildFlags_Borders))
|
||||
@@ -58,28 +68,23 @@ namespace anm2ed::imgui::wizard
|
||||
viewport_set();
|
||||
clear(isTransparent ? vec4(0) : vec4(backgroundColor, 1.0f));
|
||||
|
||||
if (document.reference.itemType == anm2::LAYER)
|
||||
if (reference.itemType == LAYER)
|
||||
{
|
||||
auto layerIt = document.anm2.content.layers.find(document.reference.itemID);
|
||||
if (layerIt != document.anm2.content.layers.end())
|
||||
auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, reference.itemID);
|
||||
auto sourceTexture = layer ? document.texture_get(layer->spritesheetId) : nullptr;
|
||||
if (sourceTexture)
|
||||
{
|
||||
auto spritesheetIt = document.anm2.content.spritesheets.find(layerIt->second.spritesheetID);
|
||||
if (spritesheetIt != document.anm2.content.spritesheets.end())
|
||||
{
|
||||
auto& texture = spritesheetIt->second.texture;
|
||||
auto index = std::clamp((int)(time * (count - 1)), 0, (count - 1));
|
||||
auto row = index / columns;
|
||||
auto column = index % columns;
|
||||
auto crop = startPosition + ivec2(size.x * column, size.y * row);
|
||||
auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(sourceTexture->size);
|
||||
auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(sourceTexture->size);
|
||||
|
||||
auto index = std::clamp((int)(time * (count - 1)), 0, (count - 1));
|
||||
auto row = index / columns;
|
||||
auto column = index % columns;
|
||||
auto crop = startPosition + ivec2(size.x * column, size.y * row);
|
||||
auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size);
|
||||
auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size);
|
||||
mat4 transform = transform_get(zoom) * math::quad_model_get(size, {}, pivot);
|
||||
|
||||
mat4 transform = transform_get(zoom) * math::quad_model_get(size, {}, pivot);
|
||||
|
||||
texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {},
|
||||
math::uv_vertices_get(uvMin, uvMax).data());
|
||||
}
|
||||
texture_render(shaderTexture, sourceTexture->id, transform, vec4(1.0f), {},
|
||||
math::uv_vertices_get(uvMin, uvMax).data());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,19 +109,32 @@ namespace anm2ed::imgui::wizard
|
||||
|
||||
if (ImGui::Button(localize.get(LABEL_GENERATE), widgetSize))
|
||||
{
|
||||
auto generate_from_grid = [&]()
|
||||
{
|
||||
auto item = document.item_get();
|
||||
auto animation = document.animation_get();
|
||||
auto queuedReference = reference;
|
||||
auto queuedStartPosition = startPosition;
|
||||
auto queuedSize = size;
|
||||
auto queuedPivot = pivot;
|
||||
auto queuedColumns = columns;
|
||||
auto queuedCount = count;
|
||||
auto queuedDelay = delay;
|
||||
|
||||
if (item && animation)
|
||||
{
|
||||
item->frames_generate_from_grid(startPosition, size, pivot, columns, count, delay);
|
||||
animation->frameNum = animation->length();
|
||||
}
|
||||
};
|
||||
manager.command_push({manager.selected,
|
||||
[=](Manager&, Document& document)
|
||||
{
|
||||
auto itemType = static_cast<ItemType>(queuedReference.itemType);
|
||||
auto item = document.anm2.element_get(queuedReference.animationIndex, itemType,
|
||||
queuedReference.itemID);
|
||||
auto animation =
|
||||
document.anm2.element_get(ElementType::ANIMATION, queuedReference.animationIndex);
|
||||
|
||||
DOCUMENT_EDIT(document, localize.get(EDIT_GENERATE_ANIMATION_FROM_GRID), Document::FRAMES, generate_from_grid());
|
||||
if (item && animation)
|
||||
{
|
||||
document.snapshot(localize.get(EDIT_GENERATE_ANIMATION_FROM_GRID));
|
||||
frames_generate_from_grid(*item, queuedStartPosition, queuedSize, queuedPivot,
|
||||
queuedColumns, queuedCount, queuedDelay);
|
||||
animation->frameNum = animation_length_get(*animation);
|
||||
document.anm2_change(Document::FRAMES);
|
||||
}
|
||||
}});
|
||||
isEnd = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "canvas.hpp"
|
||||
#include "document.hpp"
|
||||
#include "manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
@@ -15,6 +16,6 @@ namespace anm2ed::imgui::wizard
|
||||
bool isEnd{};
|
||||
|
||||
GenerateAnimationFromGrid();
|
||||
void update(Document&, Resources&, Settings&);
|
||||
void update(Manager&, Document&, Resources&, Settings&);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "process_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "process.hpp"
|
||||
#include "toast.hpp"
|
||||
|
||||
using namespace anm2ed::resource;
|
||||
@@ -16,7 +16,7 @@ namespace anm2ed::imgui::wizard
|
||||
{
|
||||
void RenderAnimation::range_to_animation_set(Manager& manager, Document& document)
|
||||
{
|
||||
if (auto animation = document.animation_get())
|
||||
if (auto animation = document.anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex))
|
||||
{
|
||||
manager.recordingStart = 0;
|
||||
manager.recordingEnd = animation->frameNum - 1;
|
||||
@@ -28,10 +28,12 @@ namespace anm2ed::imgui::wizard
|
||||
auto& frames = document.frames.selection;
|
||||
if (!frames.empty())
|
||||
{
|
||||
if (auto item = document.item_get())
|
||||
auto& reference = document.reference;
|
||||
auto itemType = static_cast<ItemType>(reference.itemType);
|
||||
if (auto item = document.anm2.element_get(reference.animationIndex, itemType, reference.itemID))
|
||||
{
|
||||
int duration{};
|
||||
for (auto [i, frame] : std::views::enumerate(item->frames))
|
||||
for (auto [i, frame] : std::views::enumerate(item->children))
|
||||
{
|
||||
if ((int)i == *frames.begin()) manager.recordingStart = duration;
|
||||
if ((int)i == *frames.rbegin()) manager.recordingEnd = duration + frame.duration - 1;
|
||||
@@ -53,7 +55,7 @@ namespace anm2ed::imgui::wizard
|
||||
{
|
||||
isEnd = false;
|
||||
|
||||
auto animation = document.animation_get();
|
||||
auto animation = document.anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex);
|
||||
if (!animation) return;
|
||||
|
||||
auto& ffmpegPath = settings.renderFFmpegPath;
|
||||
@@ -155,7 +157,7 @@ namespace anm2ed::imgui::wizard
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginDisabled(frames.empty() || reference.itemID == anm2::TRIGGER);
|
||||
ImGui::BeginDisabled(frames.empty() || reference.itemType == TRIGGER);
|
||||
if (ImGui::Button(localize.get(LABEL_TO_SELECTED_FRAMES))) range_to_frames_set(manager, document);
|
||||
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_SELECTED_FRAMES));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
+2
-2
@@ -13,12 +13,12 @@
|
||||
#include "log.hpp"
|
||||
#include "sdl.hpp"
|
||||
|
||||
#include "imgui_.hpp"
|
||||
#include "util/imgui/imgui.hpp"
|
||||
|
||||
#include "snapshots.hpp"
|
||||
#include "socket.hpp"
|
||||
|
||||
#include "util/math_.hpp"
|
||||
#include "util/math.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
#include <format>
|
||||
#include <print>
|
||||
|
||||
#include "file_.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "file.hpp"
|
||||
#include "path.hpp"
|
||||
#include "sdl.hpp"
|
||||
#include "time_.hpp"
|
||||
#include "time.hpp"
|
||||
|
||||
#if _WIN32
|
||||
#include <fcntl.h>
|
||||
|
||||
+58
-31
@@ -5,13 +5,14 @@
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "file_.hpp"
|
||||
#include "file.hpp"
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "sdl.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "vector_.hpp"
|
||||
#include "util/imgui/shortcut.hpp"
|
||||
#include "vector.hpp"
|
||||
|
||||
using namespace anm2ed::types;
|
||||
using namespace anm2ed::util;
|
||||
@@ -79,6 +80,28 @@ namespace anm2ed
|
||||
|
||||
Document* Manager::get(int index) { return vector::find(documents, index > -1 ? index : selected); }
|
||||
|
||||
void Manager::command_push(Command command)
|
||||
{
|
||||
if (command.documentIndex == -1) command.documentIndex = selected;
|
||||
commands.push_back(std::move(command));
|
||||
}
|
||||
|
||||
void Manager::commands_run()
|
||||
{
|
||||
auto queued = std::move(commands);
|
||||
commands.clear();
|
||||
|
||||
for (auto& command : queued)
|
||||
{
|
||||
if (command.runManager) continue;
|
||||
|
||||
if (auto document = get(command.documentIndex); document) document->command_run(*this, command);
|
||||
}
|
||||
|
||||
for (auto& command : queued)
|
||||
if (command.runManager) command.runManager(*this);
|
||||
}
|
||||
|
||||
Document* Manager::open(const std::filesystem::path& path, bool isNew, bool isRecent)
|
||||
{
|
||||
std::string errorString{};
|
||||
@@ -109,41 +132,44 @@ namespace anm2ed
|
||||
|
||||
void Manager::new_(const std::filesystem::path& path) { open(path, true); }
|
||||
|
||||
void Manager::save(int index, const std::filesystem::path& path, anm2::Compatibility compatibility,
|
||||
bool Manager::save(int index, const std::filesystem::path& path, Compatibility compatibility,
|
||||
bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
if (auto document = get(index); document)
|
||||
{
|
||||
std::string errorString{};
|
||||
ensure_parent_directory_exists(path);
|
||||
const auto previousAutosavePath = document->autosave_path_get();
|
||||
document->path = !path.empty() ? path : document->path;
|
||||
document->path.replace_extension(".anm2");
|
||||
auto savePath = !path.empty() ? path : document->path;
|
||||
savePath.replace_extension(".anm2");
|
||||
ensure_parent_directory_exists(savePath);
|
||||
|
||||
if (!document->save(savePath, &errorString, compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale,
|
||||
isRoundRotation))
|
||||
return false;
|
||||
|
||||
const auto autosavePath = document->autosave_path_get();
|
||||
if (document->save(document->path, &errorString, compatibility, isBakeSpecialInterpolatedFramesOnSave,
|
||||
isRoundScale, isRoundRotation))
|
||||
{
|
||||
autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), previousAutosavePath),
|
||||
autosaveFiles.end());
|
||||
if (autosavePath != previousAutosavePath)
|
||||
autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), autosavePath),
|
||||
autosaveFiles.end());
|
||||
autosave_file_remove(previousAutosavePath);
|
||||
autosave_file_remove(autosavePath);
|
||||
autosave_files_write();
|
||||
}
|
||||
autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), previousAutosavePath),
|
||||
autosaveFiles.end());
|
||||
if (autosavePath != previousAutosavePath)
|
||||
autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), autosavePath), autosaveFiles.end());
|
||||
autosave_file_remove(previousAutosavePath);
|
||||
autosave_file_remove(autosavePath);
|
||||
autosave_files_write();
|
||||
recent_file_add(document->path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Manager::save(const std::filesystem::path& path, anm2::Compatibility compatibility,
|
||||
bool Manager::save(const std::filesystem::path& path, Compatibility compatibility,
|
||||
bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
save(selected, path, compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation);
|
||||
return save(selected, path, compatibility, isBakeSpecialInterpolatedFramesOnSave, isRoundScale, isRoundRotation);
|
||||
}
|
||||
|
||||
void Manager::autosave(Document& document, anm2::Compatibility compatibility,
|
||||
bool isBakeSpecialInterpolatedFramesOnSave, bool isRoundScale, bool isRoundRotation)
|
||||
void Manager::autosave(Document& document, Compatibility compatibility, bool isBakeSpecialInterpolatedFramesOnSave,
|
||||
bool isRoundScale, bool isRoundRotation)
|
||||
{
|
||||
std::string errorString{};
|
||||
auto autosavePath = document.autosave_path_get();
|
||||
@@ -210,9 +236,9 @@ namespace anm2ed
|
||||
if (auto document = get(); document)
|
||||
{
|
||||
if (id == -1)
|
||||
editLayer = anm2::Layer();
|
||||
else if (auto it = document->anm2.content.layers.find(id); it != document->anm2.content.layers.end())
|
||||
editLayer = it->second;
|
||||
editLayer = element_make(ElementType::LAYER_ELEMENT);
|
||||
else if (auto layer = document->anm2.element_get(ElementType::LAYER_ELEMENT, id); layer)
|
||||
editLayer = *layer;
|
||||
else
|
||||
return;
|
||||
|
||||
@@ -228,7 +254,7 @@ namespace anm2ed
|
||||
|
||||
void Manager::layer_properties_close()
|
||||
{
|
||||
editLayer = anm2::Layer();
|
||||
editLayer = element_make(ElementType::LAYER_ELEMENT);
|
||||
layerPropertiesPopup.close();
|
||||
}
|
||||
|
||||
@@ -236,10 +262,11 @@ namespace anm2ed
|
||||
{
|
||||
if (auto document = get(); document)
|
||||
{
|
||||
auto nulls = document->anm2.element_get(ElementType::NULLS);
|
||||
if (id == -1)
|
||||
editNull = anm2::Null();
|
||||
else if (auto it = document->anm2.content.nulls.find(id); it != document->anm2.content.nulls.end())
|
||||
editNull = it->second;
|
||||
editNull = element_make(ElementType::NULL_ELEMENT);
|
||||
else if (auto null = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr; null)
|
||||
editNull = *null;
|
||||
else
|
||||
return;
|
||||
|
||||
@@ -255,7 +282,7 @@ namespace anm2ed
|
||||
|
||||
void Manager::null_properties_close()
|
||||
{
|
||||
editNull = anm2::Null();
|
||||
editNull = element_make(ElementType::NULL_ELEMENT);
|
||||
nullPropertiesPopup.close();
|
||||
}
|
||||
|
||||
|
||||
+20
-6
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -8,11 +9,21 @@
|
||||
#include "document.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "util/imgui/popup.hpp"
|
||||
|
||||
namespace anm2ed
|
||||
{
|
||||
constexpr auto FILE_LABEL_FORMAT = "{} [{}]";
|
||||
|
||||
class Manager;
|
||||
|
||||
struct Command
|
||||
{
|
||||
int documentIndex{-1};
|
||||
std::function<void(Manager&, Document&)> run{};
|
||||
std::function<void(Manager&)> runManager{};
|
||||
};
|
||||
|
||||
class Manager
|
||||
{
|
||||
std::filesystem::path recent_files_path_get();
|
||||
@@ -23,6 +34,7 @@ namespace anm2ed
|
||||
|
||||
public:
|
||||
std::vector<Document> documents{};
|
||||
std::vector<Command> commands{};
|
||||
std::map<std::string, std::size_t, std::less<>> recentFiles{};
|
||||
std::size_t recentFilesCounter{};
|
||||
std::vector<std::filesystem::path> autosaveFiles{};
|
||||
@@ -47,15 +59,15 @@ namespace anm2ed
|
||||
std::filesystem::path spritesheetDragDropPath{};
|
||||
bool isSpritesheetDragDrop{};
|
||||
|
||||
anm2::Layer editLayer{};
|
||||
Element editLayer{element_make(ElementType::LAYER_ELEMENT)};
|
||||
imgui::PopupHelper layerPropertiesPopup{
|
||||
imgui::PopupHelper(LABEL_MANAGER_LAYER_PROPERTIES, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
|
||||
anm2::Null editNull{};
|
||||
Element editNull{element_make(ElementType::NULL_ELEMENT)};
|
||||
imgui::PopupHelper nullPropertiesPopup{
|
||||
imgui::PopupHelper(LABEL_MANAGER_NULL_PROPERTIES, imgui::POPUP_SMALL_NO_HEIGHT)};
|
||||
|
||||
anm2::Spritesheet::Region makeRegion{};
|
||||
Element makeRegion{element_make(ElementType::REGION)};
|
||||
int makeRegionSpritesheetId{-1};
|
||||
bool isMakeRegionRequested{};
|
||||
|
||||
@@ -68,13 +80,15 @@ namespace anm2ed
|
||||
~Manager();
|
||||
|
||||
Document* get(int = -1);
|
||||
void command_push(Command);
|
||||
void commands_run();
|
||||
Document* open(const std::filesystem::path&, bool = false, bool = true);
|
||||
void new_(const std::filesystem::path&);
|
||||
void save(int, const std::filesystem::path& = {}, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true,
|
||||
bool save(int, const std::filesystem::path& = {}, Compatibility = Compatibility::ANM2ED, bool = false, bool = true,
|
||||
bool = true);
|
||||
void save(const std::filesystem::path& = {}, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true,
|
||||
bool save(const std::filesystem::path& = {}, Compatibility = Compatibility::ANM2ED, bool = false, bool = true,
|
||||
bool = true);
|
||||
void autosave(Document&, anm2::Compatibility = anm2::ANM2ED, bool = false, bool = true, bool = true);
|
||||
void autosave(Document&, Compatibility = Compatibility::ANM2ED, bool = false, bool = true, bool = true);
|
||||
void set(int);
|
||||
void close(int);
|
||||
void layer_properties_open(int = -1);
|
||||
|
||||
+3
-3
@@ -9,10 +9,10 @@
|
||||
#include <string>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "process_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "process.hpp"
|
||||
#include "sdl.hpp"
|
||||
#include "string_.hpp"
|
||||
#include "string.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
#include "file_.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ namespace anm2ed::resource::icon
|
||||
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M3 3C2.44772 3 2 3.44772 2 4V7H9.58579L12 4.58579L10.4142 3H3ZM14.4142 5L10.4142 9H2V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V6C22 5.44772 21.5523 5 21 5H14.4142Z"/></svg>
|
||||
)";
|
||||
|
||||
inline constexpr auto FOLDER_OPEN_DATA = R"(
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFF"><path d="M3 21C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H20C20.5523 5 21 5.44772 21 6V9H4V18.996L6 11H22.5L20.1894 20.2425C20.0781 20.6877 19.6781 21 19.2192 21H3Z"></path></svg>
|
||||
)";
|
||||
|
||||
inline constexpr auto CLOSE_DATA = R"(
|
||||
<svg viewBox="0 0 24 24" fill="#FFF" xmlns="http://www.w3.org/2000/svg"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"/></svg>
|
||||
)";
|
||||
@@ -173,6 +177,7 @@ namespace anm2ed::resource::icon
|
||||
X(NONE, NONE_DATA, SIZE_SMALL) \
|
||||
X(FILE, FILE_DATA, SIZE_NORMAL) \
|
||||
X(FOLDER, FOLDER_DATA, SIZE_NORMAL) \
|
||||
X(FOLDER_OPEN, FOLDER_OPEN_DATA, SIZE_NORMAL) \
|
||||
X(CLOSE, CLOSE_DATA, SIZE_NORMAL) \
|
||||
X(ROOT, ROOT_DATA, SIZE_NORMAL) \
|
||||
X(LAYER, LAYER_DATA, SIZE_NORMAL) \
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace anm2ed
|
||||
X(BASIC_ALPHA, "Alpha", "Alpha", "Прозрачность", "透明度", "불투명도") \
|
||||
X(BASIC_APPEND, "Append", "Anteponer", "Добавить к концу", "添加", "추가") \
|
||||
X(BASIC_AT_FRAME, "At Frame", "En Frame", "На кадре", "触发帧", "트리거될 프레임") \
|
||||
X(BASIC_BATCH, "Batch", "Lote", "Пакетный", "批量", "일괄") \
|
||||
X(BASIC_BEFORE, "Before", "Antes", "До", "前一帧", "전") \
|
||||
X(BASIC_BORDER, "Border", "Borde", "Границы", "边框", "경계선") \
|
||||
X(BASIC_CANCEL, "Cancel", "Cancelar", "Отмена", "取消", "취소") \
|
||||
@@ -56,6 +57,7 @@ namespace anm2ed
|
||||
X(BASIC_EVENT, "Event", "Evento", "Событие", "事件", "이벤트") \
|
||||
X(BASIC_FRAME, "Frame", "Frame", "Кадр", "帧", "프레임") \
|
||||
X(BASIC_FRAMES, "Frames", "Frames", "Кадры", "帧", "프레임") \
|
||||
X(BASIC_GROUP, "Group", "Grupo", "Группа", "组", "그룹") \
|
||||
X(BASIC_GRID, "Grid", "Cuadricula", "Сетка", "网格", "격자") \
|
||||
X(BASIC_ID, "ID", "ID", "ID", "ID", "ID") \
|
||||
X(BASIC_INDEX, "Index", "Indice", "Индекс", "下标", "인덱스") \
|
||||
@@ -88,6 +90,7 @@ namespace anm2ed
|
||||
X(BASIC_SAVE, "Save", "Guardar", "Сохранить", "保存", "저장") \
|
||||
X(BASIC_SCALE, "Scale", "Escalar", "Масштаб", "缩放", "크기") \
|
||||
X(BASIC_SET_FILE_PATH, "Set File Path", "Set File Path", "Set File Path", "Set File Path", "Set File Path") \
|
||||
X(BASIC_SINGLE, "Single", "Individual", "Один", "单个", "단일") \
|
||||
X(BASIC_SIZE, "Size", "Tamaño", "Размер", "大小", "비율") \
|
||||
X(BASIC_SOUND, "Sound", "Sonido", "Звук", "声音", "사운드") \
|
||||
X(BASIC_TRIM, "Trim", "Recortar contenido", "Обрезать по содержимому", "修剪", "내용으로 자르기") \
|
||||
@@ -121,6 +124,8 @@ namespace anm2ed
|
||||
X(EDIT_EXTEND_FRAME, "Extend Frame", "Extender Frame", "Удлиннить кадр", "延长帧时长", "프레임 확장") \
|
||||
X(EDIT_FIT_ANIMATION_LENGTH, "Fit Animation Length", "Encajar Largo de animacion", "Подогнать к длине анимации", "匹配动画时长", "애니메이션 길이 맞추기") \
|
||||
X(EDIT_FPS, "FPS", "FPS", "FPS", "每秒帧数(FPS)", "FPS") \
|
||||
X(EDIT_GROUP_ITEMS, "Group Item(s)", "Agrupar item(s)", "Сгруппировать элементы", "将项目分组", "항목 그룹화") \
|
||||
X(EDIT_TOGGLE_GROUP_EXPANDED, "Toggle Group Expanded", "Alternar grupo expandido", "Развернуть/свернуть группу", "展开/折叠组", "그룹 펼침 전환") \
|
||||
X(EDIT_FRAME_COLOR_OFFSET, "Frame Color Offset", "Offset de color de Frame", "Смещение цвета кадра", "帧颜色偏移", "프레임 색상 오프셋") \
|
||||
X(EDIT_FRAME_CROP, "Frame Crop", "Recorte de Frame", "Обрезка кадра", "帧裁剪", "프레임 자르기") \
|
||||
X(EDIT_FRAME_DURATION, "Frame Duration", "Duracion de Frame", "Продолжительность кадра", "帧时长", "프레임 유지 시간") \
|
||||
@@ -144,6 +149,7 @@ namespace anm2ed
|
||||
X(EDIT_PACK_SPRITESHEET, "Pack Spritesheet", "Empaquetar spritesheet", "Упаковать спрайт-лист", "打包图集", "스프라이트 시트 패킹") \
|
||||
X(EDIT_MOVE_ANIMATIONS, "Move Animation(s)", "Mover Animacion(es)", "Переместить анимации", "移动动画", "애니메이션 이동") \
|
||||
X(EDIT_MOVE_FRAMES, "Move Frame(s)", "Mover Frame(s)", "Перемесить кадры", "移动多个/单个帧", "프레임 이동") \
|
||||
X(EDIT_MOVE_ITEMS, "Move Item(s)", "Mover item(s)", "Переместить элементы", "移动项目", "항목 이동") \
|
||||
X(EDIT_MOVE_REGIONS, "Move Regions", "Mover regiones", "Переместить регионы", "移动区域", "영역 이동") \
|
||||
X(EDIT_MOVE_LAYER_ANIMATION, "Move Layer Animation", "Mover Animacion de Capa", "Переместить анимацию слоя", "移动动画层", "레이어 애니메이션 이동") \
|
||||
X(EDIT_PASTE_ANIMATIONS, "Paste Animation(s)", "Pegar Animacion(es)", "Вставить анимации", "粘贴动画", "애니메이션 붙여넣기") \
|
||||
@@ -164,6 +170,7 @@ namespace anm2ed
|
||||
X(EDIT_REMOVE_UNUSED_NULLS, "Remove Unused Nulls", "Remover Nulls No Utilizados", "Удалить неизпользуемые нули", "删除未使用的Null", "미사용 Null 제거") \
|
||||
X(EDIT_REMOVE_UNUSED_SOUNDS, "Remove Unused Sounds", "Remover Sonidos No Utilizados", "Удалить неизпользуемые звуки", "删除未使用的声音", "미사용 사운드 제거") \
|
||||
X(EDIT_REMOVE_UNUSED_SPRITESHEETS, "Remove Unused Spritesheets", "Remover Spritesheets No Utilizadas", "Удалить неизпользуемые спрайт-листы", "删除未使用的图集", "미사용 스프라이트 시트 제거") \
|
||||
X(EDIT_RENAME_GROUP, "Rename Group", "Renombrar grupo", "Переименовать группу", "重命名组", "그룹 이름 바꾸기") \
|
||||
X(EDIT_RENAME_EVENT, "Rename Event", "Renombrar Evento", "Переименовать событие", "重命名事件", "이벤트 이름 바꾸기") \
|
||||
X(EDIT_REPLACE_SPRITESHEET, "Replace Spritesheet", "Reemplazar Spritesheet", "Заменить спрайт-лист", "替换图集", "스프라이트 시트 교체") \
|
||||
X(EDIT_REPLACE_SOUND, "Replace Sound", "Reemplazar Sonido", "Заменить звук", "替换声音", "사운드 교체") \
|
||||
@@ -193,6 +200,7 @@ namespace anm2ed
|
||||
X(FORMAT_SPRITESHEET_ID, "Spritesheet ID: {0}", "ID de Spritesheet: {0}", "", "图集 ID: {0}", "스프라이트 시트 ID: {0}") \
|
||||
X(FORMAT_INDEX, "Index: {0}", "Indice: {0}", "Индекс: {0}", "下标: {0}", "인덱스: {0}") \
|
||||
X(FORMAT_INTERPOLATED, "Interpolation: {0}", "Interpolación: {0}", "Интерполяция: {0}", "插值: {0}", "보간: {0}") \
|
||||
X(FORMAT_ITEMS_COUNT, "Items: {0}", "Items: {0}", "Предметы: {0}", "物品: {0}", "항목: {0}") \
|
||||
X(FORMAT_LAYER, "#{0} {1} (Spritesheet: #{2})", "#{0} {1} (Spritesheet: #{2})", "#{0} {1} (Спрайт-лист: #{2})", "#{0} {1} (图集: #{2})", "#{0} {1} (스프라이트 시트: #{2})") \
|
||||
X(FORMAT_LENGTH, "Length: {0}", "Largo: {0}", "Длина: {0}", "长度: {0}", "길이: {0}") \
|
||||
X(FORMAT_LOOP, "Loop: {0}", "Loop: {0}", "Цикл: {0}", "循环: {0}", "반복: {0}") \
|
||||
@@ -302,10 +310,12 @@ namespace anm2ed
|
||||
X(LABEL_ANM2ED, "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed", "Anm2Ed") \
|
||||
X(LABEL_ANM2ED_LIMITED, "Anm2Ed Limited", "Anm2Ed Limitado", "Anm2Ed Ограниченный", "Anm2Ed 限制版", "Anm2Ed 제한") \
|
||||
X(LABEL_DESTINATION, "Destination", "Destino", "Назначение", "目标", "대상") \
|
||||
X(LABEL_ITEMS, "Items", "Items", "Предметы", "物品", "항목") \
|
||||
X(LABEL_LOCALIZATION, "Localization", "Localizacion", "Локализация", "本地化", "현지화") \
|
||||
X(LABEL_MAKE_REGION, "Make Region", "Crear Región", "Создать регион", "创建区域", "영역 만들기") \
|
||||
X(LABEL_LOOP, "Loop", "Loop", "Цикл", "循环", "반복") \
|
||||
X(LABEL_MANAGER_ANM2_DRAG_DROP, "Anm2 Drag Drop", "Arrastrar y Soltar Anm2", "Anm2 Drag Drop", "Anm2 拖放", "Anm2 드래그 앤 드롭") \
|
||||
X(LABEL_GROUP_PROPERTIES, "Group Properties", "Propiedades de grupo", "Свойства группы", "组属性", "그룹 속성") \
|
||||
X(LABEL_REGION_PROPERTIES, "Region Properties", "Propiedades de región", "Свойства региона", "区域属性", "영역 속성") \
|
||||
X(LABEL_REGION_PROPERTIES_ORIGIN, "Origin", "Origen", "Точка отсчета", "原点", "원점") \
|
||||
X(LABEL_REGION_ORIGIN_TOP_LEFT, "Top Left", "Superior izquierda", "Верхний левый", "左上", "왼쪽 위") \
|
||||
@@ -390,7 +400,6 @@ namespace anm2ed
|
||||
X(LABEL_TRANSPARENT, "Transparent", "Transparentes", "Прозрачный", "透明", "투명도") \
|
||||
X(LABEL_TYPE, "Type", "Tipo", "Тип", "类型", "유형") \
|
||||
X(LABEL_UI_SCALE, "UI Scale", "Escala de la Interfaz", "Размер UI", "界面缩放", "UI 비율") \
|
||||
X(LABEL_ITEM_HEIGHT, "Item Height", "Altura del Item", "Высота элемента", "项目高度", "항목 높이") \
|
||||
X(LABEL_USE_DEFAULT_SETTINGS, "Use Default Settings", "Usar Opciones Predeterminadas", "Изпользовать настройки по умолчанию", "使用默认设置", "기본값으로 사용") \
|
||||
X(LABEL_VALUE_COLUMN, "Value", "Valor", "Значение", "数值", "값") \
|
||||
X(LABEL_VSYNC, "Vsync", "Vsync", "Вертикальная синхронизация (V-sync)", "垂直同步", "수직 동기화") \
|
||||
@@ -418,6 +427,7 @@ namespace anm2ed
|
||||
X(SHORTCUT_STRING_EXIT, "Exit", "Salir", "Выйти", "退出", "종료") \
|
||||
X(SHORTCUT_STRING_EXTEND_FRAME, "Extend Frame", "Extender Frame", "Удлиннить кадр", "延长帧", "프레임 확장") \
|
||||
X(SHORTCUT_STRING_FIT, "Fit", "Encajar", "Подогнать", "匹配", "맞추기") \
|
||||
X(SHORTCUT_STRING_GROUP, "Group", "Grupo", "Группа", "组", "그룹") \
|
||||
X(SHORTCUT_STRING_INSERT_FRAME, "Insert Frame", "Insertar Frame", "Вставить кадр", "插入帧", "프레임 삽입") \
|
||||
X(SHORTCUT_STRING_MERGE, "Merge", "Combinar", "Соединить", "合并", "병합") \
|
||||
X(SHORTCUT_STRING_MOVE, "Move", "Mover", "Передвинуть", "移动", "이동") \
|
||||
@@ -457,6 +467,7 @@ namespace anm2ed
|
||||
X(TEXT_TOOL_SPRITESHEET_EDITOR, "This tool can only be used in Spritesheet Editor!", "¡Esta herramienta solo se puede usar en el Editor de spritesheets!", "Этот инструмент можно использовать только в \"Редакторе спрайт-листов\"!", "该工具只能在“图集编辑器”中使用!", "이 도구는 스프라이트 시트 편집기에서만 사용할 수 있습니다!") \
|
||||
X(TEXT_NEW_ANIMATION, "New Animation", "Nueva Animacion", "Новая анимация", "新动画", "새 애니메이션") \
|
||||
X(TEXT_NEW_EVENT, "New Event", "Nuevo Evento", "Новое событие", "新事件", "새 이벤트") \
|
||||
X(TEXT_NEW_GROUP, "New Group", "Nuevo grupo", "Новая группа", "新组", "새 그룹") \
|
||||
X(TEXT_NEW_REGION, "New Region", "Nueva Región", "Новый регион", "新区域", "새 영역") \
|
||||
X(TEXT_REGION_IN_USE, "A spritesheet region is in use; remove region to edit.", "Se está usando una región del spritesheet; elimina la región para editar.", "Регион спрайт-листа используется; удалите регион для редактирования.", "图集中有区域正在使用;移除区域后才能编辑。", "스프라이트 시트 영역이 사용 중입니다. 편집하려면 영역을 제거하세요.") \
|
||||
X(TEXT_RECORDING_PROGRESS, "Once recording is complete, rendering may take some time.\nPlease be patient...", "Una vez que el grabado este completo, renderizar puede tomar algo de tiempo. \nPor favor se paciente...", "Когда запись завершена, рендеринг может занять некоторое время.\nПожалуйста потерпите...", "录制完成时,渲染可能会花一些时间.\n请耐心等待...", "녹화가 완료되면 렌더링에 시간이 걸릴 수 있습니다.\n잠시만 기다려 주세요...") \
|
||||
@@ -535,6 +546,7 @@ namespace anm2ed
|
||||
X(TOOLTIP_CANCEL_ADD_ITEM, "Cancel adding an item.", "Cancelar añadir un item.", "Отменить добавление предмета.", "取消添加物品.", "항목 추가를 취소합니다.") \
|
||||
X(TOOLTIP_CANCEL_BAKE_FRAMES, "Cancel baking frames.", "Cancelar hacer bake de Frames.", "Отменить запечку кадров.", "取消提前渲染.", "프레임 베이킹을 취소합니다.") \
|
||||
X(TOOLTIP_CHANGE_ALL_DESTINATION_FRAMES, "Apply specified frame properties to selected frames.", "Aplica las propiedades de frame especificadas a los frames seleccionados.", "Применить указанные свойства кадра к выбранным кадрам.", "将指定的帧属性应用到所选帧。", "지정한 프레임 속성을 선택한 프레임에 적용합니다.") \
|
||||
X(TOOLTIP_CHANGE_ALL_DESTINATION_ITEMS, "Apply specified frame properties to all frames in the selected items.", "Aplica las propiedades de frame especificadas a todos los frames de los items seleccionados.", "Применить указанные свойства кадра ко всем кадрам выбранных предметов.", "将指定的帧属性应用到所选物品中的所有帧。", "선택한 항목의 모든 프레임에 지정한 프레임 속성을 적용합니다.") \
|
||||
X(TOOLTIP_CHANGE_ALL_DESTINATION_ANIMATIONS, "Apply specified frame properties to the specified items in the selected animations.", "Aplica las propiedades de frame especificadas a los items indicados en las animaciones seleccionadas.", "Применить указанные свойства кадра к указанным элементам в выбранных анимациях.", "将指定的帧属性应用到所选动画中的指定项目。", "지정한 프레임 속성을 선택한 애니메이션의 지정 항목에 적용합니다.") \
|
||||
X(TOOLTIP_CHANGE_ALL_ROOT, "The frame property changes will apply to root frames.", "Los cambios de propiedades de frame se aplicaran a los frames root.", "Изменения свойств кадра будут применены к корневым кадрам.", "帧属性更改将应用于根帧。", "프레임 속성 변경 사항을 Root 프레임에 적용합니다.") \
|
||||
X(TOOLTIP_CHANGE_ALL_LAYERS, "The frame property changes will apply to layer frames.", "Los cambios de propiedades de frame se aplicaran a los frames de capa.", "Изменения свойств кадра будут применены к кадрам слоев.", "帧属性更改将应用于图层帧。", "프레임 속성 변경 사항을 레이어 프레임에 적용합니다.") \
|
||||
@@ -681,7 +693,6 @@ namespace anm2ed
|
||||
X(TOOLTIP_REMOVE_TRIGGER_SOUND, "Remove the last trigger sound.", "Remover el último sonido del trigger.", "Удалить последний звук триггера.", "移除最后一个事件触发器声音.", "마지막 트리거 사운드를 제거합니다.") \
|
||||
X(TOOLTIP_TRIGGER_VISIBILITY, "Toggle the trigger's visibility.", "Alterna la visibilidad del trigger.", "Переключить видимость триггера.", "切换触发器是否可见.", "트리거를 표시하거나 숨깁니다.") \
|
||||
X(TOOLTIP_UI_SCALE, "Change the scale of the UI.", "Cambia la escala de la interfaz de usuario.", "Изменить масштаб пользовательского интерфейса.", "更改界面(UI)的缩放.", "UI 비율을 변경합니다.") \
|
||||
X(TOOLTIP_ITEM_HEIGHT, "Set the height of items in Timeline.", "Establece la altura de los items en la Línea de tiempo.", "Установить высоту элементов на таймлайне.", "设置时间轴中项目的高度.", "타임라인 항목의 높이를 설정합니다.") \
|
||||
X(TOOLTIP_UNUSED_ITEMS_HIDDEN, "Unused layers/nulls are hidden. Press to show them.", "Las capas/nulls no utilizados estan ocultos. Presiona para hacerlos visibles", "Неиспользуемые слои/нули скрыты. Нажмите, чтобы их показать.", "正在隐藏未使用的动画层/Null. 点击以显示它们.", "사용되지 않는 레이어/Null이 숨겨져 있습니다. 표시하려면 누르세요.") \
|
||||
X(TOOLTIP_UNUSED_ITEMS_SHOWN, "Unused layers/nulls are shown. Press to hide them.", "Las capas/nulls no utilizados estan visibles. Presiona para ocultarlos", "Неиспользуемые слои/нули видимы. Нажмите, чтобы их скрыть.", "正在显示未使用的动画层/Null. 点击以隐藏它们.", "사용되지 않는 레이어/Null이 표시되어 있습니다. 숨기려면 누르세요.") \
|
||||
X(TOOLTIP_USE_DEFAULT_SETTINGS, "Reset the settings to their defaults.", "Reinicia las configuraciones a sus predeterminados.", "Сбросить настройки на настройки по умолчанию.", "重设所有设置为默认.", "설정을 기본값으로 재설정합니다.") \
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
#include "file_.hpp"
|
||||
#include "math_.hpp"
|
||||
#include "file.hpp"
|
||||
#include "math.hpp"
|
||||
|
||||
using namespace anm2ed::resource::texture;
|
||||
using namespace anm2ed::util::math;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
#include <sstream>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
|
||||
using namespace anm2ed::util;
|
||||
using namespace glm;
|
||||
|
||||
+13
-7
@@ -5,7 +5,7 @@
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "anm2/anm2_type.hpp"
|
||||
#include "anm2/anm2.hpp"
|
||||
#include "origin.hpp"
|
||||
#include "render.hpp"
|
||||
#include "strings.hpp"
|
||||
@@ -25,6 +25,10 @@ namespace anm2ed
|
||||
constexpr auto OUTPUT_PATH_DEFAULT = "./output.gif";
|
||||
#endif
|
||||
|
||||
constexpr auto UI_SCALE_MIN = 0.5f;
|
||||
constexpr auto UI_SCALE_MAX = 2.0f;
|
||||
constexpr auto UI_SCALE_STEP = 0.25f;
|
||||
|
||||
#define SETTINGS_TYPES \
|
||||
X(INT, int) \
|
||||
X(BOOL, bool) \
|
||||
@@ -55,16 +59,17 @@ namespace anm2ed
|
||||
X(WINDOW_POSITION, windowPosition, STRING_UNDEFINED, IVEC2, glm::ivec2()) \
|
||||
X(IS_VSYNC, isVsync, STRING_UNDEFINED, BOOL, true) \
|
||||
X(UI_SCALE, uiScale, STRING_UNDEFINED, FLOAT, 1.0f) \
|
||||
X(TIMELINE_ITEM_HEIGHT, timelineItemHeight, STRING_UNDEFINED, FLOAT, 1.0f) \
|
||||
X(THEME, theme, STRING_UNDEFINED, INT, types::theme::DARK) \
|
||||
X(LANGUAGE, language, STRING_UNDEFINED, INT, ENGLISH) \
|
||||
\
|
||||
X(FILE_IS_AUTOSAVE, fileIsAutosave, STRING_UNDEFINED, BOOL, true) \
|
||||
X(FILE_IS_WARN_OVERWRITE, fileIsWarnOverwrite, STRING_UNDEFINED, BOOL, true) \
|
||||
X(FILE_IS_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE_REMINDER, fileIsSpecialInterpolatedFramesOnSaveReminder, STRING_UNDEFINED, BOOL, true) \
|
||||
X(FILE_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, fileBakeSpecialInterpolatedFramesOnSave, STRING_UNDEFINED, BOOL, true) \
|
||||
X(FILE_IS_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE_REMINDER, fileIsSpecialInterpolatedFramesOnSaveReminder, \
|
||||
STRING_UNDEFINED, BOOL, true) \
|
||||
X(FILE_BAKE_SPECIAL_INTERPOLATED_FRAMES_ON_SAVE, fileBakeSpecialInterpolatedFramesOnSave, STRING_UNDEFINED, BOOL, \
|
||||
true) \
|
||||
X(FILE_SNAPSHOT_STACK_SIZE, fileSnapshotStackSize, STRING_UNDEFINED, INT, 50) \
|
||||
X(FILE_COMPATIBILITY, fileCompatibility, STRING_UNDEFINED, INT, anm2::ANM2ED) \
|
||||
X(FILE_COMPATIBILITY, fileCompatibility, STRING_UNDEFINED, INT, ANM2ED) \
|
||||
\
|
||||
X(KEYBOARD_REPEAT_DELAY, keyboardRepeatDelay, STRING_UNDEFINED, FLOAT, 0.300f) \
|
||||
X(KEYBOARD_REPEAT_RATE, keyboardRepeatRate, STRING_UNDEFINED, FLOAT, 0.050f) \
|
||||
@@ -161,7 +166,7 @@ namespace anm2ed
|
||||
\
|
||||
X(MERGE_TYPE, mergeType, STRING_UNDEFINED, INT, 0) \
|
||||
X(MERGE_IS_DELETE_ANIMATIONS_AFTER, mergeIsDeleteAnimationsAfter, STRING_UNDEFINED, BOOL, false) \
|
||||
X(MERGE_SPRITESHEETS_ORIGIN, mergeSpritesheetsOrigin, STRING_UNDEFINED, INT, anm2::APPEND_RIGHT) \
|
||||
X(MERGE_SPRITESHEETS_ORIGIN, mergeSpritesheetsOrigin, STRING_UNDEFINED, INT, APPEND_RIGHT) \
|
||||
X(MERGE_SPRITESHEETS_IS_MAKE_REGIONS, mergeSpritesheetsIsMakeRegions, STRING_UNDEFINED, BOOL, true) \
|
||||
X(MERGE_SPRITESHEETS_IS_MAKE_PRIMARY_REGION, mergeSpritesheetsIsMakePrimaryRegion, STRING_UNDEFINED, BOOL, true) \
|
||||
X(MERGE_SPRITESHEETS_REGION_ORIGIN, mergeSpritesheetsRegionOrigin, STRING_UNDEFINED, INT, origin::TOP_LEFT) \
|
||||
@@ -171,7 +176,7 @@ namespace anm2ed
|
||||
X(BAKE_IS_ROUND_SCALE, bakeIsRoundScale, STRING_UNDEFINED, BOOL, true) \
|
||||
X(BAKE_IS_ROUND_ROTATION, bakeIsRoundRotation, STRING_UNDEFINED, BOOL, true) \
|
||||
\
|
||||
X(TIMELINE_ADD_ITEM_TYPE, timelineAddItemType, STRING_UNDEFINED, INT, anm2::LAYER) \
|
||||
X(TIMELINE_ADD_ITEM_TYPE, timelineAddItemType, STRING_UNDEFINED, INT, LAYER) \
|
||||
X(TIMELINE_ADD_ITEM_DESTINATION, timelineAddItemDestination, STRING_UNDEFINED, INT, types::destination::ALL) \
|
||||
X(TIMELINE_ADD_ITEM_SOURCE, timelineAddItemSource, STRING_UNDEFINED, INT, types::source::NEW) \
|
||||
X(TIMELINE_IS_SHOW_UNUSED, timelineIsShowUnused, STRING_UNDEFINED, BOOL, true) \
|
||||
@@ -218,6 +223,7 @@ namespace anm2ed
|
||||
X(SHORTCUT_RENAME, shortcutRename, SHORTCUT_STRING_RENAME, STRING, "F2") \
|
||||
X(SHORTCUT_DEFAULT, shortcutDefault, SHORTCUT_STRING_DEFAULT, STRING, "Home") \
|
||||
X(SHORTCUT_MERGE, shortcutMerge, SHORTCUT_STRING_MERGE, STRING, "Ctrl+E") \
|
||||
X(SHORTCUT_GROUP, shortcutGroup, SHORTCUT_STRING_GROUP, STRING, "Ctrl+G") \
|
||||
X(SHORTCUT_CONFIRM, shortcutConfirm, SHORTCUT_STRING_CONFIRM, STRING, "Enter") \
|
||||
X(SHORTCUT_CANCEL, shortcutCancel, SHORTCUT_STRING_CANCEL, STRING, "Escape") \
|
||||
/* Tools */ \
|
||||
|
||||
+7
-2
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include "anm2/anm2.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "playback.hpp"
|
||||
#include "storage.hpp"
|
||||
#include "texture.hpp"
|
||||
|
||||
namespace anm2ed::snapshots
|
||||
{
|
||||
@@ -29,8 +32,10 @@ namespace anm2ed
|
||||
Storage region{};
|
||||
Storage sound{};
|
||||
Storage spritesheet{};
|
||||
anm2::Anm2 anm2{};
|
||||
anm2::Reference reference{};
|
||||
std::map<int, resource::Texture> textures{};
|
||||
std::map<int, resource::Audio> sounds{};
|
||||
Anm2 anm2{};
|
||||
Reference reference{};
|
||||
float frameTime{};
|
||||
std::string message = snapshots::ACTION;
|
||||
};
|
||||
|
||||
+67
-10
@@ -8,9 +8,10 @@
|
||||
#include <imgui/backends/imgui_impl_sdl3.h>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "path_.hpp"
|
||||
#include "path.hpp"
|
||||
#include "strings.hpp"
|
||||
#include "toast.hpp"
|
||||
#include "util/imgui/shortcut.hpp"
|
||||
|
||||
using namespace anm2ed::imgui;
|
||||
using namespace anm2ed::util;
|
||||
@@ -66,6 +67,10 @@ namespace anm2ed
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
switch (event.type)
|
||||
{
|
||||
case SDL_EVENT_DROP_BEGIN:
|
||||
spritesheetDropPaths.clear();
|
||||
soundDropPaths.clear();
|
||||
break;
|
||||
case SDL_EVENT_DROP_FILE:
|
||||
{
|
||||
std::string droppedFile = event.drop.data ? event.drop.data : "";
|
||||
@@ -86,24 +91,46 @@ namespace anm2ed
|
||||
}
|
||||
else if (path::is_extension(droppedPath, "png"))
|
||||
{
|
||||
if (auto document = manager.get())
|
||||
document->spritesheet_add(droppedPath);
|
||||
else
|
||||
spritesheetDropPaths.push_back(droppedPath);
|
||||
}
|
||||
else if (path::is_extension(droppedPath, "wav") || path::is_extension(droppedPath, "ogg"))
|
||||
{
|
||||
soundDropPaths.push_back(droppedPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_DROP_COMPLETE:
|
||||
{
|
||||
if (auto document = manager.get(); document)
|
||||
{
|
||||
if (!spritesheetDropPaths.empty())
|
||||
{
|
||||
auto paths = spritesheetDropPaths;
|
||||
manager.command_push({manager.selected, [paths](Manager&, Document& document)
|
||||
{ document.spritesheets_add(paths); }});
|
||||
}
|
||||
if (!soundDropPaths.empty())
|
||||
{
|
||||
auto paths = soundDropPaths;
|
||||
manager.command_push({manager.selected, [paths](Manager&, Document& document)
|
||||
{ document.sounds_add(paths); }});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!spritesheetDropPaths.empty())
|
||||
{
|
||||
toasts.push(localize.get(TOAST_ADD_SPRITESHEET_FAILED));
|
||||
logger.warning(localize.get(TOAST_ADD_SPRITESHEET_FAILED, anm2ed::ENGLISH));
|
||||
}
|
||||
}
|
||||
else if (path::is_extension(droppedPath, "wav") || path::is_extension(droppedPath, "ogg"))
|
||||
{
|
||||
if (auto document = manager.get())
|
||||
document->sound_add(droppedPath);
|
||||
else
|
||||
if (!soundDropPaths.empty())
|
||||
{
|
||||
toasts.push(localize.get(TOAST_ADD_SOUND_FAILED));
|
||||
logger.warning(localize.get(TOAST_ADD_SOUND_FAILED, anm2ed::ENGLISH));
|
||||
}
|
||||
}
|
||||
spritesheetDropPaths.clear();
|
||||
soundDropPaths.clear();
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_USER: // Opening files
|
||||
@@ -131,10 +158,40 @@ namespace anm2ed
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
if (ImGui::GetTopMostPopupModal() == nullptr)
|
||||
{
|
||||
constexpr ImGuiKey DOCUMENT_SHORTCUT_KEYS[] = {ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5,
|
||||
ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9, ImGuiKey_0};
|
||||
for (int i = 0; i < (int)(sizeof(DOCUMENT_SHORTCUT_KEYS) / sizeof(DOCUMENT_SHORTCUT_KEYS[0])); ++i)
|
||||
{
|
||||
if (i >= (int)manager.documents.size()) break;
|
||||
if (ImGui::Shortcut(ImGuiMod_Ctrl | DOCUMENT_SHORTCUT_KEYS[i], ImGuiInputFlags_RouteGlobal))
|
||||
{
|
||||
manager.set(i);
|
||||
manager.pendingSelected = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
taskbar.update(manager, settings, resources, dialog, isQuitting);
|
||||
documents.update(taskbar, manager, settings, resources, isQuitting);
|
||||
dockspace.update(taskbar, documents, manager, settings, resources, dialog, clipboard);
|
||||
|
||||
if (!dockspace.is_canvas_focused_get())
|
||||
{
|
||||
float uiScaleDelta{};
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL)) uiScaleDelta += UI_SCALE_STEP;
|
||||
if (imgui::shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL)) uiScaleDelta -= UI_SCALE_STEP;
|
||||
if (uiScaleDelta != 0.0f)
|
||||
{
|
||||
settings.uiScale = std::clamp(settings.uiScale + uiScaleDelta, UI_SCALE_MIN, UI_SCALE_MAX);
|
||||
ImGui::GetStyle().FontScaleMain = settings.uiScale;
|
||||
}
|
||||
}
|
||||
|
||||
toasts.update();
|
||||
manager.commands_run();
|
||||
|
||||
SDL_GetWindowSize(window, &settings.windowSize.x, &settings.windowSize.y);
|
||||
SDL_GetWindowPosition(window, &settings.windowPosition.x, &settings.windowPosition.y);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "dockspace.hpp"
|
||||
|
||||
namespace anm2ed
|
||||
@@ -28,6 +31,8 @@ namespace anm2ed
|
||||
uint64_t previousUpdate{};
|
||||
double tickAccumulatorMs{};
|
||||
bool wasRecording{};
|
||||
std::vector<std::filesystem::path> spritesheetDropPaths{};
|
||||
std::vector<std::filesystem::path> soundDropPaths{};
|
||||
|
||||
State(SDL_Window*&, Settings& settings, std::vector<std::string>&);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user