diff --git a/src/animation_preview.cpp b/src/animation_preview.cpp index 17be4b5..8db80c7 100644 --- a/src/animation_preview.cpp +++ b/src/animation_preview.cpp @@ -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(); diff --git a/src/animation_preview.h b/src/animation_preview.h index 278c7eb..ef490dd 100644 --- a/src/animation_preview.h +++ b/src/animation_preview.h @@ -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&); }; -} \ No newline at end of file +} diff --git a/src/animations.cpp b/src/animations.cpp index 73e7baf..9d2b208 100644 --- a/src/animations.cpp +++ b/src/animations.cpp @@ -1,21 +1,24 @@ #include "animations.h" -#include #include -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 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 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 indices(data, data + count); - //std::vector 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 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 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(); diff --git a/src/animations.h b/src/animations.h index 552b4d1..e03e7e9 100644 --- a/src/animations.h +++ b/src/animations.h @@ -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 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&); }; -} \ No newline at end of file +} diff --git a/src/anm2.cpp b/src/anm2.cpp index b67c7a1..652b8f4 100644 --- a/src/anm2.cpp +++ b/src/anm2.cpp @@ -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& 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; } -} \ No newline at end of file + + 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; + } + } +} diff --git a/src/anm2.h b/src/anm2.h index 5257a11..02aaf5a 100644 --- a/src/anm2.h +++ b/src/anm2.h @@ -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 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& sources, types::merge::Type type, bool isDeleteAfter = true); + int merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); + bool animations_deserialize(const std::string&, int, std::set&, 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 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 events_unused(Reference reference = REFERENCE_DEFAULT); - std::set layers_unused(Reference reference = REFERENCE_DEFAULT); - std::set 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 events_unused(Reference = REFERENCE_DEFAULT); + std::set layers_unused(Reference = REFERENCE_DEFAULT); + std::set nulls_unused(Reference = REFERENCE_DEFAULT); std::vector spritesheet_names_get(); + void bake(Reference, int = 1, bool = true, bool = true); }; -} \ No newline at end of file +} diff --git a/src/canvas.h b/src/canvas.h index 55e1672..573d22d 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -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); }; } diff --git a/src/clipboard.cpp b/src/clipboard.cpp new file mode 100644 index 0000000..8ec5e53 --- /dev/null +++ b/src/clipboard.cpp @@ -0,0 +1,25 @@ +#include "clipboard.h" + +#include + +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()); + } +} \ No newline at end of file diff --git a/src/clipboard.h b/src/clipboard.h new file mode 100644 index 0000000..0b13a07 --- /dev/null +++ b/src/clipboard.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace anm2ed::clipboard +{ + class Clipboard + { + public: + bool is_empty(); + std::string get(); + void set(const std::string&); + }; +} diff --git a/src/dialog.h b/src/dialog.h index e6983c9..487c37f 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -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); }; } diff --git a/src/dockspace.cpp b/src/dockspace.cpp index a86d1f3..f77f28d 100644 --- a/src/dockspace.cpp +++ b/src/dockspace.cpp @@ -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); } } } diff --git a/src/dockspace.h b/src/dockspace.h index 59f4969..04ef75f 100644 --- a/src/dockspace.h +++ b/src/dockspace.h @@ -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&); }; } diff --git a/src/document.cpp b/src/document.cpp index 3727f79..7cdf70e 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -2,24 +2,21 @@ #include "anm2.h" #include "filesystem.h" +#include "toast.h" +#include "util.h" +#include +#include 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)); } -}; \ No newline at end of file + + 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& 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 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 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(); + } +} \ No newline at end of file diff --git a/src/document.h b/src/document.h index 7097ce6..69b27ff 100644 --- a/src/document.h +++ b/src/document.h @@ -4,6 +4,9 @@ #include #include "anm2.h" +#include "imgui.h" +#include "playback.h" +#include "snapshots.h" #include "types.h" #include @@ -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 selectedEvents{}; - std::set selectedLayers{}; - std::set selectedNulls{}; - std::set selectedAnimations{}; - std::set selectedSpritesheets{}; - + int hoveredSpritesheet{-1}; + std::set unusedSpritesheetIDs{}; std::vector spritesheetNames{}; std::vector spritesheetNamesCstr{}; + imgui::MultiSelectStorage spritesheetMultiSelect; + + int referenceLayer{-1}; + int hoveredLayer{-1}; + std::set unusedLayerIDs{}; + imgui::MultiSelectStorage layersMultiSelect; + + int referenceNull{-1}; + int hoveredNull{-1}; + std::set unusedNullIDs{}; + imgui::MultiSelectStorage nullMultiSelect; + + int referenceEvent{-1}; + int hoveredEvent{-1}; + std::set 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); + 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(); }; -}; \ No newline at end of file +} diff --git a/src/document_manager.cpp b/src/document_manager.cpp deleted file mode 100644 index 2ad1207..0000000 --- a/src/document_manager.cpp +++ /dev/null @@ -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); - } -} diff --git a/src/document_manager.h b/src/document_manager.h deleted file mode 100644 index 8bd5071..0000000 --- a/src/document_manager.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include "document.h" - -using namespace anm2ed::document; - -namespace anm2ed::document_manager -{ - class DocumentManager - { - public: - std::vector 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); - }; -} diff --git a/src/documents.cpp b/src/documents.cpp index 1059a0d..ebfa28a 100644 --- a/src/documents.cpp +++ b/src/documents.cpp @@ -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(); diff --git a/src/documents.h b/src/documents.h index f5bb60a..4aa184a 100644 --- a/src/documents.h +++ b/src/documents.h @@ -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&); }; } diff --git a/src/events.cpp b/src/events.cpp index 7e69b83..46def77 100644 --- a/src/events.cpp +++ b/src/events.cpp @@ -2,22 +2,21 @@ #include -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); diff --git a/src/events.h b/src/events.h index f38f3bf..ef27454 100644 --- a/src/events.h +++ b/src/events.h @@ -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 unusedEventIDs{}; public: - void update(document_manager::DocumentManager& manager, settings::Settings& settings, - resources::Resources& resources); + void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); }; } diff --git a/src/filesystem.h b/src/filesystem.h index 804ed22..0959d78 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -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(); }; -} \ No newline at end of file +} diff --git a/src/font.h b/src/font.h index e6dccc2..f5e4802 100644 --- a/src/font.h +++ b/src/font.h @@ -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; }; -}; \ No newline at end of file +}; diff --git a/src/frame_properties.cpp b/src/frame_properties.cpp index 35ec7ff..57e75f0 100644 --- a/src/frame_properties.cpp +++ b/src/frame_properties.cpp @@ -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)) { diff --git a/src/frame_properties.h b/src/frame_properties.h index 27ea5ee..ab092ee 100644 --- a/src/frame_properties.h +++ b/src/frame_properties.h @@ -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&); }; -} \ No newline at end of file +} diff --git a/src/icon.h b/src/icon.h index 737a2c3..7af6181 100644 --- a/src/icon.h +++ b/src/icon.h @@ -118,7 +118,7 @@ namespace icon )"; constexpr auto TARGET_DATA = R"( - + )"; 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) \ diff --git a/src/imgui.cpp b/src/imgui.cpp index c3a3007..883c15d 100644 --- a/src/imgui.cpp +++ b/src/imgui.cpp @@ -252,19 +252,15 @@ namespace anm2ed::imgui internal.AdapterSetItemSelected = external_storage_set; } - void MultiSelectStorage::user_data_set(std::set* 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; diff --git a/src/imgui.h b/src/imgui.h index 7206a54..2deec12 100644 --- a/src/imgui.h +++ b/src/imgui.h @@ -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& strings); - void combo_strings(const std::string& label, int* index, std::vector& 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&); + void combo_strings(const std::string&, int*, std::vector&); + 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 { public: ImGuiSelectionExternalStorage internal{}; - std::set* userData{}; + using std::set::set; + using std::set::operator=; + using std::set::begin; + using std::set::rbegin; + using std::set::end; + using std::set::size; + using std::set::insert; + using std::set::erase; MultiSelectStorage(); - void user_data_set(std::set* 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(); }; -} \ No newline at end of file +} diff --git a/src/layers.cpp b/src/layers.cpp index a7b958a..69b1546 100644 --- a/src/layers.cpp +++ b/src/layers.cpp @@ -2,38 +2,24 @@ #include -#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; } } diff --git a/src/layers.h b/src/layers.h index ed1538f..e72c331 100644 --- a/src/layers.h +++ b/src/layers.h @@ -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 unusedLayerIDs{}; - public: - void update(document::Document& document, settings::Settings& settings, resources::Resources& resources); + void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); }; -} \ No newline at end of file +} diff --git a/src/loader.h b/src/loader.h index fda6a20..d3cbbb1 100644 --- a/src/loader.h +++ b/src/loader.h @@ -18,7 +18,7 @@ namespace anm2ed::loader std::vector arguments; bool isError{}; - Loader(int argc, const char** argv); + Loader(int, const char**); ~Loader(); }; } diff --git a/src/log.h b/src/log.h index 84b61f5..1870827 100644 --- a/src/log.h +++ b/src/log.h @@ -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; -} \ No newline at end of file +} diff --git a/src/manager.cpp b/src/manager.cpp new file mode 100644 index 0000000..a839622 --- /dev/null +++ b/src/manager.cpp @@ -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(); + } + +} diff --git a/src/manager.h b/src/manager.h new file mode 100644 index 0000000..1d46381 --- /dev/null +++ b/src/manager.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "document.h" +#include "imgui.h" + +using namespace anm2ed::document; + +namespace anm2ed::manager +{ + class Manager + { + public: + std::vector 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(); + }; +} diff --git a/src/math.h b/src/math.h index c058de3..6e928be 100644 --- a/src/math.h +++ b/src/math.h @@ -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 = {}); -} \ No newline at end of file + 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 = {}); +} diff --git a/src/nulls.cpp b/src/nulls.cpp index fd6eef4..f40dd9e 100644 --- a/src/nulls.cpp +++ b/src/nulls.cpp @@ -2,21 +2,23 @@ #include -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(); } } \ No newline at end of file diff --git a/src/nulls.h b/src/nulls.h index 3acc7bb..1eba1bb 100644 --- a/src/nulls.h +++ b/src/nulls.h @@ -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 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&); }; } \ No newline at end of file diff --git a/src/onionskin.cpp b/src/onionskin.cpp index 970aaf1..1ec35b6 100644 --- a/src/onionskin.cpp +++ b/src/onionskin.cpp @@ -3,6 +3,7 @@ #include #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; } + } diff --git a/src/onionskin.h b/src/onionskin.h index 2157670..43f9358 100644 --- a/src/onionskin.h +++ b/src/onionskin.h @@ -4,15 +4,9 @@ namespace anm2ed::onionskin { - enum Type - { - BELOW, - ABOVE - }; - class Onionskin { public: - void update(settings::Settings& settings); + void update(settings::Settings&); }; } diff --git a/src/playback.h b/src/playback.h index edbd7f3..291c527 100644 --- a/src/playback.h +++ b/src/playback.h @@ -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); }; -} \ No newline at end of file +} diff --git a/src/settings.h b/src/settings.h index 3fa327f..535c27a 100644 --- a/src/settings.h +++ b/src/settings.h @@ -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 diff --git a/src/shader.h b/src/shader.h index a0ff1d5..c29db47 100644 --- a/src/shader.h +++ b/src/shader.h @@ -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; }; diff --git a/src/snapshots.cpp b/src/snapshots.cpp new file mode 100644 index 0000000..a456be6 --- /dev/null +++ b/src/snapshots.cpp @@ -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(); + } +}; diff --git a/src/snapshots.h b/src/snapshots.h new file mode 100644 index 0000000..e0f12ff --- /dev/null +++ b/src/snapshots.h @@ -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(); + }; +} diff --git a/src/spritesheet_editor.cpp b/src/spritesheet_editor.cpp index d153c43..81e7141 100644 --- a/src/spritesheet_editor.cpp +++ b/src/spritesheet_editor.cpp @@ -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; diff --git a/src/spritesheet_editor.h b/src/spritesheet_editor.h index fa1010a..c1b1c00 100644 --- a/src/spritesheet_editor.h +++ b/src/spritesheet_editor.h @@ -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&); }; } diff --git a/src/spritesheets.cpp b/src/spritesheets.cpp index 5de52d5..e2dbaca 100644 --- a/src/spritesheets.cpp +++ b/src/spritesheets.cpp @@ -1,10 +1,13 @@ #include "spritesheets.h" +#include + #include "imgui.h" #include "toast.h" -#include 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(); } -} +} \ No newline at end of file diff --git a/src/spritesheets.h b/src/spritesheets.h index 7bed4ce..27a30d5 100644 --- a/src/spritesheets.h +++ b/src/spritesheets.h @@ -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 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); }; } diff --git a/src/state.cpp b/src/state.cpp index 2a247b6..ab209f4 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -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); diff --git a/src/state.h b/src/state.h index bb1d099..4ee9adc 100644 --- a/src/state.h +++ b/src/state.h @@ -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& arguments); + State(SDL_Window*&, std::vector&); - void loop(SDL_Window*& window, settings::Settings& settings); + void loop(SDL_Window*&, settings::Settings&); }; }; diff --git a/src/taskbar.cpp b/src/taskbar.cpp index a769476..cec33ac 100644 --- a/src/taskbar.cpp +++ b/src/taskbar.cpp @@ -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(); } diff --git a/src/taskbar.h b/src/taskbar.h index 257b5aa..b013eab 100644 --- a/src/taskbar.h +++ b/src/taskbar.h @@ -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&); }; }; diff --git a/src/texture.cpp b/src/texture.cpp index a134a5c..76c9761 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -29,7 +29,7 @@ namespace anm2ed::texture return id != 0; } - void Texture::download(std::vector& 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.x) * static_cast(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 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); } -} \ No newline at end of file +} diff --git a/src/texture.h b/src/texture.h index 834bc09..9fc8076 100644 --- a/src/texture.h +++ b/src/texture.h @@ -20,17 +20,20 @@ namespace anm2ed::texture std::vector pixels{}; bool is_valid(); - void download(std::vector& 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); }; -} \ No newline at end of file +} diff --git a/src/timeline.cpp b/src/timeline.cpp index ef50288..dd47ff6 100644 --- a/src/timeline.cpp +++ b/src/timeline.cpp @@ -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(), 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()); - 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(); diff --git a/src/timeline.h b/src/timeline.h index 799a6e7..5284ca7 100644 --- a/src/timeline.h +++ b/src/timeline.h @@ -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&); }; -} \ No newline at end of file +} diff --git a/src/toast.cpp b/src/toast.cpp index beb84e1..a0d65c1 100644 --- a/src/toast.cpp +++ b/src/toast.cpp @@ -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; } diff --git a/src/toast.h b/src/toast.h index 306787b..6aff2cc 100644 --- a/src/toast.h +++ b/src/toast.h @@ -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 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; diff --git a/src/tools.cpp b/src/tools.cpp index 3e32d0f..c0d6073 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -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; diff --git a/src/tools.h b/src/tools.h index 895a3b3..377b636 100644 --- a/src/tools.h +++ b/src/tools.h @@ -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&); }; } diff --git a/src/types.h b/src/types.h index a4b7b82..439d138 100644 --- a/src/types.h +++ b/src/types.h @@ -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 diff --git a/src/util.h b/src/util.h index ec50cb3..ec99f47 100644 --- a/src/util.h +++ b/src/util.h @@ -1,20 +1,23 @@ #pragma once +#include #include +#include +#include #include #include #include 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; } -} \ No newline at end of file + + template std::set move_indices(std::vector& v, std::vector& indices, int index) + { + if (indices.empty()) return {}; + + std::vector 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 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 moveIndices{}; + for (int i = 0; i < (int)moveItems.size(); i++) + moveIndices.insert(insertPos + i); + + return moveIndices; + } +} diff --git a/src/xml.h b/src/xml.h index 9bca4a3..100ec82 100644 --- a/src/xml.h +++ b/src/xml.h @@ -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); -} \ No newline at end of file + 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&); +}