context menus, document refactoring, fixes

This commit is contained in:
2025-10-26 00:10:44 -04:00
parent 87c2db2a77
commit fe9366f9ef
62 changed files with 2138 additions and 793 deletions

View File

@@ -7,7 +7,7 @@
#include "tool.h"
#include "types.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::manager;
using namespace anm2ed::settings;
using namespace anm2ed::canvas;
using namespace anm2ed::playback;
@@ -17,6 +17,7 @@ using namespace glm;
namespace anm2ed::animation_preview
{
constexpr auto NULL_COLOR = vec4(0.0f, 0.0f, 1.0f, 0.90f);
constexpr auto TARGET_SIZE = vec2(32, 32);
constexpr auto PIVOT_SIZE = vec2(8, 8);
constexpr auto POINT_SIZE = vec2(4, 4);
@@ -27,10 +28,11 @@ namespace anm2ed::animation_preview
{
}
void AnimationPreview::update(DocumentManager& manager, Settings& settings, Resources& resources, Playback& playback)
void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& playback = document.playback;
auto& reference = document.reference;
auto animation = document.animation_get();
auto& pan = document.previewPan;
@@ -143,12 +145,10 @@ namespace anm2ed::animation_preview
if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor);
if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor);
auto frameTime = reference.frameTime > -1 && !playback.isPlaying ? reference.frameTime : playback.time;
if (animation)
auto render = [&](float time, vec3 colorOffset = {}, float alphaOffset = {}, bool isOnionskin = false)
{
auto transform = transform_get(zoom, pan);
auto root = animation->rootAnimation.frame_generate(playback.time, anm2::ROOT);
auto root = animation->rootAnimation.frame_generate(time, anm2::ROOT);
if (isRootTransform)
transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation);
@@ -158,7 +158,9 @@ namespace anm2ed::animation_preview
auto rootTransform = transform * math::quad_model_get(TARGET_SIZE, root.position, TARGET_SIZE * 0.5f,
math::percent_to_unit(root.scale), root.rotation);
texture_render(shaderTexture, resources.icons[icon::TARGET].id, rootTransform, color::GREEN);
vec4 color = isOnionskin ? vec4(colorOffset, alphaOffset) : color::GREEN;
texture_render(shaderTexture, resources.icons[icon::TARGET].id, rootTransform, color);
}
for (auto& id : animation->layerOrder)
@@ -168,7 +170,7 @@ namespace anm2ed::animation_preview
auto& layer = anm2.content.layers.at(id);
if (auto frame = layerAnimation.frame_generate(frameTime, anm2::LAYER); frame.isVisible)
if (auto frame = layerAnimation.frame_generate(time, anm2::LAYER); frame.isVisible)
{
auto spritesheet = anm2.spritesheet_get(layer.spritesheetID);
if (!spritesheet) continue;
@@ -183,17 +185,23 @@ namespace anm2ed::animation_preview
auto uvMin = frame.crop / vec2(texture.size) + inset;
auto uvMax = (frame.crop + frame.size) / vec2(texture.size) - inset;
auto vertices = math::uv_vertices_get(uvMin, uvMax);
vec3 frameColorOffset = frame.offset + colorOffset;
vec4 frameTint = frame.tint;
frameTint.a = std::max(0.0f, frameTint.a - alphaOffset);
texture_render(shaderTexture, texture.id, layerTransform, frame.tint, frame.offset, vertices.data());
texture_render(shaderTexture, texture.id, layerTransform, frameTint, frameColorOffset, vertices.data());
if (isBorder) rect_render(shaderLine, layerTransform, color::RED);
auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : color::RED;
if (isBorder) rect_render(shaderLine, layerTransform, color);
if (isPivots)
{
auto pivotTransform =
transform * math::quad_model_get(PIVOT_SIZE, frame.position, PIVOT_SIZE * 0.5f,
math::percent_to_unit(frame.scale), frame.rotation);
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color::RED);
texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color);
}
}
}
@@ -206,11 +214,13 @@ namespace anm2ed::animation_preview
auto& isShowRect = anm2.content.nulls[id].isShowRect;
if (auto frame = nullAnimation.frame_generate(frameTime, anm2::NULL_); frame.isVisible)
if (auto frame = nullAnimation.frame_generate(time, anm2::NULL_); frame.isVisible)
{
auto icon = isShowRect ? icon::POINT : icon::TARGET;
auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE;
auto& color = id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED : color::BLUE;
auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset)
: id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED
: NULL_COLOR;
auto nullTransform = transform * math::quad_model_get(size, frame.position, size * 0.5f,
math::percent_to_unit(frame.scale), frame.rotation);
@@ -228,6 +238,34 @@ namespace anm2ed::animation_preview
}
}
}
};
auto onionskin_render = [&](float time, int count, int direction, vec3 color)
{
for (int i = 1; i <= count; i++)
{
float useTime = time + (float)(direction * i);
float alphaOffset = (1.0f / (count + 1)) * i;
render(useTime, color, alphaOffset, true);
}
};
auto onionskins_render = [&](float time)
{
onionskin_render(time, settings.onionskinBeforeCount, -1, settings.onionskinBeforeColor);
onionskin_render(time, settings.onionskinAfterCount, 1, settings.onionskinAfterColor);
};
auto frameTime = reference.frameTime > -1 && !playback.isPlaying ? reference.frameTime : playback.time;
if (animation)
{
auto& drawOrder = settings.onionskinDrawOrder;
auto& isEnabled = settings.onionskinIsEnabled;
if (drawOrder == draw_order::BELOW && isEnabled) onionskins_render(frameTime);
render(frameTime);
if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime);
}
unbind();

View File

@@ -1,8 +1,7 @@
#pragma once
#include "canvas.h"
#include "document_manager.h"
#include "playback.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -15,7 +14,6 @@ namespace anm2ed::animation_preview
public:
AnimationPreview();
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback);
void update(manager::Manager&, settings::Settings&, resources::Resources&);
};
}
}

View File

@@ -1,21 +1,24 @@
#include "animations.h"
#include <algorithm>
#include <ranges>
using namespace anm2ed::document;
using namespace anm2ed::clipboard;
using namespace anm2ed::manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
namespace anm2ed::animations
{
void Animations::update(Document& document, int& documentIndex, Settings& settings, Resources& resources)
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.selectedAnimations;
storage.user_data_set(&selection);
auto& hovered = document.hoveredAnimation;
auto& multiSelect = document.animationMultiSelect;
auto& mergeMultiSelect = document.animationMergeMultiSelect;
auto& mergeTarget = document.mergeTarget;
if (ImGui::Begin("Animations", &settings.windowIsAnimations))
{
@@ -23,7 +26,7 @@ namespace anm2ed::animations
if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders))
{
storage.begin(anm2.animations.items.size());
multiSelect.start(anm2.animations.items.size());
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
{
@@ -31,7 +34,6 @@ namespace anm2ed::animations
auto isDefault = anm2.animations.defaultAnimation == animation.name;
auto isReferenced = reference.animationIndex == i;
auto isSelected = selection.contains(i);
auto font = isDefault && isReferenced ? font::BOLD_ITALICS
: isDefault ? font::BOLD
@@ -41,9 +43,10 @@ namespace anm2ed::animations
ImGui::PushFont(resources.fonts[font].get(), font::SIZE);
ImGui::SetNextItemSelectionUserData(i);
if (imgui::selectable_input_text(animation.name,
std::format("###Document #{} Animation #{}", documentIndex, i),
animation.name, isSelected))
if (!isReferenced) reference = {(int)i};
std::format("###Document #{} Animation #{}", manager.selected, i),
animation.name, multiSelect.contains(i)))
reference = {(int)i};
if (ImGui::IsItemHovered()) hovered = i;
ImGui::PopFont();
if (ImGui::BeginItemTooltip())
@@ -68,10 +71,11 @@ namespace anm2ed::animations
if (ImGui::BeginDragDropSource())
{
std::vector<int> sorted = {};
ImGui::SetDragDropPayload("Animation Drag Drop", sorted.data(), sorted.size() * sizeof(int));
for (auto& index : sorted)
ImGui::TextUnformatted(anm2.animations.items[index].name.c_str());
static std::vector<int> selection;
selection.assign(multiSelect.begin(), multiSelect.end());
ImGui::SetDragDropPayload("Animation Drag Drop", selection.data(), selection.size() * sizeof(int));
for (auto& i : selection)
ImGui::TextUnformatted(anm2.animations.items[i].name.c_str());
ImGui::EndDragDropSource();
}
@@ -79,16 +83,11 @@ namespace anm2ed::animations
{
if (auto payload = ImGui::AcceptDragDropPayload("Animation Drag Drop"))
{
auto count = payload->DataSize / sizeof(int);
auto data = (int*)(payload->Data);
std::vector<int> indices(data, data + count);
//std::vector<int> destinationIndices = vector::indices_move(anm2.animations.items, indices, i);
selection.clear();
/*
for (const auto& index : destinationIndices)
selection.insert((int)index);
*/
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.animations_move(indices, i);
}
ImGui::EndDragDropTarget();
}
@@ -96,63 +95,90 @@ namespace anm2ed::animations
ImGui::PopID();
}
storage.end();
multiSelect.finish();
auto copy = [&]()
{
if (!multiSelect.empty())
{
std::string clipboardText{};
for (auto& i : multiSelect)
clipboardText += anm2.animations.items[i].to_string();
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.animations.items[hovered].to_string());
};
auto cut = [&]()
{
copy();
if (!multiSelect.empty())
{
for (auto& i : multiSelect | std::views::reverse)
anm2.animations.items.erase(anm2.animations.items.begin() + i);
multiSelect.clear();
}
else if (hovered > -1)
{
anm2.animations.items.erase(anm2.animations.items.begin() + hovered);
hovered = -1;
}
};
auto paste = [&]()
{
auto clipboardText = clipboard.get();
document.animations_deserialize(clipboardText);
};
if (imgui::shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut();
if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste();
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::BeginDisabled(multiSelect.empty() && hovered == -1);
if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str())) cut();
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy();
ImGui::EndDisabled();
ImGui::BeginDisabled(clipboard.is_empty());
if (ImGui::MenuItem("Paste", settings.shortcutPaste.c_str())) paste();
ImGui::EndDisabled();
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(5);
imgui::shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize))
{
anm2::Animation 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 = selection.empty() ? (int)anm2.animations.items.size() - 1 : (int)std::ranges::max(selection) + 1;
anm2.animations.items.insert(anm2.animations.items.begin() + index, animation);
selection = {index};
reference = {index};
document.change(change::ANIMATIONS);
}
if (ImGui::Button("Add", widgetSize)) document.animation_add();
imgui::set_item_tooltip_shortcut("Add a new animation.", settings.shortcutAdd);
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
ImGui::BeginDisabled(multiSelect.empty());
{
imgui::shortcut(settings.shortcutDuplicate);
if (ImGui::Button("Duplicate", widgetSize))
{
auto duplicated = selection;
auto duplicatedEnd = std::ranges::max(duplicated);
for (auto& id : duplicated)
{
anm2.animations.items.insert(anm2.animations.items.begin() + duplicatedEnd, anm2.animations.items[id]);
selection.insert(++duplicatedEnd);
selection.erase(id);
}
document.change(change::ANIMATIONS);
}
if (ImGui::Button("Duplicate", widgetSize)) document.animation_duplicate();
imgui::set_item_tooltip_shortcut("Duplicate the selected animation(s).", settings.shortcutDuplicate);
ImGui::SameLine();
ImGui::BeginDisabled(selection.size() != 1);
if (imgui::shortcut(settings.shortcutMerge, shortcut::FOCUSED))
if (multiSelect.size() > 0) document.animations_merge_quick();
ImGui::BeginDisabled(multiSelect.size() != 1);
{
if (ImGui::Button("Merge", widgetSize))
{
mergePopup.open();
mergeSelection.clear();
mergeTarget = *selection.begin();
mergeMultiSelect.clear();
mergeTarget = *multiSelect.begin();
}
}
ImGui::EndDisabled();
@@ -165,28 +191,16 @@ namespace anm2ed::animations
ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove);
if (ImGui::Button("Remove", widgetSize))
{
/*
auto selectionErase = set::to_size_t(selection);
if (selectionErase.contains(document.reference.animationIndex)) document.reference.animationIndex = -1;
vector::range_erase(anm2.animations.items, selectionErase);
*/
document.change(change::ANIMATIONS);
selection.clear();
}
if (ImGui::Button("Remove", widgetSize)) document.animation_remove();
imgui::set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate);
ImGui::SameLine();
ImGui::BeginDisabled(selection.size() != 1);
{
imgui::shortcut(settings.shortcutDefault);
if (ImGui::Button("Default", widgetSize))
anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name;
imgui::set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDuplicate);
}
imgui::shortcut(settings.shortcutDefault);
ImGui::BeginDisabled(multiSelect.size() != 1);
if (ImGui::Button("Default", widgetSize)) document.animation_default();
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDefault);
}
ImGui::EndDisabled();
@@ -196,15 +210,13 @@ namespace anm2ed::animations
{
auto merge_close = [&]()
{
mergeSelection.clear();
mergeMultiSelect.clear();
mergePopup.close();
};
auto& type = settings.mergeType;
auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter;
mergeStorage.user_data_set(&mergeSelection);
auto footerSize = imgui::footer_size_get();
auto optionsSize = imgui::child_size_get(2);
auto deleteAfterSize = imgui::child_size_get();
@@ -214,21 +226,19 @@ namespace anm2ed::animations
if (ImGui::BeginChild("Animations", animationsSize, ImGuiChildFlags_Borders))
{
mergeSelection.begin();
mergeMultiSelect.start(anm2.animations.items.size());
for (auto [i, animation] : std::views::enumerate(anm2.animations.items))
{
auto isSelected = mergeSelection.contains(i);
ImGui::PushID(i);
ImGui::SetNextItemSelectionUserData(i);
ImGui::Selectable(animation.name.c_str(), isSelected);
ImGui::Selectable(animation.name.c_str(), mergeMultiSelect.contains(i));
ImGui::PopID();
}
mergeSelection.end();
mergeMultiSelect.finish();
}
ImGui::EndChild();
@@ -262,12 +272,7 @@ namespace anm2ed::animations
if (ImGui::Button("Merge", widgetSize))
{
/*
std::set<int> sources = set::to_set(mergeSelection);
const auto merged = anm2.animations.merge(mergeTarget, sources, (MergeType)type, isDeleteAnimationsAfter);
selection = {merged};
reference = {merged};
*/
document.animations_merge((merge::Type)type, isDeleteAnimationsAfter);
merge_close();
}
ImGui::SameLine();

View File

@@ -1,7 +1,8 @@
#pragma once
#include "document.h"
#include "clipboard.h"
#include "imgui.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -10,13 +11,8 @@ namespace anm2ed::animations
class Animations
{
imgui::PopupHelper mergePopup{imgui::PopupHelper("Merge Animations")};
imgui::MultiSelectStorage mergeStorage{};
imgui::MultiSelectStorage storage{};
std::set<int> mergeSelection{};
int mergeTarget{};
public:
void update(document::Document& document, int& documentIndex, settings::Settings& settings,
resources::Resources& resources);
void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&);
};
}
}

View File

@@ -13,6 +13,7 @@ using namespace anm2ed::filesystem;
using namespace anm2ed::texture;
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace glm;
namespace anm2ed::anm2
{
@@ -64,14 +65,14 @@ namespace anm2ed::anm2
// If it doesn't work beyond that then that's on the user :^)
if (!path_is_exist(path)) path = string::to_lower(path);
if (!path_is_exist(path)) path = string::replace_backslash(path);
texture = Texture(path, true);
texture = Texture(path);
}
Spritesheet::Spritesheet(const std::string& directory, const std::string& path)
{
this->path = !path.empty() ? path : this->path.string();
WorkingDirectory workingDirectory(directory);
texture = Texture(this->path, true);
texture = Texture(this->path);
}
bool Spritesheet::save(const std::string& directory, const std::string& path)
@@ -99,6 +100,22 @@ namespace anm2ed::anm2
return texture.is_valid();
}
std::string Spritesheet::to_string(int id)
{
XMLDocument document{};
auto* element = document.NewElement("Spritesheet");
element->SetAttribute("Id", id);
element->SetAttribute("Path", path.c_str());
document.InsertFirstChild(element);
XMLPrinter printer;
document.Print(&printer);
return std::string(printer.CStr());
}
Layer::Layer() = default;
Layer::Layer(XMLElement* element, int& id)
@@ -118,6 +135,23 @@ namespace anm2ed::anm2
parent->InsertEndChild(element);
}
std::string Layer::to_string(int id)
{
XMLDocument document{};
auto* element = document.NewElement("Layer");
element->SetAttribute("Id", id);
element->SetAttribute("Name", name.c_str());
element->SetAttribute("SpritesheetId", spritesheetID);
document.InsertFirstChild(element);
XMLPrinter printer;
document.Print(&printer);
return std::string(printer.CStr());
}
Null::Null() = default;
Null::Null(XMLElement* element, int& id)
@@ -138,6 +172,22 @@ namespace anm2ed::anm2
parent->InsertEndChild(element);
}
std::string Null::to_string(int id)
{
XMLDocument document{};
auto* element = document.NewElement("Null");
element->SetAttribute("Id", id);
element->SetAttribute("Name", name.c_str());
document.InsertFirstChild(element);
XMLPrinter printer;
document.Print(&printer);
return std::string(printer.CStr());
}
Event::Event() = default;
Event::Event(XMLElement* element, int& id)
@@ -155,6 +205,22 @@ namespace anm2ed::anm2
parent->InsertEndChild(element);
}
std::string Event::to_string(int id)
{
XMLDocument document{};
auto* element = document.NewElement("Event");
element->SetAttribute("Id", id);
element->SetAttribute("Name", name.c_str());
document.InsertFirstChild(element);
XMLPrinter printer;
document.Print(&printer);
return std::string(printer.CStr());
}
Content::Content() = default;
void Content::serialize(XMLDocument& document, XMLElement* parent)
@@ -245,6 +311,134 @@ namespace anm2ed::anm2
events[id] = Event();
}
bool Content::spritesheets_deserialize(const std::string& string, const std::string& 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(spritesheets);
spritesheets[id] = std::move(spritesheet);
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
bool Content::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(layers);
layers[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
bool Content::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 layer = Null(element, id);
if (type == merge::APPEND) id = map::next_id_get(nulls);
nulls[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
bool Content::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 layer = Event(element, id);
if (type == merge::APPEND) id = map::next_id_get(events);
events[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
Frame::Frame() = default;
Frame::Frame(XMLElement* element, Type type)
@@ -504,6 +698,25 @@ namespace anm2ed::anm2
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;
}
}
void Animation::serialize(XMLDocument& document, XMLElement* parent)
{
auto element = document.NewElement("Animation");
@@ -551,6 +764,39 @@ namespace anm2ed::anm2
return length;
}
std::string Animation::to_string()
{
XMLDocument document{};
auto* element = document.NewElement("Animation");
document.InsertFirstChild(element);
element->SetAttribute("Name", name.c_str());
element->SetAttribute("FrameNum", frameNum);
element->SetAttribute("Loop", isLoop);
rootAnimation.serialize(document, element, ROOT);
auto layerAnimationsElement = document.NewElement("LayerAnimations");
for (auto& i : layerOrder)
{
Item& layerAnimation = layerAnimations.at(i);
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i);
}
element->InsertEndChild(layerAnimationsElement);
auto nullAnimationsElement = document.NewElement("NullAnimations");
for (auto& [id, nullAnimation] : nullAnimations)
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id);
element->InsertEndChild(nullAnimationsElement);
triggers.serialize(document, element, TRIGGER);
XMLPrinter printer;
document.Print(&printer);
return std::string(printer.CStr());
}
Animations::Animations() = default;
Animations::Animations(XMLElement* element)
@@ -660,6 +906,37 @@ namespace anm2ed::anm2
return finalIndex;
}
bool Animations::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;
items.insert(items.begin() + start + count, Animation(element));
indices.insert(index);
count++;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
Anm2::Anm2()
{
info.createdOn = time::get("%d-%B-%Y %I:%M:%S");
@@ -697,7 +974,7 @@ namespace anm2ed::anm2
XMLPrinter printer;
document.Print(&printer);
return printer.CStr();
return std::string(printer.CStr());
}
Anm2::Anm2(const std::string& path, std::string* errorString)
@@ -710,8 +987,6 @@ namespace anm2ed::anm2
return;
}
isValid = false;
WorkingDirectory workingDirectory(path, true);
const XMLElement* element = document.RootElement();
@@ -907,4 +1182,53 @@ namespace anm2ed::anm2
spritesheets.push_back(std::format("#{} {}", id, spritesheet.path.c_str()));
return spritesheets;
}
}
void Anm2::bake(Reference reference, int interval, bool isRoundScale, bool isRoundRotation)
{
Item* item = item_get(reference);
if (!item) return;
Frame* frame = frame_get(reference);
if (!frame) return;
if (frame->delay == FRAME_DELAY_MIN) return;
Reference referenceNext = reference;
referenceNext.frameIndex = reference.frameIndex + 1;
Frame* frameNext = frame_get(referenceNext);
if (!frameNext) frameNext = frame;
Frame baseFrame = *frame;
Frame baseFrameNext = *frameNext;
int delay{};
int index = reference.frameIndex;
while (delay < baseFrame.delay)
{
float interpolation = (float)delay / baseFrame.delay;
Frame baked = baseFrame;
baked.delay = std::min(interval, baseFrame.delay - delay);
baked.isInterpolated = (index == reference.frameIndex) ? baseFrame.isInterpolated : false;
baked.rotation = glm::mix(baseFrame.rotation, baseFrameNext.rotation, interpolation);
baked.position = glm::mix(baseFrame.position, baseFrameNext.position, interpolation);
baked.scale = glm::mix(baseFrame.scale, baseFrameNext.scale, interpolation);
baked.offset = glm::mix(baseFrame.offset, baseFrameNext.offset, interpolation);
baked.tint = glm::mix(baseFrame.tint, baseFrameNext.tint, interpolation);
if (isRoundScale) baked.scale = vec2(ivec2(baked.scale));
if (isRoundRotation) baked.rotation = (int)baked.rotation;
if (index == reference.frameIndex)
item->frames[index] = baked;
else
item->frames.insert(item->frames.begin() + index, baked);
index++;
delay += baked.delay;
}
}
}

View File

@@ -21,6 +21,7 @@ namespace anm2ed::anm2
constexpr auto MERGED_STRING = "(Merged)";
constexpr auto NO_PATH = "(No Path)";
constexpr auto LAYER_FORMAT = "#{} {} (Spritesheet: #{})";
constexpr auto NULL_FORMAT = "#{} {}";
constexpr auto SPRITESHEET_FORMAT = "#%d %s";
@@ -43,8 +44,8 @@ namespace anm2ed::anm2
int frameIndex{-1};
int frameTime{-1};
void previous_frame(int max = FRAME_NUM_MAX - 1);
void next_frame(int max = FRAME_NUM_MAX - 1);
void previous_frame(int = FRAME_NUM_MAX - 1);
void next_frame(int = FRAME_NUM_MAX - 1);
auto operator<=>(const Reference&) const = default;
};
@@ -59,8 +60,8 @@ namespace anm2ed::anm2
int version{};
Info();
Info(tinyxml2::XMLElement* element);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
Info(tinyxml2::XMLElement*);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
};
class Spritesheet
@@ -70,12 +71,13 @@ namespace anm2ed::anm2
texture::Texture texture;
Spritesheet();
Spritesheet(tinyxml2::XMLElement* element, int& id);
Spritesheet(const std::string& directory, const std::string& path = {});
bool save(const std::string& directory, const std::string& path = {});
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
void reload(const std::string& directory);
Spritesheet(tinyxml2::XMLElement*, int&);
Spritesheet(const std::string&, const std::string& = {});
bool save(const std::string&, const std::string& = {});
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
void reload(const std::string&);
bool is_valid();
std::string to_string(int id);
};
class Layer
@@ -85,8 +87,9 @@ namespace anm2ed::anm2
int spritesheetID{};
Layer();
Layer(tinyxml2::XMLElement* element, int& id);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
Layer(tinyxml2::XMLElement*, int&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
std::string to_string(int);
};
class Null
@@ -96,8 +99,9 @@ namespace anm2ed::anm2
bool isShowRect{};
Null();
Null(tinyxml2::XMLElement* element, int& id);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
Null(tinyxml2::XMLElement*, int&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
std::string to_string(int);
};
class Event
@@ -106,8 +110,9 @@ namespace anm2ed::anm2
std::string name{"New Event"};
Event();
Event(tinyxml2::XMLElement* element, int& id);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, int id);
Event(tinyxml2::XMLElement*, int&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
std::string to_string(int);
};
struct Content
@@ -119,14 +124,18 @@ namespace anm2ed::anm2
Content();
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
Content(tinyxml2::XMLElement* element);
bool spritesheet_add(const std::string& directory, const std::string& path, int& id);
void spritesheet_remove(int& id);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
Content(tinyxml2::XMLElement*);
bool spritesheet_add(const std::string&, const std::string&, int&);
void spritesheet_remove(int&);
std::set<int> spritesheets_unused();
void layer_add(int& id);
void null_add(int& id);
void event_add(int& id);
void layer_add(int&);
void null_add(int&);
void event_add(int&);
bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr);
bool layers_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
bool nulls_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
bool events_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
};
#define MEMBERS \
@@ -152,8 +161,8 @@ namespace anm2ed::anm2
#undef X
Frame();
Frame(tinyxml2::XMLElement* element, Type type);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, Type type);
Frame(tinyxml2::XMLElement*, Type);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type);
void shorten();
void extend();
};
@@ -175,10 +184,10 @@ namespace anm2ed::anm2
Item();
Item(tinyxml2::XMLElement* element, Type type, int* id = nullptr);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent, Type type, int id = -1);
int length(Type type);
Frame frame_generate(float time, Type type);
Item(tinyxml2::XMLElement*, Type, int* = nullptr);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1);
int length(Type);
Frame frame_generate(float, Type);
};
class Animation
@@ -194,10 +203,12 @@ namespace anm2ed::anm2
Item triggers;
Animation();
Animation(tinyxml2::XMLElement* element);
Item* item_get(Type type, int id = -1);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
Animation(tinyxml2::XMLElement*);
Item* item_get(Type, int = -1);
void item_remove(Type, int = -1);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
int length();
std::string to_string();
};
struct Animations
@@ -207,42 +218,41 @@ namespace anm2ed::anm2
Animations();
Animations(tinyxml2::XMLElement* element);
void serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent);
Animations(tinyxml2::XMLElement*);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
int length();
int merge(int target, std::set<int>& sources, types::merge::Type type, bool isDeleteAfter = true);
int merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
};
class Anm2
{
bool isValid{false};
public:
Info info{};
Content content{};
Animations animations{};
Anm2();
bool serialize(const std::string& path, std::string* errorString = nullptr);
bool serialize(const std::string&, std::string* = nullptr);
std::string to_string();
Anm2(const std::string& path, std::string* errorString = nullptr);
Anm2(const std::string&, std::string* = nullptr);
uint64_t hash();
Animation* animation_get(Reference& reference);
Item* item_get(Reference& reference);
Frame* frame_get(Reference& reference);
bool spritesheet_add(const std::string& directory, const std::string& path, int& id);
Spritesheet* spritesheet_get(int id);
void spritesheet_remove(int id);
Animation* animation_get(Reference&);
Item* item_get(Reference&);
Frame* frame_get(Reference&);
bool spritesheet_add(const std::string&, const std::string&, int&);
Spritesheet* spritesheet_get(int);
void spritesheet_remove(int);
std::set<int> spritesheets_unused();
int layer_add();
Reference layer_add(Reference reference = REFERENCE_DEFAULT, std::string name = {}, int spritesheetID = 0,
types::locale::Type locale = types::locale::GLOBAL);
Reference null_add(Reference reference = REFERENCE_DEFAULT, std::string name = {},
types::locale::Type locale = types::locale::GLOBAL);
void event_add(int& id);
std::set<int> events_unused(Reference reference = REFERENCE_DEFAULT);
std::set<int> layers_unused(Reference reference = REFERENCE_DEFAULT);
std::set<int> nulls_unused(Reference reference = REFERENCE_DEFAULT);
Reference layer_add(Reference = REFERENCE_DEFAULT, std::string = {}, int = 0,
types::locale::Type = types::locale::GLOBAL);
Reference null_add(Reference = REFERENCE_DEFAULT, std::string = {}, types::locale::Type = types::locale::GLOBAL);
void event_add(int&);
std::set<int> events_unused(Reference = REFERENCE_DEFAULT);
std::set<int> layers_unused(Reference = REFERENCE_DEFAULT);
std::set<int> nulls_unused(Reference = REFERENCE_DEFAULT);
std::vector<std::string> spritesheet_names_get();
void bake(Reference, int = 1, bool = true, bool = true);
};
}
}

View File

@@ -32,24 +32,24 @@ namespace anm2ed::canvas
glm::vec2 size{};
Canvas();
Canvas(glm::vec2 size);
Canvas(glm::vec2);
~Canvas();
bool is_valid();
void framebuffer_set();
void framebuffer_resize_check();
void size_set(glm::vec2 size);
glm::mat4 transform_get(float zoom, glm::vec2 pan);
void axes_render(shader::Shader& shader, float zoom, glm::vec2 pan, glm::vec4 color = glm::vec4(1.0f));
void grid_render(shader::Shader& shader, float zoom, glm::vec2 pan, glm::ivec2 size = glm::ivec2(32, 32),
glm::ivec2 offset = {}, glm::vec4 color = glm::vec4(1.0f));
void texture_render(shader::Shader& shader, GLuint& texture, glm::mat4& transform, glm::vec4 tint = glm::vec4(1.0f),
glm::vec3 colorOffset = {}, float* vertices = (float*)TEXTURE_VERTICES);
void rect_render(shader::Shader& shader, glm::mat4& transform, glm::vec4 color = glm::vec4(1.0f));
void size_set(glm::vec2);
glm::mat4 transform_get(float, glm::vec2);
void axes_render(shader::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f));
void grid_render(shader::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {},
glm::vec4 = glm::vec4(1.0f));
void texture_render(shader::Shader&, GLuint&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
float* = (float*)TEXTURE_VERTICES);
void rect_render(shader::Shader&, glm::mat4&, glm::vec4 = glm::vec4(1.0f));
void viewport_set();
void clear(glm::vec4& color);
void clear(glm::vec4&);
void bind();
void unbind();
void zoom_set(float& zoom, glm::vec2& pan, glm::vec2& focus, float step);
glm::vec2 position_translate(float& zoom, glm::vec2& pan, glm::vec2 position);
void zoom_set(float&, glm::vec2&, glm::vec2&, float);
glm::vec2 position_translate(float&, glm::vec2&, glm::vec2);
};
}

25
src/clipboard.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include "clipboard.h"
#include <SDL3/SDL.h>
namespace anm2ed::clipboard
{
std::string Clipboard::get()
{
auto text = SDL_GetClipboardText();
auto string = std::string(text);
SDL_free(text);
return string;
}
bool Clipboard::is_empty()
{
return get().empty();
}
void Clipboard::set(const std::string& string)
{
SDL_SetClipboardText(string.data());
}
}

14
src/clipboard.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <string>
namespace anm2ed::clipboard
{
class Clipboard
{
public:
bool is_empty();
std::string get();
void set(const std::string&);
};
}

View File

@@ -26,14 +26,14 @@ namespace anm2ed::dialog
int replaceID{-1};
Dialog();
Dialog(SDL_Window* window);
Dialog(SDL_Window*);
void anm2_new();
void anm2_open();
void anm2_save();
void spritesheet_open();
void spritesheet_replace();
void file_explorer_open(const std::string& path);
void file_explorer_open(const std::string&);
void reset();
bool is_selected_file(Type type);
bool is_selected_file(Type);
};
}

View File

@@ -6,7 +6,8 @@
using namespace anm2ed::animations;
using namespace anm2ed::dialog;
using namespace anm2ed::document_manager;
using namespace anm2ed::clipboard;
using namespace anm2ed::manager;
using namespace anm2ed::documents;
using namespace anm2ed::playback;
using namespace anm2ed::resources;
@@ -15,8 +16,8 @@ using namespace anm2ed::taskbar;
namespace anm2ed::dockspace
{
void Dockspace::update(Taskbar& taskbar, Documents& documents, DocumentManager& manager, Settings& settings,
Resources& resources, Dialog& dialog, Playback& playback)
void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings,
Resources& resources, Dialog& dialog, Clipboard& clipboard)
{
auto viewport = ImGui::GetMainViewport();
@@ -35,17 +36,17 @@ namespace anm2ed::dockspace
{
if (auto document = manager.get(); document)
{
if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources, playback);
if (settings.windowIsAnimations) animations.update(*document, manager.selected, settings, resources);
if (settings.windowIsEvents) events.update(manager, settings, resources);
if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources);
if (settings.windowIsAnimations) animations.update(manager, settings, resources, clipboard);
if (settings.windowIsEvents) events.update(manager, settings, resources, clipboard);
if (settings.windowIsFrameProperties) frameProperties.update(manager, settings);
if (settings.windowIsLayers) layers.update(*document, settings, resources);
if (settings.windowIsNulls) nulls.update(*document, manager.selected, settings, resources);
if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard);
if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard);
if (settings.windowIsOnionskin) onionskin.update(settings);
if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources);
if (settings.windowIsSpritesheets) spritesheets.update(*document, settings, resources, dialog);
if (settings.windowIsTimeline) timeline.update(manager, settings, resources, playback);
if (settings.windowIsTools) tools.update(settings, resources);
if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard);
if (settings.windowIsTimeline) timeline.update(manager, settings, resources);
if (settings.windowIsTools) tools.update(manager, settings, resources);
}
}
}

View File

@@ -31,8 +31,7 @@ namespace anm2ed::dockspace
tools::Tools tools;
public:
void update(taskbar::Taskbar& taskbar, documents::Documents& documents, document_manager::DocumentManager& manager,
settings::Settings& settings, resources::Resources& resources, dialog::Dialog& dialog,
playback::Playback& playback);
void update(taskbar::Taskbar&, documents::Documents&, manager::Manager&, settings::Settings&, resources::Resources&,
dialog::Dialog&, clipboard::Clipboard&);
};
}

View File

@@ -2,24 +2,21 @@
#include "anm2.h"
#include "filesystem.h"
#include "toast.h"
#include "util.h"
#include <algorithm>
#include <ranges>
using namespace anm2ed::anm2;
using namespace anm2ed::filesystem;
using namespace anm2ed::toast;
using namespace anm2ed::types;
using namespace anm2ed::util;
namespace anm2ed::document
{
Document::Document()
{
for (auto& value : isJustChanged)
value = true;
}
Document::Document(const std::string& path, bool isNew, std::string* errorString)
{
for (auto& value : isJustChanged)
value = true;
if (!path_is_exist(path)) return;
if (isNew)
@@ -31,8 +28,8 @@ namespace anm2ed::document
}
this->path = path;
on_change();
clean();
change(change::ALL);
}
bool Document::save(const std::string& path, std::string* errorString)
@@ -62,7 +59,42 @@ namespace anm2ed::document
void Document::change(change::Type type)
{
hash_set();
isJustChanged[type] = true;
auto layer_set = [&]() { unusedLayerIDs = anm2.layers_unused(); };
auto null_set = [&]() { unusedNullIDs = anm2.nulls_unused(); };
auto event_set = [&]() { unusedEventIDs = anm2.events_unused(); };
auto spritesheet_set = [&]()
{
unusedSpritesheetIDs = anm2.spritesheets_unused();
spritesheetNames = anm2.spritesheet_names_get();
spritesheetNamesCstr.clear();
for (auto& name : spritesheetNames)
spritesheetNamesCstr.push_back(name.c_str());
};
switch (type)
{
case change::LAYERS:
layer_set();
break;
case change::NULLS:
null_set();
break;
case change::EVENTS:
event_set();
break;
case change::SPRITESHEETS:
spritesheet_set();
break;
case change::ALL:
layer_set();
null_set();
event_set();
spritesheet_set();
break;
default:
break;
}
}
bool Document::is_dirty()
@@ -70,11 +102,6 @@ namespace anm2ed::document
return hash != saveHash;
}
bool Document::is_just_changed(types::change::Type type)
{
return isJustChanged[type];
}
std::string Document::directory_get()
{
return path.parent_path();
@@ -85,9 +112,9 @@ namespace anm2ed::document
return path.filename().string();
}
anm2::Animation* Document::animation_get()
bool Document::is_valid()
{
return anm2.animation_get(reference);
return !path.empty();
}
anm2::Frame* Document::frame_get()
@@ -95,6 +122,42 @@ namespace anm2ed::document
return anm2.frame_get(reference);
}
void Document::frames_bake(int interval, bool isRoundScale, bool isRoundRotation)
{
snapshot("Bake Frames");
anm2.bake(reference, interval, isRoundScale, isRoundRotation);
change(change::FRAMES);
}
void Document::frames_add(anm2::Item* item)
{
if (!item) return;
auto frame = frame_get();
if (frame)
{
item->frames.insert(item->frames.begin() + reference.frameIndex, *frame);
reference.frameIndex++;
}
else if (!item->frames.empty())
{
auto frame = item->frames.back();
item->frames.emplace_back(frame);
reference.frameIndex = item->frames.size() - 1;
}
}
void Document::frames_delete(anm2::Item* item)
{
if (!item) return;
snapshot("Delete Frames");
item->frames.erase(item->frames.begin() + reference.frameIndex);
reference.frameIndex = glm::max(-1, --reference.frameIndex);
change(change::FRAMES);
}
anm2::Item* Document::item_get()
{
return anm2.item_get(reference);
@@ -105,27 +168,317 @@ namespace anm2ed::document
return anm2.spritesheet_get(referenceSpritesheet);
}
bool Document::is_valid()
void Document::spritesheet_add(const std::string& path)
{
return !path.empty();
}
void Document::on_change()
{
if (is_just_changed(change::SPRITESHEETS))
int id{};
snapshot("Add Spritesheet");
if (anm2.spritesheet_add(directory_get(), path, id))
{
spritesheetNames = anm2.spritesheet_names_get();
spritesheetNamesCstr.clear();
for (auto& name : spritesheetNames)
spritesheetNamesCstr.push_back(name.c_str());
spritesheetMultiSelect = {id};
toasts.info(std::format("Initialized spritesheet #{}: {}", id, path));
change(change::SPRITESHEETS);
}
else
toasts.error(std::format("Failed to initialize spritesheet: {}", path));
}
void Document::update()
void Document::spritesheets_deserialize(const std::string& string, merge::Type type)
{
on_change();
for (auto& value : isJustChanged)
value = false;
snapshot("Paste Spritesheet(s)");
std::string errorString{};
if (anm2.content.spritesheets_deserialize(string, directory_get(), type, &errorString))
change(change::SPRITESHEETS);
else
toasts.error(std::format("Failed to deserialize spritesheet(s): {}", errorString));
}
};
void Document::layers_deserialize(const std::string& string, merge::Type type)
{
snapshot("Paste Layer(s)");
std::string errorString{};
if (anm2.content.layers_deserialize(string, type, &errorString))
change(change::NULLS);
else
toasts.error(std::format("Failed to deserialize layer(s): {}", errorString));
}
void Document::layer_set(anm2::Layer& layer)
{
if (referenceLayer > -1)
{
snapshot("Set Layer");
anm2.content.layers[referenceLayer] = layer;
layersMultiSelect = {referenceLayer};
}
else
{
snapshot("Add Layer");
auto id = map::next_id_get(anm2.content.layers);
anm2.content.layers[id] = layer;
layersMultiSelect = {id};
}
change(change::LAYERS);
}
void Document::layers_remove_unused()
{
snapshot("Remove Unused Layers");
for (auto& id : unusedLayerIDs)
anm2.content.layers.erase(id);
change(change::LAYERS);
unusedLayerIDs.clear();
}
void Document::null_set(anm2::Null& null)
{
if (referenceNull > -1)
{
snapshot("Set Null");
anm2.content.nulls[referenceNull] = null;
nullMultiSelect = {referenceNull};
}
else
{
snapshot("Add Null");
auto id = map::next_id_get(anm2.content.nulls);
anm2.content.nulls[id] = null;
nullMultiSelect = {id};
}
change(change::NULLS);
}
void Document::null_rect_toggle(anm2::Null& null)
{
snapshot("Null Rect");
null.isShowRect = !null.isShowRect;
change(change::NULLS);
}
void Document::nulls_remove_unused()
{
snapshot("Remove Unused Nulls");
for (auto& id : unusedNullIDs)
anm2.content.nulls.erase(id);
change(change::NULLS);
unusedNullIDs.clear();
}
void Document::nulls_deserialize(const std::string& string, merge::Type type)
{
snapshot("Paste Null(s)");
std::string errorString{};
if (anm2.content.nulls_deserialize(string, type, &errorString))
change(change::NULLS);
else
toasts.error(std::format("Failed to deserialize null(s): {}", errorString));
}
void Document::event_add()
{
snapshot("Add Event");
int id{};
anm2.event_add(id);
eventMultiSelect = {id};
change(change::EVENTS);
}
void Document::events_remove_unused()
{
snapshot("Remove Unused Events");
for (auto& id : unusedEventIDs)
anm2.content.events.erase(id);
change(change::EVENTS);
unusedEventIDs.clear();
}
void Document::events_deserialize(const std::string& string, merge::Type type)
{
snapshot("Paste Event(s)");
std::string errorString{};
if (anm2.content.events_deserialize(string, type, &errorString))
change(change::EVENTS);
else
toasts.error(std::format("Failed to deserialize event(s): {}", errorString));
}
void Document::item_visible_toggle(anm2::Item* item)
{
if (!item) return;
snapshot("Item Visible");
item->isVisible = !item->isVisible;
change(change::ITEMS);
}
void Document::item_add(anm2::Type type, int id, std::string& name, locale::Type locale, int spritesheetID)
{
snapshot("Add Item");
anm2::Reference addReference;
if (type == anm2::LAYER)
addReference =
anm2.layer_add({reference.animationIndex, anm2::LAYER, id}, name, spritesheetID, (locale::Type)locale);
else if (type == anm2::NULL_)
addReference = anm2.null_add({reference.animationIndex, anm2::LAYER, id}, name, (locale::Type)locale);
reference = addReference;
change(change::ITEMS);
}
void Document::item_remove(anm2::Animation* animation)
{
snapshot("Remove Item");
if (!animation) return;
animation->item_remove(reference.itemType, reference.itemID);
reference = {reference.animationIndex};
change(change::ITEMS);
}
anm2::Animation* Document::animation_get()
{
return anm2.animation_get(reference);
}
void Document::animation_add()
{
snapshot("Add Animation");
anm2::Animation animation;
if (anm2::Animation* referenceAnimation = 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 =
animationMultiSelect.empty() ? (int)anm2.animations.items.size() - 1 : *animationMultiSelect.rbegin() + 1;
anm2.animations.items.insert(anm2.animations.items.begin() + index, animation);
animationMultiSelect = {index};
reference = {index};
change(change::ANIMATIONS);
}
void Document::animation_duplicate()
{
snapshot("Duplicate Animation(s)");
auto duplicated = animationMultiSelect;
auto duplicatedEnd = std::ranges::max(duplicated);
for (auto& id : duplicated)
{
anm2.animations.items.insert(anm2.animations.items.begin() + duplicatedEnd, anm2.animations.items[id]);
animationMultiSelect.insert(++duplicatedEnd);
animationMultiSelect.erase(id);
}
change(change::ANIMATIONS);
}
void Document::animations_move(std::vector<int>& indices, int index)
{
snapshot("Move Animation(s)");
animationMultiSelect = vector::move_indices(anm2.animations.items, indices, index);
change(change::ANIMATIONS);
}
void Document::animation_remove()
{
snapshot("Remove Animation(s)");
for (auto& i : animationMultiSelect | std::views::reverse)
anm2.animations.items.erase(anm2.animations.items.begin() + i);
animationMultiSelect.clear();
change(change::ANIMATIONS);
}
void Document::animation_default()
{
snapshot("Default Animation");
anm2.animations.defaultAnimation = anm2.animations.items[*animationMultiSelect.begin()].name;
change(change::ANIMATIONS);
}
void Document::animations_deserialize(const std::string& string)
{
snapshot("Paste Animations");
auto& multiSelect = animationMultiSelect;
auto start = multiSelect.empty() ? anm2.animations.items.size() : *multiSelect.rbegin() + 1;
std::set<int> indices{};
std::string errorString{};
if (anm2.animations.animations_deserialize(string, start, indices, &errorString))
{
multiSelect = indices;
change(change::ANIMATIONS);
}
else
toasts.error(std::format("Failed to deserialize animation(s): {}", errorString));
}
void Document::animations_merge_quick()
{
snapshot("Merge Animations");
int merged{};
if (animationMultiSelect.size() > 1)
merged = anm2.animations.merge(*animationMultiSelect.begin(), animationMultiSelect);
else if (animationMultiSelect.size() == 1 && *animationMultiSelect.begin() != (int)anm2.animations.items.size() - 1)
{
auto start = *animationMultiSelect.begin();
auto next = *animationMultiSelect.begin() + 1;
std::set<int> animationSet{};
animationSet.insert(start);
animationSet.insert(next);
merged = anm2.animations.merge(start, animationSet);
}
else
return;
animationMultiSelect = {merged};
reference = {merged};
change(change::ANIMATIONS);
}
void Document::animations_merge(merge::Type type, bool isDeleteAnimationsAfter)
{
snapshot("Merge Animations");
auto merged = anm2.animations.merge(mergeTarget, animationMergeMultiSelect, type, isDeleteAnimationsAfter);
animationMultiSelect = {merged};
reference = {merged};
change(change::ANIMATIONS);
}
void Document::snapshot(const std::string& message)
{
snapshots.push(anm2, reference, message);
}
void Document::undo()
{
snapshots.undo(anm2, reference, message);
toasts.info(std::format("Undo: {}", message));
change(change::ALL);
}
void Document::redo()
{
toasts.info(std::format("Redo: {}", message));
snapshots.redo(anm2, reference, message);
change(change::ALL);
}
bool Document::is_undo()
{
return !snapshots.undoStack.is_empty();
}
bool Document::is_redo()
{
return !snapshots.redoStack.is_empty();
}
}

View File

@@ -4,6 +4,9 @@
#include <set>
#include "anm2.h"
#include "imgui.h"
#include "playback.h"
#include "snapshots.h"
#include "types.h"
#include <glm/glm.hpp>
@@ -15,7 +18,9 @@ namespace anm2ed::document
public:
std::filesystem::path path{};
anm2::Anm2 anm2{};
anm2::Reference reference{};
std::string message{};
playback::Playback playback{};
snapshots::Snapshots snapshots{};
float previewZoom{200};
glm::vec2 previewPan{};
@@ -23,40 +28,92 @@ namespace anm2ed::document
float editorZoom{200};
int overlayIndex{};
anm2::Reference reference{};
int hoveredAnimation{-1};
int mergeTarget{-1};
imgui::MultiSelectStorage animationMultiSelect;
imgui::MultiSelectStorage animationMergeMultiSelect;
int referenceSpritesheet{-1};
int referenceLayer{-1};
std::set<int> selectedEvents{};
std::set<int> selectedLayers{};
std::set<int> selectedNulls{};
std::set<int> selectedAnimations{};
std::set<int> selectedSpritesheets{};
int hoveredSpritesheet{-1};
std::set<int> unusedSpritesheetIDs{};
std::vector<std::string> spritesheetNames{};
std::vector<const char*> spritesheetNamesCstr{};
imgui::MultiSelectStorage spritesheetMultiSelect;
int referenceLayer{-1};
int hoveredLayer{-1};
std::set<int> unusedLayerIDs{};
imgui::MultiSelectStorage layersMultiSelect;
int referenceNull{-1};
int hoveredNull{-1};
std::set<int> unusedNullIDs{};
imgui::MultiSelectStorage nullMultiSelect;
int referenceEvent{-1};
int hoveredEvent{-1};
std::set<int> unusedEventIDs{};
imgui::MultiSelectStorage eventMultiSelect;
uint64_t hash{};
uint64_t saveHash{};
bool isJustChanged[types::change::COUNT]{};
bool isOpen{true};
Document();
Document(const std::string& path, bool isNew = false, std::string* errorString = nullptr);
bool save(const std::string& path = {}, std::string* errorString = nullptr);
Document(const std::string&, bool = false, std::string* = nullptr);
bool save(const std::string& = {}, std::string* = nullptr);
void hash_set();
void clean();
void on_change();
void change(types::change::Type type);
bool is_just_changed(types::change::Type type);
void change(types::change::Type);
bool is_dirty();
void update();
std::string directory_get();
std::string filename_get();
anm2::Animation* animation_get();
anm2::Frame* frame_get();
anm2::Item* item_get();
anm2::Spritesheet* spritesheet_get();
bool is_valid();
anm2::Frame* frame_get();
void frames_add(anm2::Item* item);
void frames_delete(anm2::Item* item);
void frames_bake(int, bool, bool);
anm2::Item* item_get();
void item_add(anm2::Type, int, std::string&, types::locale::Type, int);
void item_remove(anm2::Animation* animation);
anm2::Spritesheet* spritesheet_get();
void spritesheet_add(const std::string&);
void spritesheets_deserialize(const std::string&, types::merge::Type);
void layer_set(anm2::Layer& layer);
void layers_remove_unused();
void layers_deserialize(const std::string&, types::merge::Type);
void null_set(anm2::Null& null);
void null_rect_toggle(anm2::Null& null);
void nulls_remove_unused();
void nulls_deserialize(const std::string&, types::merge::Type);
void event_add();
void events_remove_unused();
void events_deserialize(const std::string&, types::merge::Type);
void item_visible_toggle(anm2::Item*);
void animation_add();
void animation_duplicate();
void animation_default();
void animation_remove();
void animations_move(std::vector<int>&, int);
void animations_merge(types::merge::Type, bool);
void animations_merge_quick();
anm2::Animation* animation_get();
void animations_deserialize(const std::string& string);
void snapshot(const std::string& message);
void undo();
void redo();
bool is_undo();
bool is_redo();
};
};
}

View File

@@ -1,58 +0,0 @@
#include "document_manager.h"
#include "util.h"
using namespace anm2ed::util;
namespace anm2ed::document_manager
{
Document* DocumentManager::get()
{
return vector::find(documents, selected);
}
Document* DocumentManager::get(int index)
{
return vector::find(documents, index);
}
bool DocumentManager::open(const std::string& path, bool isNew)
{
std::string errorString{};
Document document = Document(path, isNew, &errorString);
if (document.is_valid())
{
documents.emplace_back(std::move(document));
selected = documents.size() - 1;
pendingSelected = selected;
return true;
}
return false;
}
bool DocumentManager::new_(const std::string& path)
{
return open(path, true);
}
void DocumentManager::save(int index, const std::string& path)
{
auto document = get(index);
if (!document) return;
std::string errorString{};
document->path = !path.empty() ? path : document->path.string();
document->save(document->path, &errorString);
}
void DocumentManager::save(const std::string& path)
{
save(selected, path);
}
void DocumentManager::close(int index)
{
documents.erase(documents.begin() + index);
}
}

View File

@@ -1,27 +0,0 @@
#pragma once
#include <vector>
#include "document.h"
using namespace anm2ed::document;
namespace anm2ed::document_manager
{
class DocumentManager
{
public:
std::vector<Document> documents{};
int selected{};
int pendingSelected{};
Document* get();
Document* get(int index);
bool open(const std::string& path, bool isNew = false);
bool new_(const std::string& path);
void save(int index, const std::string& path = {});
void save(const std::string& path = {});
void close(int index);
void spritesheet_add(const std::string& path);
};
}

View File

@@ -5,12 +5,12 @@
#include "imgui.h"
using namespace anm2ed::taskbar;
using namespace anm2ed::document_manager;
using namespace anm2ed::manager;
using namespace anm2ed::resources;
namespace anm2ed::documents
{
void Documents::update(Taskbar& taskbar, DocumentManager& manager, Resources& resources)
void Documents::update(Taskbar& taskbar, Manager& manager, Resources& resources)
{
auto viewport = ImGui::GetMainViewport();
auto windowHeight = ImGui::GetFrameHeightWithSpacing();
@@ -32,7 +32,6 @@ namespace anm2ed::documents
for (auto [i, document] : std::views::enumerate(manager.documents))
{
auto isDirty = document.is_dirty();
auto isSelected = i == manager.selected;
auto isRequested = i == manager.pendingSelected;
auto font = isDirty ? font::ITALICS : font::REGULAR;
@@ -65,8 +64,6 @@ namespace anm2ed::documents
else
manager.close(i);
}
if (isSelected) document.update();
}
ImGui::EndTabBar();

View File

@@ -1,7 +1,7 @@
#pragma once
#include "document_manager.h"
#include "imgui.h"
#include "manager.h"
#include "resources.h"
#include "taskbar.h"
@@ -17,6 +17,6 @@ namespace anm2ed::documents
public:
float height{};
void update(taskbar::Taskbar& taskbar, document_manager::DocumentManager& manager, resources::Resources& resources);
void update(taskbar::Taskbar&, manager::Manager&, resources::Resources&);
};
}

View File

@@ -2,22 +2,21 @@
#include <ranges>
using namespace anm2ed::document_manager;
using namespace anm2ed::clipboard;
using namespace anm2ed::manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
namespace anm2ed::events
{
void Events::update(DocumentManager& manager, Settings& settings, Resources& resources)
void Events::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& selection = document.selectedEvents;
if (document.is_just_changed(change::EVENTS)) unusedEventIDs = anm2.events_unused();
storage.user_data_set(&selection);
auto& unused = document.unusedEventIDs;
auto& hovered = document.hoveredEvent;
auto& multiSelect = document.eventMultiSelect;
if (ImGui::Begin("Events", &settings.windowIsEvents))
{
@@ -26,17 +25,16 @@ namespace anm2ed::events
if (ImGui::BeginChild("##Events Child", childSize, true))
{
storage.begin(anm2.content.events.size());
multiSelect.start(anm2.content.events.size());
for (auto& [id, event] : anm2.content.events)
{
auto isSelected = selection.contains(id);
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
if (imgui::selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id),
event.name, isSelected, 0, &isRenamed))
if (isRenamed) document.change(change::EVENTS);
event.name, multiSelect.contains(id), 0, &isRenamed))
if (ImGui::IsItemHovered()) hovered = id;
if (isRenamed) document.change(change::EVENTS);
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
@@ -47,34 +45,67 @@ namespace anm2ed::events
ImGui::PopID();
}
storage.end();
multiSelect.finish();
auto copy = [&]()
{
if (!multiSelect.empty())
{
std::string clipboardText{};
for (auto& id : multiSelect)
clipboardText += anm2.content.events[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.events[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
auto clipboardText = clipboard.get();
document.events_deserialize(clipboardText, type);
};
if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::BeginDisabled();
ImGui::MenuItem("Cut", settings.shortcutCut.c_str());
ImGui::EndDisabled();
ImGui::BeginDisabled(multiSelect.empty() && hovered == -1);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy();
ImGui::EndDisabled();
ImGui::BeginDisabled(clipboard.is_empty());
{
if (ImGui::BeginMenu("Paste"))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize))
{
int id{};
anm2.event_add(id);
selection = {id};
document.change(change::EVENTS);
}
if (ImGui::Button("Add", widgetSize)) document.event_add();
imgui::set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd);
ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove);
ImGui::BeginDisabled(unusedEventIDs.empty());
{
if (ImGui::Button("Remove Unused", widgetSize))
{
for (auto& id : unusedEventIDs)
anm2.content.events.erase(id);
document.change(change::EVENTS);
unusedEventIDs.clear();
}
}
ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize)) document.events_remove_unused();
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Remove unused events (i.e., ones not used by any trigger in any animation.)",
settings.shortcutRemove);

View File

@@ -1,7 +1,7 @@
#pragma once
#include "document_manager.h"
#include "imgui.h"
#include "clipboard.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -9,11 +9,8 @@ namespace anm2ed::events
{
class Events
{
imgui::MultiSelectStorage storage{};
std::set<int> unusedEventIDs{};
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources);
void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&);
};
}

View File

@@ -6,15 +6,15 @@
namespace anm2ed::filesystem
{
std::string path_preferences_get();
bool path_is_exist(const std::string& path);
bool path_is_extension(const std::string& path, const std::string& extension);
bool path_is_exist(const std::string&);
bool path_is_extension(const std::string&, const std::string&);
class WorkingDirectory
{
public:
std::filesystem::path previous;
WorkingDirectory(const std::string& path, bool isFile = false);
WorkingDirectory(const std::string&, bool = false);
~WorkingDirectory();
};
}
}

View File

@@ -5230,9 +5230,9 @@ namespace anm2ed::font
public:
Font();
Font(void* data, size_t length, int size);
Font(void*, size_t, int);
~Font();
ImFont* get();
Font& operator=(Font&& other) noexcept;
Font& operator=(Font&&) noexcept;
};
};
};

View File

@@ -9,7 +9,7 @@
#include "types.h"
using namespace anm2ed::settings;
using namespace anm2ed::document_manager;
using namespace anm2ed::manager;
using namespace anm2ed::math;
using namespace anm2ed::types;
using namespace glm;
@@ -17,7 +17,7 @@ using namespace glm;
namespace anm2ed::frame_properties
{
void FrameProperties::update(DocumentManager& manager, Settings& settings)
void FrameProperties::update(Manager& manager, Settings& settings)
{
if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties))
{

View File

@@ -1,6 +1,6 @@
#pragma once
#include "document_manager.h"
#include "manager.h"
#include "settings.h"
namespace anm2ed::frame_properties
@@ -8,6 +8,6 @@ namespace anm2ed::frame_properties
class FrameProperties
{
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings);
void update(manager::Manager&, settings::Settings&);
};
}
}

View File

@@ -118,7 +118,7 @@ namespace icon
)";
constexpr auto TARGET_DATA = R"(
<svg viewBox="0 0 24 24" fill="none" stroke="#FFF" stroke-width="1" xmlns="http://www.w3.org/2000/svg"> <circle cx="12" cy="12" r="3.5"/> <line x1="12" y1="-2" x2="12" y2="26"/> <line x1="-2" y1="12" x2="26" y2="12"/> </svg>
<svg viewBox="0 0 24 24" fill="none" stroke="#FFF" stroke-width="0.75" xmlns="http://www.w3.org/2000/svg"> <circle cx="12" cy="12" r="2.8"/> <line x1="12" y1="5" x2="12" y2="19"/> <line x1="5" y1="12" x2="19" y2="12"/> </svg>
)";
constexpr auto INTERPOLATED_DATA = R"(
@@ -142,7 +142,7 @@ namespace icon
)";
#define LIST \
X(NONE, NONE_DATA, SIZE_NORMAL) \
X(NONE, NONE_DATA, SIZE_SMALL) \
X(FILE, FILE_DATA, SIZE_NORMAL) \
X(FOLDER, FOLDER_DATA, SIZE_NORMAL) \
X(CLOSE, CLOSE_DATA, SIZE_NORMAL) \

View File

@@ -252,19 +252,15 @@ namespace anm2ed::imgui
internal.AdapterSetItemSelected = external_storage_set;
}
void MultiSelectStorage::user_data_set(std::set<int>* userData)
void MultiSelectStorage::start(size_t size)
{
internal.UserData = userData;
this->userData = userData;
}
internal.UserData = this;
void MultiSelectStorage::begin(size_t size)
{
auto io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, userData ? userData->size() : 0, size);
auto io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, this->size(), size);
internal.ApplyRequests(io);
}
void MultiSelectStorage::end()
void MultiSelectStorage::finish()
{
auto io = ImGui::EndMultiSelect();
internal.ApplyRequests(io);
@@ -281,6 +277,7 @@ namespace anm2ed::imgui
{
isOpen = true;
isTriggered = true;
isJustOpened = true;
}
void PopupHelper::trigger()
@@ -296,6 +293,11 @@ namespace anm2ed::imgui
ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * percent));
}
void PopupHelper::end()
{
isJustOpened = false;
}
void PopupHelper::close()
{
isOpen = false;

View File

@@ -124,37 +124,43 @@ namespace anm2ed::imgui
{"Super", ImGuiMod_Super},
};
std::string chord_to_string(ImGuiKeyChord chord);
ImGuiKeyChord string_to_chord(const std::string& string);
float row_widget_width_get(int count, float width = ImGui::GetContentRegionAvail().x);
ImVec2 widget_size_with_row_get(int count, float width = ImGui::GetContentRegionAvail().x);
float footer_height_get(int itemCount = 1);
ImVec2 footer_size_get(int itemCount = 1);
ImVec2 size_without_footer_get(int rowCount = 1);
ImVec2 child_size_get(int rowCount = 1);
int input_text_callback(ImGuiInputTextCallbackData* data);
bool input_text_string(const char* label, std::string* string, ImGuiInputTextFlags flags = 0);
void combo_strings(const std::string& label, int* index, std::vector<std::string>& strings);
void combo_strings(const std::string& label, int* index, std::vector<const char*>& strings);
bool selectable_input_text(const std::string& label, const std::string& id, std::string& text,
bool isSelected = false, ImGuiSelectableFlags flags = 0, bool* isRenamed = nullptr);
void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut = {});
void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected);
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);
void combo_strings(const std::string&, int*, std::vector<std::string>&);
void combo_strings(const std::string&, int*, std::vector<const char*>&);
bool selectable_input_text(const std::string&, const std::string&, std::string&, bool = false,
ImGuiSelectableFlags = 0, bool* = nullptr);
void set_item_tooltip_shortcut(const char*, const std::string& = {});
void external_storage_set(ImGuiSelectionExternalStorage*, int, bool);
ImVec2 icon_size_get();
bool chord_held(ImGuiKeyChord chord);
bool chord_repeating(ImGuiKeyChord chord, float delay = 0.125f, float rate = 0.025f);
bool shortcut(std::string string, types::shortcut::Type type = types::shortcut::FOCUSED_SET);
bool chord_held(ImGuiKeyChord);
bool chord_repeating(ImGuiKeyChord, float = 0.125f, float = 0.025f);
bool shortcut(std::string, types::shortcut::Type = types::shortcut::FOCUSED_SET);
class MultiSelectStorage
class MultiSelectStorage : public std::set<int>
{
public:
ImGuiSelectionExternalStorage internal{};
std::set<int>* userData{};
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 user_data_set(std::set<int>* userData);
void begin(size_t size);
void end();
void start(size_t);
void finish();
};
class PopupHelper
@@ -163,12 +169,14 @@ namespace anm2ed::imgui
const char* label{};
bool isOpen{};
bool isTriggered{};
bool isJustOpened{};
bool isNoHeight{};
float percent{};
PopupHelper(const char* label, float percent = POPUP_NORMAL, bool isNoHeight = false);
PopupHelper(const char*, float = POPUP_NORMAL, bool = false);
void open();
void trigger();
void end();
void close();
};
}
}

View File

@@ -2,38 +2,24 @@
#include <ranges>
#include "util.h"
using namespace anm2ed::document;
using namespace anm2ed::settings;
using namespace anm2ed::clipboard;
using namespace anm2ed::manager;
using namespace anm2ed::resources;
using namespace anm2ed::settings;
using namespace anm2ed::types;
using namespace anm2ed::util;
namespace anm2ed::layers
{
void Layers::update(Document& document, Settings& settings, Resources& resources)
void Layers::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& selection = document.selectedLayers;
auto& referenceLayer = document.referenceLayer;
if (document.is_just_changed(change::LAYERS)) unusedLayerIDs = anm2.layers_unused();
storage.user_data_set(&selection);
auto properties_popup_open = [&](int id = -1)
{
if (id == -1)
{
isAdd = true;
editLayer = anm2::Layer();
}
else
editLayer = anm2.content.layers.at(id);
propertiesPopup.open();
};
auto& reference = document.referenceLayer;
auto& unused = document.unusedLayerIDs;
auto& hovered = document.hoveredLayer;
auto& multiSelect = document.layersMultiSelect;
auto& propertiesPopup = manager.layerPropertiesPopup;
if (ImGui::Begin("Layers", &settings.windowIsLayers))
{
@@ -41,26 +27,24 @@ namespace anm2ed::layers
if (ImGui::BeginChild("##Layers Child", childSize, true))
{
storage.begin(anm2.content.layers.size());
multiSelect.start(anm2.content.layers.size());
for (auto& [id, layer] : anm2.content.layers)
{
auto isSelected = selection.contains(id);
auto isReferenced = referenceLayer == id;
auto isSelected = multiSelect.contains(id);
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
ImGui::Selectable(std::format("#{} {} (Spritesheet: #{})", id, layer.name, layer.spritesheetID).c_str(),
isSelected);
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
ImGui::Selectable(std::format(anm2::LAYER_FORMAT, id, layer.name, layer.spritesheetID).c_str(), isSelected);
if (ImGui::IsItemHovered())
{
referenceLayer = id;
properties_popup_open(id);
hovered = id;
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) manager.layer_properties_open(id);
}
else
hovered = -1;
if (isReferenced) ImGui::PopFont();
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
@@ -73,50 +57,83 @@ namespace anm2ed::layers
ImGui::PopID();
}
storage.end();
multiSelect.finish();
auto copy = [&]()
{
if (!multiSelect.empty())
{
std::string clipboardText{};
for (auto& id : multiSelect)
clipboardText += anm2.content.layers[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.layers[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
auto clipboardText = clipboard.get();
document.layers_deserialize(clipboardText, type);
};
if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::BeginDisabled();
ImGui::MenuItem("Cut", settings.shortcutCut.c_str());
ImGui::EndDisabled();
ImGui::BeginDisabled(multiSelect.empty() && hovered == -1);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy();
ImGui::EndDisabled();
ImGui::BeginDisabled(clipboard.is_empty());
{
if (ImGui::BeginMenu("Paste"))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize)) properties_popup_open();
if (ImGui::Button("Add", widgetSize)) manager.layer_properties_open();
imgui::set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd);
ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove);
ImGui::BeginDisabled(unusedLayerIDs.empty());
{
if (ImGui::Button("Remove Unused", widgetSize))
{
for (auto& id : unusedLayerIDs)
anm2.content.layers.erase(id);
document.change(change::LAYERS);
unusedLayerIDs.clear();
}
}
ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize)) document.layers_remove_unused();
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Remove unused layers (i.e., ones not used in any animation.)",
settings.shortcutRemove);
}
ImGui::End();
propertiesPopup.trigger();
manager.layer_properties_trigger();
if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto childSize = imgui::child_size_get(2);
auto& layer = editLayer;
auto close = [&]()
{
isAdd = false;
editLayer = anm2::Layer();
propertiesPopup.close();
};
auto& layer = manager.editLayer;
if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders))
{
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
imgui::input_text_string("Name", &layer.name);
ImGui::SetItemTooltip("Set the item's name.");
imgui::combo_strings("Spritesheet", &layer.spritesheetID, document.spritesheetNames);
@@ -126,27 +143,18 @@ namespace anm2ed::layers
auto widgetSize = imgui::widget_size_with_row_get(2);
if (ImGui::Button(isAdd ? "Add" : "Confirm", widgetSize))
if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize))
{
if (isAdd)
{
auto id = map::next_id_get(anm2.content.layers);
anm2.content.layers[id] = editLayer;
referenceLayer = id;
}
else
anm2.content.layers[referenceLayer] = editLayer;
document.change(change::LAYERS);
close();
document.layer_set(layer);
manager.layer_properties_close();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) close();
if (ImGui::Button("Cancel", widgetSize)) manager.layer_properties_close();
manager.layer_properties_end();
ImGui::EndPopup();
}
referenceLayer = propertiesPopup.isOpen ? referenceLayer : -1;
}
}

View File

@@ -1,22 +1,15 @@
#pragma once
#include "document.h"
#include "clipboard.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
#include "imgui.h"
namespace anm2ed::layers
{
class Layers
{
bool isAdd{};
imgui::PopupHelper propertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL, true)};
imgui::MultiSelectStorage storage;
anm2::Layer editLayer{};
std::set<int> unusedLayerIDs{};
public:
void update(document::Document& document, settings::Settings& settings, resources::Resources& resources);
void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&);
};
}
}

View File

@@ -18,7 +18,7 @@ namespace anm2ed::loader
std::vector<std::string> arguments;
bool isError{};
Loader(int argc, const char** argv);
Loader(int, const char**);
~Loader();
};
}

View File

@@ -30,15 +30,15 @@ namespace anm2ed::log
std::ofstream file{};
public:
void write(const Level level, const std::string& message);
void info(const std::string& message);
void warning(const std::string& message);
void error(const std::string& message);
void fatal(const std::string& message);
void open(const std::filesystem::path& path);
void write(const Level, const std::string&);
void info(const std::string&);
void warning(const std::string&);
void error(const std::string&);
void fatal(const std::string&);
void open(const std::filesystem::path&);
Logger();
~Logger();
};
extern Logger logger;
}
}

120
src/manager.cpp Normal file
View File

@@ -0,0 +1,120 @@
#include "manager.h"
#include "toast.h"
#include "util.h"
using namespace anm2ed::toast;
using namespace anm2ed::types;
using namespace anm2ed::util;
namespace anm2ed::manager
{
Document* Manager::get(int index)
{
return vector::find(documents, index > -1 ? index : selected);
}
void Manager::open(const std::string& path, bool isNew)
{
std::string errorString{};
Document document = Document(path, isNew, &errorString);
if (document.is_valid())
{
documents.emplace_back(std::move(document));
selected = documents.size() - 1;
pendingSelected = selected;
toasts.info(std::format("Initialized document: {}", path));
}
else
toasts.error(std::format("Failed to initialize document: {} ({})", path, errorString));
}
void Manager::new_(const std::string& path)
{
open(path, true);
}
void Manager::save(int index, const std::string& path)
{
if (auto document = get(index); document)
{
std::string errorString{};
document->path = !path.empty() ? path : document->path.string();
document->save(document->path, &errorString);
}
}
void Manager::save(const std::string& path)
{
save(selected, path);
}
void Manager::close(int index)
{
documents.erase(documents.begin() + index);
}
void Manager::layer_properties_open(int id)
{
if (auto document = get(); document)
{
if (id == -1)
editLayer = anm2::Layer();
else
editLayer = document->anm2.content.layers.at(id);
document->referenceLayer = id;
layerPropertiesPopup.open();
}
}
void Manager::layer_properties_trigger()
{
layerPropertiesPopup.trigger();
}
void Manager::layer_properties_end()
{
layerPropertiesPopup.end();
}
void Manager::layer_properties_close()
{
editLayer = anm2::Layer();
layerPropertiesPopup.close();
}
void Manager::null_properties_open(int id)
{
if (auto document = get(); document)
{
if (id == -1)
editNull = anm2::Null();
else
editNull = document->anm2.content.nulls.at(id);
document->referenceNull = id;
nullPropertiesPopup.open();
}
}
void Manager::null_properties_trigger()
{
nullPropertiesPopup.trigger();
}
void Manager::null_properties_end()
{
nullPropertiesPopup.end();
}
void Manager::null_properties_close()
{
editNull = anm2::Null();
nullPropertiesPopup.close();
}
}

40
src/manager.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include <vector>
#include "document.h"
#include "imgui.h"
using namespace anm2ed::document;
namespace anm2ed::manager
{
class Manager
{
public:
std::vector<Document> documents{};
int selected{};
int pendingSelected{};
anm2::Layer editLayer{};
imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL, true)};
anm2::Null editNull{};
imgui::PopupHelper nullPropertiesPopup{imgui::PopupHelper("Null Properties", imgui::POPUP_SMALL, true)};
Document* get(int = -1);
void open(const std::string&, bool = false);
void new_(const std::string&);
void save(int, const std::string& = {});
void save(const std::string& = {});
void close(int);
void layer_properties_open(int = -1);
void layer_properties_trigger();
void layer_properties_end();
void layer_properties_close();
void null_properties_open(int = -1);
void null_properties_trigger();
void null_properties_end();
void null_properties_close();
};
}

View File

@@ -31,16 +31,15 @@ namespace anm2ed::math
1.0f, 1.0f, uvMax.x, uvMax.y, 0.0f, 1.0f, uvMin.x, uvMax.y};
}
float round_nearest_multiple(float value, float multiple);
float round_nearest_multiple(float, float);
int float_decimals_needed(float value);
int float_decimals_needed(float);
const char* float_format_get(float value);
const char* float_format_get(float);
const char* vec2_format_get(glm::vec2& value);
const char* vec2_format_get(glm::vec2&);
glm::mat4 quad_model_get(glm::vec2 size = {}, glm::vec2 position = {}, glm::vec2 pivot = {},
glm::vec2 scale = glm::vec2(1.0f), float rotation = {});
glm::mat4 quad_model_parent_get(glm::vec2 position = {}, glm::vec2 pivot = {}, glm::vec2 scale = glm::vec2(1.0f),
float rotation = {});
}
glm::mat4 quad_model_get(glm::vec2 = {}, glm::vec2 = {}, glm::vec2 = {},
glm::vec2 = glm::vec2(1.0f), float = {});
glm::mat4 quad_model_parent_get(glm::vec2 = {}, glm::vec2 = {}, glm::vec2 = glm::vec2(1.0f), float = {});
}

View File

@@ -2,21 +2,23 @@
#include <ranges>
using namespace anm2ed::document;
using namespace anm2ed::clipboard;
using namespace anm2ed::manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::types;
namespace anm2ed::nulls
{
void Nulls::update(Document& document, int& documentIndex, Settings& settings, Resources& resources)
void Nulls::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& selection = document.selectedNulls;
if (document.is_just_changed(change::NULLS)) unusedNullsIDs = anm2.nulls_unused();
storage.user_data_set(&selection);
auto& reference = document.referenceNull;
auto& unused = document.unusedNullIDs;
auto& hovered = document.hoveredNull;
auto& multiSelect = document.nullMultiSelect;
auto& propertiesPopup = manager.nullPropertiesPopup;
if (ImGui::Begin("Nulls", &settings.windowIsNulls))
{
@@ -24,17 +26,25 @@ namespace anm2ed::nulls
if (ImGui::BeginChild("##Nulls Child", childSize, true))
{
storage.begin(anm2.content.nulls.size());
multiSelect.start(anm2.content.nulls.size());
for (auto& [id, null] : anm2.content.nulls)
{
auto isSelected = selection.contains(id);
auto isSelected = multiSelect.contains(id);
auto isReferenced = reference == id;
ImGui::PushID(id);
ImGui::SetNextItemSelectionUserData(id);
imgui::selectable_input_text(std::format("#{} {}", id, null.name),
std::format("###Document #{} Null #{}", documentIndex, id), null.name,
isSelected);
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
ImGui::Selectable(std::format(anm2::NULL_FORMAT, id, null.name).c_str(), isSelected);
if (ImGui::IsItemHovered())
{
hovered = id;
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) manager.null_properties_open(id);
}
if (isReferenced) ImGui::PopFont();
if (ImGui::BeginItemTooltip())
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
@@ -46,29 +56,106 @@ namespace anm2ed::nulls
ImGui::PopID();
}
storage.end();
multiSelect.finish();
auto copy = [&]()
{
if (!multiSelect.empty())
{
std::string clipboardText{};
for (auto& id : multiSelect)
clipboardText += anm2.content.nulls[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.nulls[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
auto clipboardText = clipboard.get();
document.nulls_deserialize(clipboardText, type);
};
if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::BeginDisabled();
ImGui::MenuItem("Cut", settings.shortcutCut.c_str());
ImGui::EndDisabled();
ImGui::BeginDisabled(multiSelect.empty() && hovered == -1);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy();
ImGui::EndDisabled();
ImGui::BeginDisabled(clipboard.is_empty());
{
if (ImGui::BeginMenu("Paste"))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
imgui::shortcut(settings.shortcutAdd);
ImGui::Button("Add", widgetSize);
if (ImGui::Button("Add", widgetSize)) manager.null_properties_open();
imgui::set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd);
ImGui::SameLine();
imgui::shortcut(settings.shortcutRemove);
ImGui::BeginDisabled(unusedNullsIDs.empty());
{
if (ImGui::Button("Remove Unused", widgetSize))
for (auto& id : unusedNullsIDs)
anm2.content.nulls.erase(id);
document.change(change::NULLS);
}
ImGui::BeginDisabled(unused.empty());
if (ImGui::Button("Remove Unused", widgetSize)) document.nulls_remove_unused();
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Remove unused nulls (i.e., ones not used in any animation.)",
settings.shortcutRemove);
}
ImGui::End();
manager.null_properties_trigger();
if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto childSize = imgui::child_size_get(2);
auto& null = manager.editNull;
if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders))
{
if (propertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere();
imgui::input_text_string("Name", &null.name);
ImGui::SetItemTooltip("Set the null's name.");
ImGui::Checkbox("Rect", &null.isShowRect);
ImGui::SetItemTooltip("The null will have a rectangle show around it.");
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
if (ImGui::Button(reference == -1 ? "Add" : "Confirm", widgetSize))
{
document.null_set(null);
manager.null_properties_close();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) manager.null_properties_close();
ImGui::EndPopup();
}
manager.null_properties_end();
}
}

View File

@@ -1,7 +1,7 @@
#pragma once
#include "document.h"
#include "imgui.h"
#include "clipboard.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -9,11 +9,7 @@ namespace anm2ed::nulls
{
class Nulls
{
imgui::MultiSelectStorage storage{};
std::set<int> unusedNullsIDs{};
public:
void update(document::Document& document, int& documentIndex, settings::Settings& settings,
resources::Resources& resources);
void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&);
};
}

View File

@@ -3,6 +3,7 @@
#include <glm/gtc/type_ptr.hpp>
#include "imgui.h"
#include "types.h"
using namespace anm2ed::settings;
using namespace anm2ed::types;
@@ -24,7 +25,6 @@ namespace anm2ed::onionskin
ImGui::PopID();
};
imgui::shortcut(settings.shortcutOnionskin);
ImGui::Checkbox("Enabled", &settings.onionskinIsEnabled);
order_configure("Before", settings.onionskinBeforeCount, settings.onionskinBeforeColor);
@@ -32,14 +32,14 @@ namespace anm2ed::onionskin
ImGui::Text("Order");
ImGui::SameLine();
ImGui::RadioButton("Before", &settings.onionskinDrawOrder, BELOW);
ImGui::RadioButton("Before", &settings.onionskinDrawOrder, draw_order::BELOW);
ImGui::SameLine();
ImGui::RadioButton("After", &settings.onionskinDrawOrder, ABOVE);
ImGui::RadioButton("After", &settings.onionskinDrawOrder, draw_order::ABOVE);
}
if (imgui::shortcut(settings.shortcutOnionskin), shortcut::GLOBAL)
settings.onionskinIsEnabled = !settings.onionskinIsEnabled;
ImGui::End();
if (imgui::shortcut(settings.shortcutOnionskin, shortcut::GLOBAL))
settings.onionskinIsEnabled = !settings.onionskinIsEnabled;
}
}

View File

@@ -4,15 +4,9 @@
namespace anm2ed::onionskin
{
enum Type
{
BELOW,
ABOVE
};
class Onionskin
{
public:
void update(settings::Settings& settings);
void update(settings::Settings&);
};
}

View File

@@ -10,9 +10,9 @@ namespace anm2ed::playback
bool isFinished{};
void toggle();
void clamp(int length);
void tick(int fps, int length, bool isLoop);
void decrement(int length);
void increment(int length);
void clamp(int);
void tick(int, int, bool);
void decrement(int);
void increment(int);
};
}
}

View File

@@ -206,8 +206,8 @@ namespace anm2ed::settings
Settings();
Settings(const std::string& path);
void save(const std::string& path, const std::string& imguiData);
Settings(const std::string&);
void save(const std::string&, const std::string&);
};
enum ShortcutType

View File

@@ -39,19 +39,6 @@ namespace anm2ed::shader
}
)";
constexpr auto GRID_VERTEX = R"(
#version 330 core
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 i_uv_out;
void main() {
i_uv_out = i_position;
gl_Position = vec4(i_position, 0.0, 1.0);
}
)";
constexpr auto FRAGMENT = R"(
#version 330 core
out vec4 o_fragColor;
@@ -78,6 +65,19 @@ void main() {
}
)";
constexpr auto GRID_VERTEX = R"(
#version 330 core
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 i_uv_out;
void main() {
i_uv_out = i_position;
gl_Position = vec4(i_position, 0.0, 1.0);
}
)";
constexpr auto GRID_FRAGMENT = R"(
#version 330 core
in vec2 i_uv_out;
@@ -98,6 +98,7 @@ void main() {
vec2 pan = u_pan;
vec2 world = (i_uv_out - (2.0 * pan / viewSize)) * (viewSize / (2.0 * zoom));
world += vec2(0.5); // Half pixel nudge
vec2 cell = max(u_size, vec2(1.0));
vec2 grid = (world - u_offset) / cell;
@@ -148,8 +149,8 @@ void main() {
GLuint id{};
Shader();
Shader(const char* vertex, const char* fragment);
Shader& operator=(Shader&& other) noexcept;
Shader(const char*, const char*);
Shader& operator=(Shader&&) noexcept;
~Shader();
bool is_valid() const;
};

68
src/snapshots.cpp Normal file
View File

@@ -0,0 +1,68 @@
#include "snapshots.h"
namespace anm2ed::snapshots
{
bool SnapshotStack::is_empty()
{
return top == 0;
}
void SnapshotStack::push(Snapshot& snapshot)
{
if (top >= MAX)
{
for (int i = 0; i < MAX - 1; i++)
snapshots[i] = snapshots[i + 1];
top = MAX - 1;
}
snapshots[top++] = snapshot;
}
Snapshot* SnapshotStack::pop()
{
if (is_empty()) return nullptr;
return &snapshots[--top];
}
void SnapshotStack::clear()
{
top = 0;
}
void Snapshots::push(const anm2::Anm2& anm2, anm2::Reference reference, const std::string& message)
{
Snapshot snapshot = {anm2, reference, message};
undoStack.push(snapshot);
redoStack.clear();
}
void Snapshots::undo(anm2::Anm2& anm2, anm2::Reference& reference, std::string& message)
{
if (auto current = undoStack.pop())
{
Snapshot snapshot = {anm2, reference, message};
redoStack.push(snapshot);
anm2 = current->anm2;
reference = current->reference;
message = current->message;
}
}
void Snapshots::redo(anm2::Anm2& anm2, anm2::Reference& reference, std::string& message)
{
if (auto current = redoStack.pop())
{
Snapshot snapshot = {anm2, reference, message};
undoStack.push(snapshot);
anm2 = current->anm2;
reference = current->reference;
message = current->message;
}
}
void Snapshots::reset()
{
undoStack.clear();
redoStack.clear();
}
};

42
src/snapshots.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include "anm2.h"
namespace anm2ed::snapshots
{
constexpr auto ACTION = "Action";
constexpr auto MAX = 100;
class Snapshot
{
public:
anm2::Anm2 anm2{};
anm2::Reference reference{};
std::string message = ACTION;
};
class SnapshotStack
{
public:
Snapshot snapshots[MAX];
int top{};
bool is_empty();
void push(Snapshot& snapshot);
Snapshot* pop();
void clear();
};
class Snapshots
{
public:
SnapshotStack undoStack{};
SnapshotStack redoStack{};
Snapshot* get();
void push(const anm2::Anm2&, anm2::Reference, const std::string&);
void undo(anm2::Anm2& anm2, anm2::Reference& reference, std::string&);
void redo(anm2::Anm2& anm2, anm2::Reference& reference, std::string&);
void reset();
};
}

View File

@@ -5,7 +5,7 @@
#include "tool.h"
#include "types.h"
using namespace anm2ed::document_manager;
using namespace anm2ed::manager;
using namespace anm2ed::settings;
using namespace anm2ed::canvas;
using namespace anm2ed::resources;
@@ -18,7 +18,7 @@ namespace anm2ed::spritesheet_editor
{
}
void SpritesheetEditor::update(DocumentManager& manager, Settings& settings, Resources& resources)
void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources)
{
auto& document = *manager.get();
auto& pan = document.editorPan;

View File

@@ -1,7 +1,7 @@
#pragma once
#include "canvas.h"
#include "document_manager.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -13,7 +13,6 @@ namespace anm2ed::spritesheet_editor
public:
SpritesheetEditor();
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources);
void update(manager::Manager&, settings::Settings&, resources::Resources&);
};
}

View File

@@ -1,10 +1,13 @@
#include "spritesheets.h"
#include <ranges>
#include "imgui.h"
#include "toast.h"
#include <ranges>
using namespace anm2ed::anm2;
using namespace anm2ed::clipboard;
using namespace anm2ed::manager;
using namespace anm2ed::settings;
using namespace anm2ed::resources;
using namespace anm2ed::dialog;
@@ -15,17 +18,72 @@ using namespace glm;
namespace anm2ed::spritesheets
{
void Spritesheets::update(Document& document, Settings& settings, Resources& resources, Dialog& dialog)
void Spritesheets::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog,
Clipboard& clipboard)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
auto& selection = document.selectedSpritesheets;
if (document.is_just_changed(change::SPRITESHEETS)) unusedSpritesheetIDs = anm2.spritesheets_unused();
auto& multiSelect = document.spritesheetMultiSelect;
auto& unused = document.unusedSpritesheetIDs;
auto& hovered = document.hoveredSpritesheet;
auto& reference = document.referenceSpritesheet;
if (ImGui::Begin("Spritesheets", &settings.windowIsSpritesheets))
{
auto style = ImGui::GetStyle();
storage.user_data_set(&selection);
auto context_menu = [&]()
{
auto copy = [&]()
{
if (!multiSelect.empty())
{
std::string clipboardText{};
for (auto& id : multiSelect)
clipboardText += anm2.content.spritesheets[id].to_string(id);
clipboard.set(clipboardText);
}
else if (hovered > -1)
clipboard.set(anm2.content.spritesheets[hovered].to_string(hovered));
};
auto paste = [&](merge::Type type)
{
auto clipboardText = clipboard.get();
document.spritesheets_deserialize(clipboardText, type);
};
if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy();
if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight))
{
ImGui::BeginDisabled();
ImGui::MenuItem("Cut", settings.shortcutCut.c_str());
ImGui::EndDisabled();
ImGui::BeginDisabled(multiSelect.empty() && hovered == -1);
if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str())) copy();
ImGui::EndDisabled();
ImGui::BeginDisabled(clipboard.is_empty());
{
if (ImGui::BeginMenu("Paste"))
{
if (ImGui::MenuItem("Append", settings.shortcutPaste.c_str())) paste(merge::APPEND);
if (ImGui::MenuItem("Replace")) paste(merge::REPLACE);
ImGui::EndMenu();
}
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
ImGui::PopStyleVar(2);
};
auto childSize = imgui::size_without_footer_get(2);
@@ -37,7 +95,7 @@ namespace anm2ed::spritesheets
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
storage.begin(anm2.content.spritesheets.size());
multiSelect.start(anm2.content.spritesheets.size());
for (auto& [id, spritesheet] : anm2.content.spritesheets)
{
@@ -45,15 +103,16 @@ namespace anm2ed::spritesheets
if (ImGui::BeginChild("##Spritesheet Child", spritesheetChildSize, ImGuiChildFlags_Borders))
{
auto isSelected = selection.contains(id);
auto isReferenced = id == document.referenceSpritesheet;
auto isSelected = multiSelect.contains(id);
auto isReferenced = id == reference;
auto cursorPos = ImGui::GetCursorPos();
auto& texture = spritesheet.texture;
auto& texture = spritesheet.texture.is_valid() ? spritesheet.texture : resources.icons[icon::NONE];
auto path = spritesheet.path.empty() ? anm2::NO_PATH : spritesheet.path.c_str();
ImGui::SetNextItemSelectionUserData(id);
ImGui::SetNextItemStorageID(id);
if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize))
document.referenceSpritesheet = id;
if (ImGui::Selectable("##Spritesheet Selectable", isSelected, 0, spritesheetChildSize)) reference = id;
if (ImGui::IsItemHovered()) hovered = id;
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
@@ -63,32 +122,31 @@ namespace anm2ed::spritesheets
auto viewport = ImGui::GetMainViewport();
auto size = texture.size.x * texture.size.y > (viewport->Size.x * viewport->Size.y) * 0.5f
? to_vec2(viewport->Size) * 0.5f
: vec2(texture.size);
auto textureSize = texture.size.x * texture.size.y > (viewport->Size.x * viewport->Size.y) * 0.5f
? to_vec2(viewport->Size) * 0.5f
: vec2(texture.size);
auto aspectRatio = (float)texture.size.x / texture.size.y;
if (size.x / size.y > aspectRatio)
size.x = size.y * aspectRatio;
if (textureSize.x / textureSize.y > aspectRatio)
textureSize.x = textureSize.y * aspectRatio;
else
size.y = size.x / aspectRatio;
textureSize.y = textureSize.x / aspectRatio;
if (ImGui::BeginChild("##Spritesheet Tooltip Image Child", to_imvec2(size), ImGuiChildFlags_Borders))
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::BeginChild("##Spritesheet Tooltip Image Child", to_imvec2(textureSize),
ImGuiChildFlags_Borders))
ImGui::Image(texture.id, ImGui::GetContentRegionAvail());
ImGui::PopStyleVar();
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::SameLine();
if (ImGui::BeginChild(
"##Spritesheet Info Tooltip Child",
ImVec2(ImGui::CalcTextSize(spritesheet.path.c_str()).x + ImGui::GetTextLineHeightWithSpacing(),
0)))
if (ImGui::BeginChild("##Spritesheet Info Tooltip Child"))
{
ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE);
ImGui::TextUnformatted(spritesheet.path.c_str());
ImGui::TextUnformatted(path);
ImGui::PopFont();
ImGui::Text("ID: %d", id);
ImGui::Text("Size: %d x %d", texture.size.x, texture.size.y);
@@ -97,7 +155,6 @@ namespace anm2ed::spritesheets
ImGui::EndTooltip();
}
ImGui::PopStyleVar(2);
auto imageSize = to_imvec2(vec2(spritesheetChildSize.y));
@@ -116,22 +173,24 @@ namespace anm2ed::spritesheets
spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2));
if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE);
ImGui::Text(SPRITESHEET_FORMAT, id, spritesheet.path.c_str());
ImGui::Text(SPRITESHEET_FORMAT, id, path);
if (isReferenced) ImGui::PopFont();
context_menu();
}
ImGui::EndChild();
ImGui::PopID();
}
storage.end();
multiSelect.finish();
ImGui::PopStyleVar();
ImGui::PopStyleVar(2);
context_menu();
}
ImGui::EndChild();
ImGui::PopStyleVar();
auto rowOneWidgetSize = imgui::widget_size_with_row_get(4);
imgui::shortcut(settings.shortcutAdd);
@@ -140,24 +199,21 @@ namespace anm2ed::spritesheets
if (dialog.is_selected_file(dialog::SPRITESHEET_OPEN))
{
int id{};
anm2.spritesheet_add(document.directory_get(), dialog.path, id);
selection = {id};
document.change(change::SPRITESHEETS);
document.spritesheet_add(dialog.path);
dialog.reset();
}
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
ImGui::BeginDisabled(multiSelect.empty());
{
if (ImGui::Button("Reload", rowOneWidgetSize))
{
for (auto& id : selection)
for (auto& id : multiSelect)
{
Spritesheet& spritesheet = anm2.content.spritesheets[id];
spritesheet.reload(document.directory_get());
toasts.add(std::format("Reloaded spritesheet #{}: {}", id, spritesheet.path.string()));
toasts.info(std::format("Reloaded spritesheet #{}: {}", id, spritesheet.path.string()));
}
}
ImGui::SetItemTooltip("Reloads the selected spritesheets.");
@@ -166,7 +222,7 @@ namespace anm2ed::spritesheets
ImGui::SameLine();
ImGui::BeginDisabled(selection.size() != 1);
ImGui::BeginDisabled(multiSelect.size() != 1);
{
if (ImGui::Button("Replace", rowOneWidgetSize)) dialog.spritesheet_replace();
ImGui::SetItemTooltip("Replace the selected spritesheet with a new one.");
@@ -175,27 +231,27 @@ namespace anm2ed::spritesheets
if (dialog.is_selected_file(dialog::SPRITESHEET_REPLACE))
{
auto& id = *selection.begin();
auto& id = *multiSelect.begin();
Spritesheet& spritesheet = anm2.content.spritesheets[id];
spritesheet = Spritesheet(document.directory_get(), dialog.path);
toasts.add(std::format("Replaced spritesheet #{}: {}", id, spritesheet.path.string()));
toasts.info(std::format("Replaced spritesheet #{}: {}", id, spritesheet.path.string()));
dialog.reset();
}
ImGui::SameLine();
ImGui::BeginDisabled(unusedSpritesheetIDs.empty());
ImGui::BeginDisabled(unused.empty());
{
imgui::shortcut(settings.shortcutRemove);
if (ImGui::Button("Remove Unused", rowOneWidgetSize))
{
for (auto& id : unusedSpritesheetIDs)
for (auto& id : unused)
{
Spritesheet& spritesheet = anm2.content.spritesheets[id];
toasts.add(std::format("Removed spritesheet #{}: {}", id, spritesheet.path.string()));
toasts.info(std::format("Removed spritesheet #{}: {}", id, spritesheet.path.string()));
anm2.spritesheet_remove(id);
}
unusedSpritesheetIDs.clear();
unused.clear();
document.change(change::SPRITESHEETS);
}
imgui::set_item_tooltip_shortcut("Remove all unused spritesheets (i.e., not used in any layer.).",
@@ -206,11 +262,11 @@ namespace anm2ed::spritesheets
auto rowTwoWidgetSize = imgui::widget_size_with_row_get(3);
imgui::shortcut(settings.shortcutSelectAll);
ImGui::BeginDisabled(selection.size() == anm2.content.spritesheets.size());
ImGui::BeginDisabled(multiSelect.size() == anm2.content.spritesheets.size());
{
if (ImGui::Button("Select All", rowTwoWidgetSize))
for (auto& id : anm2.content.spritesheets | std::views::keys)
selection.insert(id);
multiSelect.insert(id);
}
ImGui::EndDisabled();
imgui::set_item_tooltip_shortcut("Select all spritesheets.", settings.shortcutSelectAll);
@@ -218,24 +274,24 @@ namespace anm2ed::spritesheets
ImGui::SameLine();
imgui::shortcut(settings.shortcutSelectNone);
ImGui::BeginDisabled(selection.empty());
if (ImGui::Button("Select None", rowTwoWidgetSize)) selection.clear();
ImGui::BeginDisabled(multiSelect.empty());
if (ImGui::Button("Select None", rowTwoWidgetSize)) multiSelect.clear();
imgui::set_item_tooltip_shortcut("Unselect all spritesheets.", settings.shortcutSelectNone);
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(selection.empty());
ImGui::BeginDisabled(multiSelect.empty());
{
if (ImGui::Button("Save", rowTwoWidgetSize))
{
for (auto& id : selection)
for (auto& id : multiSelect)
{
Spritesheet& spritesheet = anm2.content.spritesheets[id];
if (spritesheet.save(document.directory_get()))
toasts.add(std::format("Saved spritesheet #{}: {}", id, spritesheet.path.string()));
toasts.info(std::format("Saved spritesheet #{}: {}", id, spritesheet.path.string()));
else
toasts.add(std::format("Unable to save spritesheet #{}: {}", id, spritesheet.path.string()));
toasts.info(std::format("Unable to save spritesheet #{}: {}", id, spritesheet.path.string()));
}
}
}
@@ -244,4 +300,4 @@ namespace anm2ed::spritesheets
}
ImGui::End();
}
}
}

View File

@@ -1,8 +1,8 @@
#pragma once
#include "clipboard.h"
#include "dialog.h"
#include "document.h"
#include "imgui.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -10,11 +10,8 @@ namespace anm2ed::spritesheets
{
class Spritesheets
{
imgui::MultiSelectStorage storage{};
std::set<int> unusedSpritesheetIDs{};
public:
void update(document::Document& document, settings::Settings& settings, resources::Resources& resources,
dialog::Dialog& dialog);
void update(manager::Manager&, settings::Settings&, resources::Resources&, dialog::Dialog&,
clipboard::Clipboard& clipboard);
};
}

View File

@@ -30,8 +30,9 @@ namespace anm2ed::state
{
if (auto document = manager.get())
if (auto animation = document->animation_get())
if (playback.isPlaying)
playback.tick(document->anm2.info.fps, animation->frameNum, animation->isLoop || settings.playbackIsLoop);
if (document->playback.isPlaying)
document->playback.tick(document->anm2.info.fps, animation->frameNum,
animation->isLoop || settings.playbackIsLoop);
}
void State::update(SDL_Window*& window, Settings& settings)
@@ -44,9 +45,21 @@ namespace anm2ed::state
switch (event.type)
{
case SDL_EVENT_DROP_FILE:
if (auto droppedFile = event.drop.data; filesystem::path_is_extension(droppedFile, "anm2"))
{
auto droppedFile = event.drop.data;
if (filesystem::path_is_extension(droppedFile, "anm2"))
manager.open(std::string(droppedFile));
else if (filesystem::path_is_extension(droppedFile, "png"))
{
if (auto document = manager.get())
document->spritesheet_add(droppedFile);
else
toasts.warning(std::format("Could not open spritesheet: (open a document first!)", droppedFile));
}
else
toasts.warning(std::format("Could not parse file: {} (must be .anm2 or .png)", droppedFile));
break;
}
case SDL_EVENT_QUIT:
isQuit = true;
break;
@@ -59,8 +72,8 @@ namespace anm2ed::state
ImGui_ImplOpenGL3_NewFrame();
ImGui::NewFrame();
taskbar.update(settings, dialog, manager, isQuit);
dockspace.update(taskbar, documents, manager, settings, resources, dialog, playback);
taskbar.update(manager, settings, dialog, isQuit);
dockspace.update(taskbar, documents, manager, settings, resources, dialog, clipboard);
toasts.update();
documents.update(taskbar, manager, resources);

View File

@@ -8,16 +8,16 @@ namespace anm2ed::state
{
class State
{
void tick(settings::Settings& settings);
void update(SDL_Window*& window, settings::Settings& settings);
void render(SDL_Window*& window, settings::Settings& settings);
void tick(settings::Settings&);
void update(SDL_Window*&, settings::Settings&);
void render(SDL_Window*&, settings::Settings&);
public:
bool isQuit{};
dialog::Dialog dialog;
resources::Resources resources;
playback::Playback playback;
document_manager::DocumentManager manager;
manager::Manager manager;
clipboard::Clipboard clipboard;
taskbar::Taskbar taskbar;
documents::Documents documents;
@@ -26,8 +26,8 @@ namespace anm2ed::state
uint64_t previousTick{};
uint64_t previousUpdate{};
State(SDL_Window*& window, std::vector<std::string>& arguments);
State(SDL_Window*&, std::vector<std::string>&);
void loop(SDL_Window*& window, settings::Settings& settings);
void loop(SDL_Window*&, settings::Settings&);
};
};

View File

@@ -7,12 +7,12 @@
using namespace anm2ed::settings;
using namespace anm2ed::dialog;
using namespace anm2ed::document_manager;
using namespace anm2ed::manager;
using namespace anm2ed::types;
namespace anm2ed::taskbar
{
void Taskbar::update(Settings& settings, Dialog& dialog, DocumentManager& manager, bool& isQuit)
void Taskbar::update(Manager& manager, Settings& settings, Dialog& dialog, bool& isQuit)
{
auto document = manager.get();
auto animation = document ? document->animation_get() : nullptr;
@@ -89,7 +89,11 @@ namespace anm2ed::taskbar
if (ImGui::BeginMenu("Settings"))
{
if (ImGui::MenuItem("Configure")) configurePopup.open();
if (ImGui::MenuItem("Configure"))
{
editSettings = settings;
configurePopup.open();
}
ImGui::EndMenu();
}

View File

@@ -1,8 +1,8 @@
#pragma once
#include "dialog.h"
#include "document_manager.h"
#include "imgui.h"
#include "manager.h"
#include "settings.h"
namespace anm2ed::taskbar
@@ -12,12 +12,11 @@ namespace anm2ed::taskbar
imgui::PopupHelper configurePopup{imgui::PopupHelper("Configure")};
imgui::PopupHelper aboutPopup{imgui::PopupHelper("About")};
settings::Settings editSettings{};
int selectedShortcut{};
int selectedShortcut{-1};
public:
float height{};
void update(settings::Settings& settings, dialog::Dialog& dialog, document_manager::DocumentManager& manager,
bool& isQuit);
void update(manager::Manager&, settings::Settings&, dialog::Dialog&, bool&);
};
};

View File

@@ -29,7 +29,7 @@ namespace anm2ed::texture
return id != 0;
}
void Texture::download(std::vector<uint8_t>& pixels)
void Texture::download()
{
pixels.resize(size.x * size.y * CHANNELS);
glBindTexture(GL_TEXTURE_2D, id);
@@ -37,9 +37,20 @@ namespace anm2ed::texture
glBindTexture(GL_TEXTURE_2D, 0);
}
void Texture::init(const uint8_t* data, bool isDownload)
void Texture::upload(const uint8_t* data)
{
glGenTextures(1, &id);
if (!data || size.x <= 0 || size.y <= 0) return;
const size_t pixelCount = static_cast<size_t>(size.x) * static_cast<size_t>(size.y) * CHANNELS;
pixels.assign(data, data + pixelCount);
upload();
}
void Texture::upload()
{
if (pixels.empty() || size.x <= 0 || size.y <= 0) return;
if (!is_valid()) glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
@@ -48,12 +59,10 @@ namespace anm2ed::texture
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
if (isDownload) download(pixels);
}
Texture::Texture() = default;
@@ -63,11 +72,31 @@ namespace anm2ed::texture
if (is_valid()) glDeleteTextures(1, &id);
}
Texture::Texture(const Texture& other)
{
*this = other;
}
Texture::Texture(Texture&& other)
{
*this = std::move(other);
}
Texture& Texture::operator=(const Texture& other)
{
if (this != &other)
{
if (is_valid()) glDeleteTextures(1, &id);
id = 0;
size = other.size;
filter = other.filter;
channels = other.channels;
pixels = other.pixels;
if (!pixels.empty()) upload();
}
return *this;
}
Texture& Texture::operator=(Texture&& other)
{
if (this != &other)
@@ -94,24 +123,21 @@ namespace anm2ed::texture
size = svgSize;
filter = GL_LINEAR;
init(bitmap.data());
upload(bitmap.data());
}
Texture::Texture(const std::string& pngPath, bool isDownload)
Texture::Texture(const std::string& pngPath)
{
if (const uint8* data = stbi_load(pngPath.c_str(), &size.x, &size.y, nullptr, CHANNELS); data)
{
init(data, isDownload);
upload(data);
stbi_image_free((void*)data);
}
}
bool Texture::write_png(const std::string& path)
{
std::vector<uint8_t> pixels;
download(pixels);
const bool isSuccess = stbi_write_png(path.c_str(), size.x, size.y, CHANNELS, pixels.data(), size.x * CHANNELS);
return isSuccess;
return stbi_write_png(path.c_str(), size.x, size.y, CHANNELS, this->pixels.data(), size.x * CHANNELS);
}
void Texture::bind(GLuint unit)
@@ -125,4 +151,4 @@ namespace anm2ed::texture
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, 0);
}
}
}

View File

@@ -20,17 +20,20 @@ namespace anm2ed::texture
std::vector<uint8_t> pixels{};
bool is_valid();
void download(std::vector<uint8_t>& pixels);
void init(const uint8_t* data, bool isDownload = false);
void download();
void upload(const uint8_t*);
void upload();
Texture();
~Texture();
Texture(Texture&& other);
Texture& operator=(Texture&& other);
Texture(const char* svgData, size_t svgDataLength, glm::ivec2 svgSize);
Texture(const std::string& pngPath, bool isDownload = false);
bool write_png(const std::string& path);
void bind(GLuint unit = 0);
void unbind(GLuint unit = 0);
Texture(const Texture&);
Texture(Texture&&);
Texture& operator=(const Texture&);
Texture& operator=(Texture&&);
Texture(const char*, size_t, glm::ivec2);
Texture(const std::string&);
bool write_png(const std::string&);
void bind(GLuint = 0);
void unbind(GLuint = 0);
};
}
}

View File

@@ -7,7 +7,7 @@
#include "imgui.h"
using namespace anm2ed::types;
using namespace anm2ed::document_manager;
using namespace anm2ed::manager;
using namespace anm2ed::resources;
using namespace anm2ed::settings;
using namespace anm2ed::playback;
@@ -50,9 +50,12 @@ namespace anm2ed::timeline
- Press {} to shorten the selected frame, by one frame.
- Hold Alt while clicking a non-trigger frame to toggle interpolation.)";
void Timeline::item_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation,
Settings& settings, Resources& resources, anm2::Type type, int id, int& index)
void Timeline::item_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings,
Resources& resources, anm2::Type type, int id, int& index)
{
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false;
auto isActive = reference.itemType == type && reference.itemID == id;
@@ -102,7 +105,24 @@ namespace anm2ed::timeline
{
anm2::Reference itemReference = {reference.animationIndex, type, id};
if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference;
if (ImGui::IsWindowHovered())
{
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
switch (type)
{
case anm2::LAYER:
manager.layer_properties_open(id); // Handled in layers.cpp
break;
case anm2::NULL_:
manager.null_properties_open(id); // Handled in layers.cpp
default:
break;
}
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference;
}
ImGui::Image(resources.icons[icon].id, imgui::icon_size_get());
ImGui::SameLine();
@@ -121,7 +141,7 @@ namespace anm2ed::timeline
int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE;
if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, imgui::icon_size_get()))
isVisible = !isVisible;
document.item_visible_toggle(item);
ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show.");
if (type == anm2::NULL_)
@@ -134,7 +154,7 @@ namespace anm2ed::timeline
ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x,
(itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2));
if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, imgui::icon_size_get()))
isShowRect = !isShowRect;
document.null_rect_toggle(null);
ImGui::SetItemTooltip(isShowRect ? "The null's rect is shown. Press to hide."
: "The null's rect is hidden. Press to show.");
}
@@ -180,9 +200,9 @@ namespace anm2ed::timeline
index++;
}
void Timeline::items_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources)
void Timeline::items_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings,
Resources& resources)
{
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto itemsChildSize = ImVec2(ImGui::GetTextLineHeightWithSpacing() * 15, ImGui::GetContentRegionAvail().y);
@@ -212,7 +232,7 @@ namespace anm2ed::timeline
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
item_child(anm2, reference, animation, settings, resources, type, id, index);
item_child(manager, document, animation, settings, resources, type, id, index);
};
item_child_row(anm2::NONE);
@@ -253,20 +273,24 @@ namespace anm2ed::timeline
}
ImGui::EndChild();
auto widgetSize = imgui::widget_size_with_row_get(2);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + style.WindowPadding.x, ImGui::GetCursorPosY()));
auto widgetSize = imgui::widget_size_with_row_get(2, ImGui::GetContentRegionAvail().x - style.WindowPadding.x);
ImGui::BeginDisabled(!animation);
{
imgui::shortcut(settings.shortcutAdd);
if (ImGui::Button("Add", widgetSize)) propertiesPopup.open();
ImGui::SetItemTooltip("%s", "Add a new item to the animation.");
imgui::set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd);
ImGui::SameLine();
ImGui::BeginDisabled(document.item_get());
ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER &&
reference.itemType != anm2::NULL_);
{
ImGui::Button("Remove", widgetSize);
ImGui::SetItemTooltip("%s", "Remove the selected items from the animation.");
imgui::shortcut(settings.shortcutRemove);
if (ImGui::Button("Remove", widgetSize)) document.item_remove(animation);
imgui::set_item_tooltip_shortcut("Remove the selected items from the animation.", settings.shortcutRemove);
}
ImGui::EndDisabled();
}
@@ -278,9 +302,10 @@ namespace anm2ed::timeline
}
void Timeline::frame_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources,
Playback& playback, anm2::Type type, int id, int& index, float width)
anm2::Type type, int id, int& index, float width)
{
auto& anm2 = document.anm2;
auto& playback = document.playback;
auto& reference = document.reference;
auto item = animation ? animation->item_get(type, id) : nullptr;
auto isVisible = item ? item->isVisible : false;
@@ -455,7 +480,11 @@ namespace anm2ed::timeline
if (ImGui::Button("##Frame Button", size))
{
if (type != anm2::TRIGGER && ImGui::IsKeyDown(ImGuiMod_Alt)) frame.isInterpolated = !frame.isInterpolated;
if (type == anm2::LAYER) document.referenceSpritesheet = anm2.content.layers[id].spritesheetID;
if (type == anm2::LAYER)
{
document.referenceSpritesheet = anm2.content.layers[id].spritesheetID;
document.layersMultiSelect = {id};
}
reference = frameReference;
reference.frameTime = frameTime;
}
@@ -481,10 +510,10 @@ namespace anm2ed::timeline
ImGui::PopID();
}
void Timeline::frames_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources,
Playback& playback)
void Timeline::frames_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources)
{
auto& anm2 = document.anm2;
auto& playback = document.playback;
auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15;
@@ -541,7 +570,7 @@ namespace anm2ed::timeline
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
frame_child(document, animation, settings, resources, playback, type, id, index, childWidth);
frame_child(document, animation, settings, resources, type, id, index, childWidth);
};
frames_child_row(anm2::NONE);
@@ -614,25 +643,35 @@ namespace anm2ed::timeline
ImGui::SameLine();
imgui::shortcut(settings.shortcutAdd);
ImGui::Button("Insert Frame", widgetSize);
imgui::set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd);
auto item = document.item_get();
ImGui::SameLine();
ImGui::BeginDisabled(!item);
{
imgui::shortcut(settings.shortcutAdd);
if (ImGui::Button("Insert Frame", widgetSize)) document.frames_add(item);
imgui::set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd);
imgui::shortcut(settings.shortcutRemove);
ImGui::Button("Delete Frame", widgetSize);
imgui::set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove);
ImGui::SameLine();
ImGui::SameLine();
ImGui::BeginDisabled(!document.frame_get());
{
imgui::shortcut(settings.shortcutRemove);
if (ImGui::Button("Delete Frame", widgetSize)) document.frames_delete(item);
imgui::set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove);
ImGui::Button("Bake", widgetSize);
ImGui::SetItemTooltip("%s", "Turn interpolated frames into uninterpolated ones.");
ImGui::SameLine();
if (ImGui::Button("Bake", widgetSize)) bakePopup.open();
ImGui::SetItemTooltip("Turn interpolated frames into uninterpolated ones.");
}
ImGui::EndDisabled();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Fit Animation Length", widgetSize)) animation->frameNum = animation->length();
ImGui::SetItemTooltip("%s", "The animation length will be set to the effective length of the animation.");
ImGui::SetItemTooltip("The animation length will be set to the effective length of the animation.");
ImGui::SameLine();
@@ -640,13 +679,13 @@ namespace anm2ed::timeline
ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value<int>(), step::NORMAL,
step::FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0);
if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX);
ImGui::SetItemTooltip("%s", "Set the animation's length.");
ImGui::SetItemTooltip("Set the animation's length.");
ImGui::SameLine();
ImGui::SetNextItemWidth(widgetSize.x);
ImGui::Checkbox("Loop", animation ? &animation->isLoop : &dummy_value<bool>());
ImGui::SetItemTooltip("%s", "Toggle the animation looping.");
ImGui::SetItemTooltip("Toggle the animation looping.");
}
ImGui::EndDisabled();
@@ -655,13 +694,13 @@ namespace anm2ed::timeline
ImGui::SetNextItemWidth(widgetSize.x);
ImGui::InputInt("FPS", &anm2.info.fps, 1, 5);
anm2.info.fps = clamp(anm2.info.fps, anm2::FPS_MIN, anm2::FPS_MAX);
ImGui::SetItemTooltip("%s", "Set the FPS of all animations.");
ImGui::SetItemTooltip("Set the FPS of all animations.");
ImGui::SameLine();
ImGui::SetNextItemWidth(widgetSize.x);
imgui::input_text_string("Author", &anm2.info.createdBy);
ImGui::SetItemTooltip("%s", "Set the author of the document.");
ImGui::SetItemTooltip("Set the author of the document.");
ImGui::PopStyleVar();
}
@@ -710,7 +749,6 @@ namespace anm2ed::timeline
auto optionsSize = imgui::child_size_get(11);
auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y -
(optionsSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 4));
if (ImGui::BeginChild("Options", optionsSize, ImGuiChildFlags_Borders))
{
ImGui::SeparatorText("Type");
@@ -827,17 +865,7 @@ namespace anm2ed::timeline
if (ImGui::Button("Add", widgetSize))
{
anm2::Reference addReference;
if (type == anm2::LAYER)
addReference = anm2.layer_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName,
addItemSpritesheetID, (locale::Type)locale);
else if (type == anm2::NULL_)
addReference =
anm2.null_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, (locale::Type)locale);
reference = addReference;
document.item_add((anm2::Type)type, addItemID, addItemName, (locale::Type)locale, addItemSpritesheetID);
item_properties_close();
}
ImGui::SetItemTooltip("Add the item, with the settings specified.");
@@ -849,11 +877,49 @@ namespace anm2ed::timeline
ImGui::EndPopup();
}
bakePopup.trigger();
if (ImGui::BeginPopupModal(bakePopup.label, &bakePopup.isOpen, ImGuiWindowFlags_NoResize))
{
auto& interval = settings.bakeInterval;
auto& isRoundRotation = settings.bakeIsRoundRotation;
auto& isRoundScale = settings.bakeIsRoundScale;
auto frame = document.frame_get();
ImGui::InputInt("Interval", &interval, step::NORMAL, step::FAST);
ImGui::SetItemTooltip("Set the maximum delay of each frame that will be baked.");
interval = glm::clamp(interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN);
ImGui::Checkbox("Round Rotation", &isRoundRotation);
ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number.");
ImGui::Checkbox("Round Scale", &isRoundScale);
ImGui::SetItemTooltip("Scale will be rounded to the nearest whole number.");
auto widgetSize = imgui::widget_size_with_row_get(2);
if (ImGui::Button("Bake", widgetSize))
{
document.frames_bake(interval, isRoundScale, isRoundRotation);
bakePopup.close();
}
ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected.");
ImGui::SameLine();
if (ImGui::Button("Cancel", widgetSize)) bakePopup.close();
ImGui::SetItemTooltip("Cancel baking frames.");
ImGui::EndPopup();
}
}
void Timeline::update(DocumentManager& manager, Settings& settings, Resources& resources, Playback& playback)
void Timeline::update(Manager& manager, Settings& settings, Resources& resources)
{
auto& document = *manager.get();
auto& playback = document.playback;
auto& anm2 = document.anm2;
auto& reference = document.reference;
auto animation = document.animation_get();
@@ -864,8 +930,8 @@ namespace anm2ed::timeline
if (ImGui::Begin("Timeline", &settings.windowIsTimeline))
{
isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
frames_child(document, animation, settings, resources, playback);
items_child(document, animation, settings, resources);
frames_child(document, animation, settings, resources);
items_child(manager, document, animation, settings, resources);
}
ImGui::PopStyleVar();
ImGui::End();

View File

@@ -2,9 +2,7 @@
#include "anm2.h"
#include "document.h"
#include "document_manager.h"
#include "imgui.h"
#include "playback.h"
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -16,6 +14,7 @@ namespace anm2ed::timeline
bool isWindowHovered{};
bool isHorizontalScroll{};
imgui::PopupHelper propertiesPopup{imgui::PopupHelper("Item Properties")};
imgui::PopupHelper bakePopup{imgui::PopupHelper("Bake", imgui::POPUP_SMALL, true)};
std::string addItemName{};
int addItemSpritesheetID{};
bool addItemIsRect{};
@@ -26,20 +25,16 @@ namespace anm2ed::timeline
ImDrawList* pickerLineDrawList{};
ImGuiStyle style{};
void item_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation,
settings::Settings& settings, resources::Resources& resources, anm2::Type type, int id, int& index);
void items_child(Document& document, anm2::Animation* animation, settings::Settings& settings,
resources::Resources& resources);
void frame_child(document::Document& document, anm2::Animation* animation, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback, anm2::Type type, int id, int& index,
float width);
void frames_child(document::Document& document, anm2::Animation* animation, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback);
void item_child(manager::Manager&, Document&, anm2::Animation*, settings::Settings&, resources::Resources&,
anm2::Type, int, int&);
void items_child(manager::Manager&, Document&, anm2::Animation*, settings::Settings&, resources::Resources&);
void frame_child(document::Document&, anm2::Animation*, settings::Settings&, resources::Resources&, anm2::Type, int,
int&, float);
void frames_child(document::Document&, anm2::Animation*, settings::Settings&, resources::Resources&);
void popups(document::Document& document, anm2::Animation* animation, settings::Settings& settings);
void popups(document::Document&, anm2::Animation*, settings::Settings&);
public:
void update(document_manager::DocumentManager& manager, settings::Settings& settings,
resources::Resources& resources, playback::Playback& playback);
void update(manager::Manager&, settings::Settings&, resources::Resources&);
};
}
}

View File

@@ -64,17 +64,23 @@ namespace anm2ed::toast
}
}
void Toasts::add(const std::string& message)
void Toasts::info(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.info(message);
}
void Toasts::add_error(const std::string& message)
void Toasts::error(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.error(message);
}
void Toasts::warning(const std::string& message)
{
toasts.emplace_back(Toast(message));
logger.warning(message);
}
Toasts toasts;
}

View File

@@ -12,7 +12,7 @@ namespace anm2ed::toast
std::string message{};
float lifetime{};
Toast(const std::string& message);
Toast(const std::string&);
};
class Toasts
@@ -21,8 +21,9 @@ namespace anm2ed::toast
std::vector<Toast> toasts{};
void update();
void add(const std::string& message);
void add_error(const std::string& message);
void info(const std::string&);
void error(const std::string&);
void warning(const std::string&);
};
extern Toasts toasts;

View File

@@ -8,6 +8,7 @@
#include "types.h"
using namespace anm2ed::settings;
using namespace anm2ed::manager;
using namespace anm2ed::resources;
using namespace anm2ed::types;
using namespace glm;
@@ -16,8 +17,10 @@ namespace anm2ed::tools
{
constexpr auto COLOR_EDIT_LABEL = "##Color Edit";
void Tools::update(Settings& settings, Resources& resources)
void Tools::update(Manager& manager, Settings& settings, Resources& resources)
{
auto& document = *manager.get();
if (ImGui::Begin("Tools", &settings.windowIsTools))
{
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 2));
@@ -31,8 +34,10 @@ namespace anm2ed::tools
switch (type)
{
case tool::UNDO:
if (document.is_undo()) document.undo();
break;
case tool::REDO:
if (document.is_redo()) document.redo();
break;
case tool::COLOR:
if (ImGui::IsPopupOpen(COLOR_EDIT_LABEL))
@@ -68,8 +73,12 @@ namespace anm2ed::tools
}
else
{
if (i == tool::UNDO) ImGui::BeginDisabled(!document.is_undo());
if (i == tool::REDO) ImGui::BeginDisabled(!document.is_redo());
if (ImGui::ImageButton(info.label, resources.icons[info.icon].id, to_imvec2(size)))
tool_switch((tool::Type)i);
if (i == tool::UNDO) ImGui::EndDisabled();
if (i == tool::REDO) ImGui::EndDisabled();
}
auto widthIncrement = ImGui::GetItemRectSize().x + ImGui::GetStyle().ItemSpacing.x;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "manager.h"
#include "resources.h"
#include "settings.h"
@@ -11,6 +12,6 @@ namespace anm2ed::tools
ImVec2 colorEditPosition{};
public:
void update(settings::Settings& settings, resources::Resources& resources);
void update(manager::Manager&, settings::Settings&, resources::Resources&);
};
}

View File

@@ -15,10 +15,20 @@ namespace anm2ed::types::change
ANIMATIONS,
ITEMS,
FRAMES,
ALL,
COUNT
};
}
namespace anm2ed::types::draw_order
{
enum Type
{
BELOW,
ABOVE
};
}
namespace anm2ed::types::shortcut
{
enum Type

View File

@@ -1,20 +1,23 @@
#pragma once
#include <algorithm>
#include <map>
#include <ranges>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
namespace anm2ed::util::time
{
std::string get(const char* format);
std::string get(const char*);
}
namespace anm2ed::util::string
{
std::string to_lower(const std::string& string);
std::string replace_backslash(const std::string& string);
bool to_bool(const std::string& string);
std::string to_lower(const std::string&);
std::string replace_backslash(const std::string&);
bool to_bool(const std::string&);
}
namespace anm2ed::util::map
@@ -52,4 +55,44 @@ namespace anm2ed::util::vector
{
return index >= 0 && index < (int)v.size() ? &v[index] : nullptr;
}
}
template <typename T> std::set<int> move_indices(std::vector<T>& v, std::vector<int>& indices, int index)
{
if (indices.empty()) return {};
std::vector<int> sorted = indices;
std::sort(sorted.begin(), sorted.end());
sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end());
// Determine if we are dragging items from below the target (insert before) or above (insert after)
bool insertAfter = !sorted.empty() && sorted.front() <= index;
std::vector<T> moveItems;
moveItems.reserve(sorted.size());
for (int i : sorted)
moveItems.push_back(std::move(v[i]));
for (auto i : sorted | std::views::reverse)
v.erase(v.begin() + i);
int originalSize = (int)v.size() + (int)sorted.size();
int reference = insertAfter ? index + 1 : index;
reference = std::clamp(reference, 0, originalSize);
int removedBeforeReference = 0;
for (int i : sorted)
if (i < reference) ++removedBeforeReference;
int insertPos = reference - removedBeforeReference;
insertPos = std::clamp(insertPos, 0, (int)v.size());
v.insert(v.begin() + insertPos, std::make_move_iterator(moveItems.begin()),
std::make_move_iterator(moveItems.end()));
std::set<int> moveIndices{};
for (int i = 0; i < (int)moveItems.size(); i++)
moveIndices.insert(insertPos + i);
return moveIndices;
}
}

View File

@@ -7,9 +7,8 @@
namespace anm2ed::xml
{
std::string document_to_string(tinyxml2::XMLDocument& self);
tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement* element, const char* attribute, std::string* out);
tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement* element, const char* attribute,
std::filesystem::path* out);
void query_color_attribute(tinyxml2::XMLElement* element, const char* attribute, float& out);
}
std::string document_to_string(tinyxml2::XMLDocument&);
tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement*, const char*, std::string*);
tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement*, const char*, std::filesystem::path*);
void query_color_attribute(tinyxml2::XMLElement*, const char*, float&);
}