From 729d5fb21634fb1df44bf3f0ce3af180a56ac27c Mon Sep 17 00:00:00 2001 From: shweet Date: Tue, 28 Oct 2025 15:47:54 -0400 Subject: [PATCH] Autosave feature, spritesheet editor dashed lines, refactoring, fixes --- CMakeLists.txt | 5 +- src/animation_preview.cpp | 77 ++++---- src/animations.cpp | 19 +- src/anm2.cpp | 211 +++++++++++++++++++--- src/anm2.h | 14 +- src/canvas.cpp | 24 ++- src/canvas.h | 9 +- src/dockspace.cpp | 9 +- src/dockspace.h | 2 + src/document.cpp | 131 ++++++++++++-- src/document.h | 23 ++- src/documents.cpp | 101 ++++++++--- src/documents.h | 7 +- src/events.cpp | 2 + src/filesystem.cpp | 5 + src/filesystem.h | 1 + src/frame_properties.cpp | 4 +- src/icon.h | 10 ++ src/imgui.cpp | 5 + src/imgui.h | 1 + src/layers.cpp | 2 + src/loader.cpp | 2 +- src/manager.cpp | 219 +++++++++++++++++++++-- src/manager.h | 30 +++- src/nulls.cpp | 2 + src/settings.h | 8 +- src/shader.h | 86 ++++++++- src/spritesheet_editor.cpp | 14 +- src/spritesheets.cpp | 2 + src/state.cpp | 9 +- src/state.h | 5 +- src/taskbar.cpp | 353 +++++++++++++++++++++++++++++++++---- src/taskbar.h | 11 +- src/timeline.cpp | 111 +++++++++--- src/timeline.h | 16 +- src/types.h | 10 ++ src/util.h | 12 ++ src/welcome.cpp | 106 +++++++++++ src/welcome.h | 16 ++ 39 files changed, 1446 insertions(+), 228 deletions(-) create mode 100644 src/welcome.cpp create mode 100644 src/welcome.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2602b5b..9316794 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,13 +63,14 @@ if (WIN32) target_link_options(${PROJECT_NAME} PRIVATE /STACK:0xffffff) else () target_compile_options(${PROJECT_NAME} PRIVATE - -O2 -Wall -Wextra -pedantic + -Wall -Wextra -pedantic ) if (CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG) - target_compile_options(${PROJECT_NAME} PRIVATE -pg) + target_compile_options(${PROJECT_NAME} PRIVATE -O0 -pg) else () set(CMAKE_BUILD_TYPE "Release") + target_compile_options(${PROJECT_NAME} PRIVATE -O2) endif () target_link_libraries(${PROJECT_NAME} PRIVATE m) diff --git a/src/animation_preview.cpp b/src/animation_preview.cpp index f73ded1..4c523b0 100644 --- a/src/animation_preview.cpp +++ b/src/animation_preview.cpp @@ -48,15 +48,18 @@ namespace anm2ed::animation_preview auto& isRootTransform = settings.previewIsRootTransform; auto& isPivots = settings.previewIsPivots; auto& isAxes = settings.previewIsAxes; - auto& isIcons = settings.previewIsIcons; auto& isAltIcons = settings.previewIsAltIcons; auto& isBorder = settings.previewIsBorder; auto& tool = settings.tool; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto& shaderLine = resources.shaders[shader::LINE]; auto& shaderAxes = resources.shaders[shader::AXIS]; auto& shaderGrid = resources.shaders[shader::GRID]; auto& shaderTexture = resources.shaders[shader::TEXTURE]; + settings.previewPan = pan; + settings.previewZoom = zoom; + if (ImGui::Begin("Animation Preview", &settings.windowIsAnimationPreview)) { @@ -128,7 +131,6 @@ namespace anm2ed::animation_preview { ImGui::Checkbox("Root Transform", &isRootTransform); ImGui::Checkbox("Pivots", &isPivots); - ImGui::Checkbox("Icons", &isIcons); } ImGui::EndChild(); @@ -152,7 +154,8 @@ namespace anm2ed::animation_preview if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor); if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); - auto render = [&](float time, vec3 colorOffset = {}, float alphaOffset = {}, bool isOnionskin = false) + auto render = [&](anm2::Animation* animation, float time, vec3 colorOffset = {}, float alphaOffset = {}, + bool isOnionskin = false) { auto transform = transform_get(zoom, pan); auto root = animation->rootAnimation.frame_generate(time, anm2::ROOT); @@ -160,7 +163,7 @@ namespace anm2ed::animation_preview if (isRootTransform) transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); - if (isIcons && root.isVisible && animation->rootAnimation.isVisible) + if (!isOnlyShowLayers && root.isVisible && animation->rootAnimation.isVisible) { auto rootTransform = transform * math::quad_model_get(TARGET_SIZE, root.position, TARGET_SIZE * 0.5f, math::percent_to_unit(root.scale), root.rotation); @@ -185,13 +188,14 @@ namespace anm2ed::animation_preview auto& texture = spritesheet->texture; if (!texture.is_valid()) continue; - auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot, - math::percent_to_unit(frame.scale), frame.rotation); + auto layerModel = math::quad_model_get(frame.size, frame.position, frame.pivot, + math::percent_to_unit(frame.scale), frame.rotation); + auto layerTransform = transform * layerModel; auto uvMin = frame.crop / vec2(texture.size); auto uvMax = (frame.crop + frame.size) / vec2(texture.size); auto vertices = math::uv_vertices_get(uvMin, uvMax); - vec3 frameColorOffset = frame.offset + colorOffset; + vec3 frameColorOffset = frame.colorOffset + colorOffset; vec4 frameTint = frame.tint; frameTint.a = std::max(0.0f, frameTint.a - alphaOffset); @@ -199,48 +203,46 @@ namespace anm2ed::animation_preview auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : color::RED; - if (isBorder) rect_render(shaderLine, layerTransform, color); + if (isBorder) rect_render(shaderLine, layerTransform, layerModel, 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); + auto pivotModel = math::quad_model_get(PIVOT_SIZE, frame.position, PIVOT_SIZE * 0.5f, + math::percent_to_unit(frame.scale), frame.rotation); + auto pivotTransform = transform * pivotModel; texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color); } } } - if (isIcons) + for (auto& [id, nullAnimation] : animation->nullAnimations) { - for (auto& [id, nullAnimation] : animation->nullAnimations) + if (!nullAnimation.isVisible || isOnlyShowLayers) continue; + + auto& isShowRect = anm2.content.nulls[id].isShowRect; + + if (auto frame = nullAnimation.frame_generate(time, anm2::NULL_); frame.isVisible) { - if (!nullAnimation.isVisible) continue; + auto icon = isShowRect ? icon::POINT : icon::TARGET; + auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE; + auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) + : id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED + : NULL_COLOR; - auto& isShowRect = anm2.content.nulls[id].isShowRect; + auto nullModel = math::quad_model_get(size, frame.position, size * 0.5f, math::percent_to_unit(frame.scale), + frame.rotation); + auto nullTransform = transform * nullModel; - if (auto frame = nullAnimation.frame_generate(time, anm2::NULL_); frame.isVisible) + texture_render(shaderTexture, resources.icons[icon].id, nullTransform, color); + + if (isShowRect) { - auto icon = isShowRect ? icon::POINT : icon::TARGET; - auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE; - auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) - : id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED - : NULL_COLOR; + auto rectModel = math::quad_model_get(NULL_RECT_SIZE, frame.position, NULL_RECT_SIZE * 0.5f, + math::percent_to_unit(frame.scale), frame.rotation); + auto rectTransform = transform * rectModel; - auto nullTransform = transform * math::quad_model_get(size, frame.position, size * 0.5f, - math::percent_to_unit(frame.scale), frame.rotation); - - texture_render(shaderTexture, resources.icons[icon].id, nullTransform, color); - - if (isShowRect) - { - auto rectTransform = - transform * math::quad_model_get(NULL_RECT_SIZE, frame.position, NULL_RECT_SIZE * 0.5f, - math::percent_to_unit(frame.scale), frame.rotation); - - rect_render(shaderLine, rectTransform, color); - } + rect_render(shaderLine, rectTransform, rectModel, color); } } } @@ -252,7 +254,7 @@ namespace anm2ed::animation_preview { float useTime = time + (float)(direction * i); float alphaOffset = (1.0f / (count + 1)) * i; - render(useTime, color, alphaOffset, true); + render(animation, useTime, color, alphaOffset, true); } }; @@ -270,7 +272,10 @@ namespace anm2ed::animation_preview auto& isEnabled = settings.onionskinIsEnabled; if (drawOrder == draw_order::BELOW && isEnabled) onionskins_render(frameTime); - render(frameTime); + render(animation, frameTime); + if (overlayIndex > 0) + render(document.anm2.animation_get({overlayIndex - 1}), frameTime, {}, + 1.0f - math::uint8_to_float(overlayTransparency)); if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime); } diff --git a/src/animations.cpp b/src/animations.cpp index 9d2b208..30f4ee9 100644 --- a/src/animations.cpp +++ b/src/animations.cpp @@ -20,6 +20,8 @@ namespace anm2ed::animations auto& mergeMultiSelect = document.animationMergeMultiSelect; auto& mergeTarget = document.mergeTarget; + hovered = -1; + if (ImGui::Begin("Animations", &settings.windowIsAnimations)) { auto childSize = imgui::size_without_footer_get(); @@ -45,7 +47,7 @@ namespace anm2ed::animations if (imgui::selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i), animation.name, multiSelect.contains(i))) - reference = {(int)i}; + document.animation_set(i); if (ImGui::IsItemHovered()) hovered = i; ImGui::PopFont(); @@ -113,18 +115,7 @@ namespace anm2ed::animations 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; - } + document.animations_remove(); }; auto paste = [&]() @@ -191,7 +182,7 @@ namespace anm2ed::animations ImGui::SameLine(); imgui::shortcut(settings.shortcutRemove); - if (ImGui::Button("Remove", widgetSize)) document.animation_remove(); + if (ImGui::Button("Remove", widgetSize)) document.animations_remove(); imgui::set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate); ImGui::SameLine(); diff --git a/src/anm2.cpp b/src/anm2.cpp index 814f0cd..3ec5acf 100644 --- a/src/anm2.cpp +++ b/src/anm2.cpp @@ -17,17 +17,6 @@ using namespace glm; namespace anm2ed::anm2 { - - void Reference::previous_frame(int max) - { - frameIndex = glm::clamp(--frameIndex, 0, max); - } - - void Reference::next_frame(int max) - { - frameIndex = glm::clamp(++frameIndex, 0, max); - } - Info::Info() = default; Info::Info(XMLElement* element) @@ -466,9 +455,9 @@ namespace anm2ed::anm2 xml::query_color_attribute(element, "GreenTint", tint.g); xml::query_color_attribute(element, "BlueTint", tint.b); xml::query_color_attribute(element, "AlphaTint", tint.a); - xml::query_color_attribute(element, "RedOffset", offset.r); - xml::query_color_attribute(element, "GreenOffset", offset.g); - xml::query_color_attribute(element, "BlueOffset", offset.b); + xml::query_color_attribute(element, "RedOffset", colorOffset.r); + xml::query_color_attribute(element, "GreenOffset", colorOffset.g); + xml::query_color_attribute(element, "BlueOffset", colorOffset.b); element->QueryFloatAttribute("Rotation", &rotation); element->QueryBoolAttribute("Interpolated", &isInterpolated); } @@ -497,9 +486,9 @@ namespace anm2ed::anm2 element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); - element->SetAttribute("RedOffset", math::float_to_uint8(offset.r)); - element->SetAttribute("GreenOffset", math::float_to_uint8(offset.g)); - element->SetAttribute("BlueOffset", math::float_to_uint8(offset.b)); + element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); + element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); + element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); element->SetAttribute("Rotation", rotation); element->SetAttribute("Interpolated", isInterpolated); break; @@ -520,9 +509,9 @@ namespace anm2ed::anm2 element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); - element->SetAttribute("RedOffset", math::float_to_uint8(offset.r)); - element->SetAttribute("GreenOffset", math::float_to_uint8(offset.g)); - element->SetAttribute("BlueOffset", math::float_to_uint8(offset.b)); + element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); + element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); + element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); element->SetAttribute("Rotation", rotation); element->SetAttribute("Interpolated", isInterpolated); break; @@ -537,6 +526,69 @@ namespace anm2ed::anm2 parent->InsertEndChild(element); } + std::string Frame::to_string(Type type) + { + XMLDocument document; + auto element = document.NewElement(type == TRIGGER ? "Trigger" : "Frame"); + + switch (type) + { + case ROOT: + case NULL_: + element->SetAttribute("XPosition", position.x); + element->SetAttribute("YPosition", position.y); + element->SetAttribute("Delay", delay); + element->SetAttribute("Visible", isVisible); + element->SetAttribute("XScale", scale.x); + element->SetAttribute("YScale", scale.y); + element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); + element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); + element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); + element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); + element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); + element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); + element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); + element->SetAttribute("Rotation", rotation); + element->SetAttribute("Interpolated", isInterpolated); + break; + case LAYER: + element->SetAttribute("XPosition", position.x); + element->SetAttribute("YPosition", position.y); + element->SetAttribute("XPivot", pivot.x); + element->SetAttribute("YPivot", pivot.y); + element->SetAttribute("XCrop", crop.x); + element->SetAttribute("YCrop", crop.y); + element->SetAttribute("Width", size.x); + element->SetAttribute("Height", size.y); + element->SetAttribute("XScale", scale.x); + element->SetAttribute("YScale", scale.y); + element->SetAttribute("Delay", delay); + element->SetAttribute("Visible", isVisible); + element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); + element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); + element->SetAttribute("BlueTint", math::float_to_uint8(tint.b)); + element->SetAttribute("AlphaTint", math::float_to_uint8(tint.a)); + element->SetAttribute("RedOffset", math::float_to_uint8(colorOffset.r)); + element->SetAttribute("GreenOffset", math::float_to_uint8(colorOffset.g)); + element->SetAttribute("BlueOffset", math::float_to_uint8(colorOffset.b)); + element->SetAttribute("Rotation", rotation); + element->SetAttribute("Interpolated", isInterpolated); + break; + case TRIGGER: + element->SetAttribute("EventId", eventID); + element->SetAttribute("AtFrame", atFrame); + break; + default: + break; + } + + document.InsertFirstChild(element); + + XMLPrinter printer; + document.Print(&printer); + return std::string(printer.CStr()); + } + void Frame::shorten() { delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); @@ -640,13 +692,98 @@ namespace anm2ed::anm2 frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation); frame.position = glm::mix(frame.position, frameNext->position, interpolation); frame.scale = glm::mix(frame.scale, frameNext->scale, interpolation); - frame.offset = glm::mix(frame.offset, frameNext->offset, interpolation); + frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, interpolation); frame.tint = glm::mix(frame.tint, frameNext->tint, interpolation); } return frame; } + void Item::frames_change(anm2::FrameChange& change, frame_change::Type type, int start, int numberFrames) + { + auto useStart = numberFrames > -1 ? start : 0; + auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size(); + vector::clamp_in_bounds(frames, useStart); + end = glm::clamp(end, start, (int)frames.size()); + + for (int i = useStart; i < end; i++) + { + Frame& frame = frames[i]; + + if (change.isVisible) frame.isVisible = *change.isVisible; + if (change.isInterpolated) frame.isInterpolated = *change.isInterpolated; + + switch (type) + { + case frame_change::ADJUST: + if (change.rotation) frame.rotation = *change.rotation; + if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, *change.delay); + if (change.crop) frame.crop = *change.crop; + if (change.pivot) frame.pivot = *change.pivot; + if (change.position) frame.position = *change.position; + if (change.size) frame.size = *change.size; + if (change.scale) frame.scale = *change.scale; + if (change.colorOffset) frame.colorOffset = glm::clamp(*change.colorOffset, 0.0f, 1.0f); + if (change.tint) frame.tint = glm::clamp(*change.tint, 0.0f, 1.0f); + break; + + case frame_change::ADD: + if (change.rotation) frame.rotation += *change.rotation; + if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay + *change.delay); + if (change.crop) frame.crop += *change.crop; + if (change.pivot) frame.pivot += *change.pivot; + if (change.position) frame.position += *change.position; + if (change.size) frame.size += *change.size; + if (change.scale) frame.scale += *change.scale; + if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset + *change.colorOffset, 0.0f, 1.0f); + if (change.tint) frame.tint = glm::clamp(frame.tint + *change.tint, 0.0f, 1.0f); + break; + + case frame_change::SUBTRACT: + if (change.rotation) frame.rotation -= *change.rotation; + if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay - *change.delay); + if (change.crop) frame.crop -= *change.crop; + if (change.pivot) frame.pivot -= *change.pivot; + if (change.position) frame.position -= *change.position; + if (change.size) frame.size -= *change.size; + if (change.scale) frame.scale -= *change.scale; + if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset - *change.colorOffset, 0.0f, 1.0f); + if (change.tint) frame.tint = glm::clamp(frame.tint - *change.tint, 0.0f, 1.0f); + break; + } + } + } + + bool Item::frames_deserialize(const std::string& string, Type type, int start, std::set& indices, + std::string* errorString) + { + XMLDocument document{}; + + if (document.Parse(string.c_str()) == XML_SUCCESS) + { + if (!document.FirstChildElement("Frame")) + { + if (errorString) *errorString = "No valid frame(s)."; + return false; + } + + int count{}; + for (auto element = document.FirstChildElement("Frame"); element; element = element->NextSiblingElement("Frame")) + { + auto index = start + count; + frames.insert(frames.begin() + start + count, Frame(element, type)); + indices.insert(index); + count++; + } + + return true; + } + else if (errorString) + *errorString = document.ErrorStr(); + + return false; + } + Animation::Animation() = default; Animation::Animation(XMLElement* element) @@ -1048,12 +1185,12 @@ namespace anm2ed::anm2 return std::hash{}(to_string()); } - Animation* Anm2::animation_get(Reference& reference) + Animation* Anm2::animation_get(Reference reference) { return vector::find(animations.items, reference.animationIndex); } - Item* Anm2::item_get(Reference& reference) + Item* Anm2::item_get(Reference reference) { if (Animation* animation = animation_get(reference)) { @@ -1074,7 +1211,7 @@ namespace anm2ed::anm2 return nullptr; } - Frame* Anm2::frame_get(Reference& reference) + Frame* Anm2::frame_get(Reference reference) { Item* item = item_get(reference); if (!item) return nullptr; @@ -1260,7 +1397,7 @@ namespace anm2ed::anm2 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.colorOffset = glm::mix(baseFrame.colorOffset, baseFrameNext.colorOffset, interpolation); baked.tint = glm::mix(baseFrame.tint, baseFrameNext.tint, interpolation); if (isRoundScale) baked.scale = vec2(ivec2(baked.scale)); @@ -1275,4 +1412,26 @@ namespace anm2ed::anm2 delay += baked.delay; } } -} + + void Anm2::generate_from_grid(Reference reference, ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, + int count, int delay) + { + auto item = item_get(reference); + if (!item) return; + + for (int i = 0; i < count; i++) + { + auto row = i / columns; + auto column = i % columns; + + Frame frame{}; + + frame.delay = delay; + frame.pivot = pivot; + frame.size = size; + frame.crop = startPosition + ivec2(size.x * column, size.y * row); + + item->frames.emplace_back(frame); + } + } +} \ No newline at end of file diff --git a/src/anm2.h b/src/anm2.h index 935f71c..056cc84 100644 --- a/src/anm2.h +++ b/src/anm2.h @@ -44,8 +44,6 @@ namespace anm2ed::anm2 int frameIndex{-1}; int frameTime{-1}; - void previous_frame(int = FRAME_NUM_MAX - 1); - void next_frame(int = FRAME_NUM_MAX - 1); auto operator<=>(const Reference&) const = default; }; @@ -150,7 +148,7 @@ namespace anm2ed::anm2 X(position, glm::vec2, {}) \ X(size, glm::vec2, {}) \ X(scale, glm::vec2, glm::vec2(100.0f)) \ - X(offset, glm::vec3, types::color::TRANSPARENT) \ + X(colorOffset, glm::vec3, types::color::TRANSPARENT) \ X(tint, glm::vec4, types::color::WHITE) class Frame @@ -162,6 +160,7 @@ namespace anm2ed::anm2 Frame(); Frame(tinyxml2::XMLElement*, Type); + std::string to_string(Type type); void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type); void shorten(); void extend(); @@ -188,6 +187,8 @@ namespace anm2ed::anm2 void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type, int = -1); int length(Type); Frame frame_generate(float, Type); + void frames_change(anm2::FrameChange&, types::frame_change::Type, int, int = 0); + bool frames_deserialize(const std::string&, Type, int, std::set&, std::string*); }; class Animation @@ -238,9 +239,9 @@ namespace anm2ed::anm2 std::string to_string(); Anm2(const std::string&, std::string* = nullptr); uint64_t hash(); - Animation* animation_get(Reference&); - Item* item_get(Reference&); - Frame* frame_get(Reference&); + 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); @@ -255,5 +256,6 @@ namespace anm2ed::anm2 std::set nulls_unused(Reference = REFERENCE_DEFAULT); std::vector spritesheet_names_get(); void bake(Reference, int = 1, bool = true, bool = true); + void generate_from_grid(Reference, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); }; } diff --git a/src/canvas.cpp b/src/canvas.cpp index 526829b..2776404 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -226,13 +226,35 @@ namespace anm2ed::canvas glUseProgram(0); } - void Canvas::rect_render(Shader& shader, mat4& transform, vec4 color) + void Canvas::rect_render(Shader& shader, const mat4& transform, const mat4& model, vec4 color, float dashLength, + float dashGap, float dashOffset) { glUseProgram(shader.id); glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform)); + if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_MODEL); location != -1) + glUniformMatrix4fv(location, 1, GL_FALSE, value_ptr(model)); glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color)); + auto origin = model * vec4(0.0f, 0.0f, 0.0f, 1.0f); + auto edgeX = model * vec4(1.0f, 0.0f, 0.0f, 1.0f); + auto edgeY = model * vec4(0.0f, 1.0f, 0.0f, 1.0f); + + auto axisX = vec2(edgeX - origin); + auto axisY = vec2(edgeY - origin); + + if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_AXIS_X); location != -1) + glUniform2fv(location, 1, value_ptr(axisX)); + if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_AXIS_Y); location != -1) + glUniform2fv(location, 1, value_ptr(axisY)); + + if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_DASH_LENGTH); location != -1) + glUniform1f(location, dashLength); + if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_DASH_GAP); location != -1) + glUniform1f(location, dashGap); + if (auto location = glGetUniformLocation(shader.id, shader::UNIFORM_DASH_OFFSET); location != -1) + glUniform1f(location, dashOffset); + glBindVertexArray(rectVAO); glDrawArrays(GL_LINE_LOOP, 0, 4); diff --git a/src/canvas.h b/src/canvas.h index 271bc6e..925c62d 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -14,6 +14,10 @@ namespace anm2ed::canvas constexpr auto ZOOM_MAX = 2000.0f; constexpr auto POSITION_FORMAT = "Position: ({:8} {:8})"; + constexpr auto DASH_LENGTH = 4.0f; + constexpr auto DASH_GAP = 1.0f; + constexpr auto DASH_OFFSET = 1.0f; + class Canvas { public: @@ -39,13 +43,14 @@ namespace anm2ed::canvas void framebuffer_set(); void framebuffer_resize_check(); void size_set(glm::vec2); - glm::mat4 transform_get(float, glm::vec2); + glm::mat4 transform_get(float = 100.0f, 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 rect_render(shader::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f), + float dashLength = DASH_LENGTH, float dashGap = DASH_GAP, float dashOffset = DASH_OFFSET); void viewport_set(); void clear(glm::vec4&); void bind(); diff --git a/src/dockspace.cpp b/src/dockspace.cpp index f77f28d..ec28d91 100644 --- a/src/dockspace.cpp +++ b/src/dockspace.cpp @@ -13,6 +13,7 @@ using namespace anm2ed::playback; using namespace anm2ed::resources; using namespace anm2ed::settings; using namespace anm2ed::taskbar; +using namespace anm2ed::welcome; namespace anm2ed::dockspace { @@ -32,9 +33,9 @@ namespace anm2ed::dockspace ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus)) { - if (ImGui::DockSpace(ImGui::GetID("##DockSpace"), ImVec2(), ImGuiDockNodeFlags_PassthruCentralNode)) + if (auto document = manager.get(); document) { - if (auto document = manager.get(); document) + if (ImGui::DockSpace(ImGui::GetID("##DockSpace"), ImVec2(), ImGuiDockNodeFlags_PassthruCentralNode)) { if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources); if (settings.windowIsAnimations) animations.update(manager, settings, resources, clipboard); @@ -45,10 +46,12 @@ namespace anm2ed::dockspace if (settings.windowIsOnionskin) onionskin.update(settings); if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources); if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard); - if (settings.windowIsTimeline) timeline.update(manager, settings, resources); + if (settings.windowIsTimeline) timeline.update(manager, settings, resources, clipboard); if (settings.windowIsTools) tools.update(manager, settings, resources); } } + else + welcome.update(manager, resources, dialog, taskbar, documents); } ImGui::End(); } diff --git a/src/dockspace.h b/src/dockspace.h index 04ef75f..532fed3 100644 --- a/src/dockspace.h +++ b/src/dockspace.h @@ -13,6 +13,7 @@ #include "taskbar.h" #include "timeline.h" #include "tools.h" +#include "welcome.h" namespace anm2ed::dockspace { @@ -29,6 +30,7 @@ namespace anm2ed::dockspace spritesheets::Spritesheets spritesheets; timeline::Timeline timeline; tools::Tools tools; + welcome::Welcome welcome; public: void update(taskbar::Taskbar&, documents::Documents&, manager::Manager&, settings::Settings&, resources::Resources&, diff --git a/src/document.cpp b/src/document.cpp index ff46ba3..a24ccb4 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -1,17 +1,20 @@ #include "document.h" +#include +#include + #include "anm2.h" #include "filesystem.h" +#include "log.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; +using namespace anm2ed::log; using namespace glm; @@ -40,9 +43,28 @@ namespace anm2ed::document if (anm2.serialize(this->path, errorString)) { + toasts.info(std::format("Saved document to: {}", path)); clean(); return true; } + else if (errorString) + toasts.warning(std::format("Could not save document to: {} ({})", path, *errorString)); + + return false; + } + + bool Document::autosave(const std::string& path, std::string* errorString) + { + if (anm2.serialize(path, errorString)) + { + autosaveHash = hash; + lastAutosaveTime = 0.0f; + toasts.info("Autosaving..."); + logger.info(std::format("Autosaved document to: {}", path)); + return true; + } + else if (errorString) + toasts.warning(std::format("Could not autosave document to: {} ({})", path, *errorString)); return false; } @@ -56,6 +78,8 @@ namespace anm2ed::document { saveHash = anm2.hash(); hash = saveHash; + lastAutosaveTime = 0.0f; + isForceDirty = false; } void Document::change(change::Type type) @@ -88,6 +112,8 @@ namespace anm2ed::document case change::SPRITESHEETS: spritesheet_set(); break; + case change::ITEMS: + break; case change::ALL: layer_set(); null_set(); @@ -104,14 +130,19 @@ namespace anm2ed::document return hash != saveHash; } - std::string Document::directory_get() + bool Document::is_autosave_dirty() + { + return hash != autosaveHash; + } + + std::filesystem::path Document::directory_get() { return path.parent_path(); } - std::string Document::filename_get() + std::filesystem::path Document::filename_get() { - return path.filename().string(); + return path.filename(); } bool Document::is_valid() @@ -224,11 +255,11 @@ namespace anm2ed::document change(change::FRAMES); } - void Document::frame_offset_set(anm2::Frame* frame, vec3 offset) + void Document::frame_color_offset_set(anm2::Frame* frame, vec3 colorOffset) { if (!frame) return; snapshot("Frame Color Offset"); - frame->offset = offset; + frame->colorOffset = colorOffset; change(change::FRAMES); } @@ -264,6 +295,52 @@ namespace anm2ed::document change(change::FRAMES); } + void Document::frame_shorten() + { + auto frame = frame_get(); + if (!frame) return; + snapshot("Shorten Frame"); + frame->shorten(); + change(change::FRAMES); + } + + void Document::frame_extend() + { + auto frame = frame_get(); + if (!frame) return; + snapshot("Extend Frame"); + frame->extend(); + change(change::FRAMES); + } + + void Document::frames_change(anm2::FrameChange& frameChange, frame_change::Type type, bool isFromSelectedFrame, + int numberFrames) + { + auto item = item_get(); + if (!item) return; + snapshot("Change All Frame Properties"); + item->frames_change(frameChange, type, isFromSelectedFrame && frame_get() ? reference.frameIndex : 0, + isFromSelectedFrame ? numberFrames : -1); + change(change::FRAMES); + } + + void Document::frames_deserialize(const std::string& string) + { + if (auto item = item_get()) + { + snapshot("Paste Frame(s)"); + std::set indices{}; + std::string errorString{}; + auto start = reference.frameIndex + 1; + if (item->frames_deserialize(string, reference.itemType, start, indices, &errorString)) + change(change::FRAMES); + else + toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); + } + else + toasts.error(std::format("Failed to deserialize frame(s): select an item first!")); + } + anm2::Item* Document::item_get() { return anm2.item_get(reference); @@ -446,6 +523,13 @@ namespace anm2ed::document return anm2.animation_get(reference); } + void Document::animation_set(int index) + { + snapshot("Select Animation"); + reference = {index}; + change(change::ITEMS); + } + void Document::animation_add() { snapshot("Add Animation"); @@ -489,12 +573,21 @@ namespace anm2ed::document change(change::ANIMATIONS); } - void Document::animation_remove() + void Document::animations_remove() { snapshot("Remove Animation(s)"); - for (auto& i : animationMultiSelect | std::views::reverse) - anm2.animations.items.erase(anm2.animations.items.begin() + i); - animationMultiSelect.clear(); + + if (!animationMultiSelect.empty()) + { + for (auto& i : animationMultiSelect | std::views::reverse) + anm2.animations.items.erase(anm2.animations.items.begin() + i); + animationMultiSelect.clear(); + } + else if (hoveredAnimation > -1) + { + anm2.animations.items.erase(anm2.animations.items.begin() + hoveredAnimation); + hoveredAnimation = -1; + } change(change::ANIMATIONS); } @@ -508,7 +601,7 @@ namespace anm2ed::document void Document::animations_deserialize(const std::string& string) { - snapshot("Paste Animations"); + snapshot("Paste Animation(s)"); auto& multiSelect = animationMultiSelect; auto start = multiSelect.empty() ? anm2.animations.items.size() : *multiSelect.rbegin() + 1; std::set indices{}; @@ -522,9 +615,21 @@ namespace anm2ed::document toasts.error(std::format("Failed to deserialize animation(s): {}", errorString)); } + void Document::generate_animation_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, + int delay) + { + snapshot("Generate Animation from Grid"); + + anm2.generate_from_grid(reference, startPosition, size, pivot, columns, count, delay); + + if (auto animation = animation_get()) animation->frameNum = animation->length(); + + change(change::ALL); + } + void Document::animations_merge_quick() { - snapshot("Merge Animations"); + snapshot("Merge Animation(s)"); int merged{}; if (animationMultiSelect.size() > 1) merged = anm2.animations.merge(*animationMultiSelect.begin(), animationMultiSelect); diff --git a/src/document.h b/src/document.h index d7c92c2..46ec309 100644 --- a/src/document.h +++ b/src/document.h @@ -26,6 +26,7 @@ namespace anm2ed::document glm::vec2 previewPan{}; glm::vec2 editorPan{}; float editorZoom{200}; + int overlayIndex{}; anm2::Reference reference{}; @@ -34,6 +35,8 @@ namespace anm2ed::document imgui::MultiSelectStorage animationMultiSelect; imgui::MultiSelectStorage animationMergeMultiSelect; + anm2::Reference hoveredFrame{anm2::REFERENCE_DEFAULT}; + int referenceSpritesheet{-1}; int hoveredSpritesheet{-1}; std::set unusedSpritesheetIDs{}; @@ -58,17 +61,22 @@ namespace anm2ed::document uint64_t hash{}; uint64_t saveHash{}; + uint64_t autosaveHash{}; + double lastAutosaveTime{}; bool isOpen{true}; + bool isForceDirty{false}; Document(const std::string&, bool = false, std::string* = nullptr); bool save(const std::string& = {}, std::string* = nullptr); + bool autosave(const std::string&, std::string* = nullptr); void hash_set(); void clean(); void on_change(); void change(types::change::Type); bool is_dirty(); - std::string directory_get(); - std::string filename_get(); + bool is_autosave_dirty(); + std::filesystem::path directory_get(); + std::filesystem::path filename_get(); bool is_valid(); anm2::Frame* frame_get(); @@ -84,11 +92,15 @@ namespace anm2ed::document void frame_rotation_set(anm2::Frame*, float); void frame_delay_set(anm2::Frame*, int); void frame_tint_set(anm2::Frame*, glm::vec4); - void frame_offset_set(anm2::Frame*, glm::vec3); + void frame_color_offset_set(anm2::Frame*, glm::vec3); void frame_is_visible_set(anm2::Frame*, bool); void frame_is_interpolated_set(anm2::Frame*, bool); void frame_flip_x(anm2::Frame* frame); void frame_flip_y(anm2::Frame* frame); + void frame_shorten(); + void frame_extend(); + void frames_change(anm2::FrameChange&, types::frame_change::Type, bool, int = -1); + void frames_deserialize(const std::string&); anm2::Item* item_get(); void item_add(anm2::Type, int, std::string&, types::locale::Type, int); @@ -113,15 +125,18 @@ namespace anm2ed::document void events_deserialize(const std::string&, types::merge::Type); void animation_add(); + void animation_set(int); void animation_duplicate(); void animation_default(); - void animation_remove(); + void animations_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 generate_animation_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); + void snapshot(const std::string& message); void undo(); void redo(); diff --git a/src/documents.cpp b/src/documents.cpp index ebfa28a..5fae943 100644 --- a/src/documents.cpp +++ b/src/documents.cpp @@ -1,16 +1,19 @@ #include "documents.h" -#include +#include -#include "imgui.h" +#include "util.h" using namespace anm2ed::taskbar; using namespace anm2ed::manager; +using namespace anm2ed::settings; using namespace anm2ed::resources; +using namespace anm2ed::types; +using namespace anm2ed::util; namespace anm2ed::documents { - void Documents::update(Taskbar& taskbar, Manager& manager, Resources& resources) + void Documents::update(Taskbar& taskbar, Manager& manager, Settings& settings, Resources& resources, bool& isQuitting) { auto viewport = ImGui::GetMainViewport(); auto windowHeight = ImGui::GetFrameHeightWithSpacing(); @@ -19,6 +22,14 @@ namespace anm2ed::documents ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height)); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight)); + for (auto& document : manager.documents) + { + auto isDirty = document.is_dirty() && document.is_autosave_dirty(); + document.lastAutosaveTime += ImGui::GetIO().DeltaTime; + + if (isDirty && document.lastAutosaveTime > time::SECOND_S) manager.autosave(document); + } + if (ImGui::Begin("##Documents", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | @@ -29,14 +40,48 @@ namespace anm2ed::documents if (ImGui::BeginTabBar("Documents Bar", ImGuiTabBarFlags_Reorderable)) { - for (auto [i, document] : std::views::enumerate(manager.documents)) + auto documentsCount = (int)manager.documents.size(); + bool closeShortcut = imgui::shortcut(settings.shortcutClose, shortcut::GLOBAL) && !closePopup.is_open(); + int closeShortcutIndex = + closeShortcut && manager.selected >= 0 && manager.selected < documentsCount ? manager.selected : -1; + + std::vector closeIndices{}; + closeIndices.reserve(documentsCount); + + for (int i = 0; i < documentsCount; ++i) { - auto isDirty = document.is_dirty(); + auto& document = manager.documents[i]; + auto isDirty = document.is_dirty() || document.isForceDirty; + + if (!closePopup.is_open()) + { + if (isQuitting) + document.isOpen = false; + else if (i == closeShortcutIndex) + document.isOpen = false; + } + + if (!closePopup.is_open() && !document.isOpen) + { + if (isDirty) + { + closePopup.open(); + closeDocumentIndex = i; + document.isOpen = true; + } + else + { + closeIndices.push_back(i); + continue; + } + } + auto isRequested = i == manager.pendingSelected; auto font = isDirty ? font::ITALICS : font::REGULAR; - auto string = isDirty ? std::format("[Not Saved] {}", document.filename_get()) : document.filename_get(); + auto string = isDirty ? std::format("[Not Saved] {}", document.filename_get().string()) + : document.filename_get().string(); auto label = std::format("{}###Document{}", string, i); @@ -46,24 +91,17 @@ namespace anm2ed::documents ImGui::PushFont(resources.fonts[font].get(), font::SIZE); if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags)) { - manager.selected = i; + manager.set(i); if (isRequested) manager.pendingSelected = -1; ImGui::EndTabItem(); } ImGui::PopFont(); + } - if (!document.isOpen) - { - if (isDirty) - { - isCloseDocument = true; - isOpenCloseDocumentPopup = true; - closeDocumentIndex = i; - document.isOpen = true; - } - else - manager.close(i); - } + for (auto it = closeIndices.rbegin(); it != closeIndices.rend(); ++it) + { + if (closePopup.is_open() && closeDocumentIndex > *it) --closeDocumentIndex; + manager.close(*it); } ImGui::EndTabBar(); @@ -71,21 +109,21 @@ namespace anm2ed::documents closePopup.trigger(); - if (isCloseDocument) + if (ImGui::BeginPopupModal(closePopup.label, &closePopup.isOpen, ImGuiWindowFlags_NoResize)) { - if (ImGui::BeginPopupModal(closePopup.label, &closePopup.isOpen, ImGuiWindowFlags_NoResize)) + if (closeDocumentIndex >= 0 && closeDocumentIndex < (int)manager.documents.size()) { - auto closeDocument = manager.get(closeDocumentIndex); + auto& closeDocument = manager.documents[closeDocumentIndex]; ImGui::TextUnformatted(std::format("The document \"{}\" has been modified.\nDo you want to save it?", - closeDocument->filename_get()) + closeDocument.filename_get().string()) .c_str()); auto widgetSize = imgui::widget_size_with_row_get(3); auto close = [&]() { - closeDocumentIndex = 0; + closeDocumentIndex = -1; closePopup.close(); }; @@ -106,10 +144,19 @@ namespace anm2ed::documents ImGui::SameLine(); - if (ImGui::Button("Cancel", widgetSize)) close(); - - ImGui::EndPopup(); + if (ImGui::Button("Cancel", widgetSize)) + { + isQuitting = false; + close(); + } } + else + { + closeDocumentIndex = -1; + closePopup.close(); + } + + ImGui::EndPopup(); } } diff --git a/src/documents.h b/src/documents.h index 4aa184a..80050cc 100644 --- a/src/documents.h +++ b/src/documents.h @@ -3,20 +3,19 @@ #include "imgui.h" #include "manager.h" #include "resources.h" +#include "settings.h" #include "taskbar.h" namespace anm2ed::documents { class Documents { - bool isCloseDocument{}; - bool isOpenCloseDocumentPopup{}; - int closeDocumentIndex{}; + int closeDocumentIndex{-1}; imgui::PopupHelper closePopup{imgui::PopupHelper("Close Document", imgui::POPUP_TO_CONTENT)}; public: float height{}; - void update(taskbar::Taskbar&, manager::Manager&, resources::Resources&); + void update(taskbar::Taskbar&, manager::Manager&, settings::Settings&, resources::Resources&, bool&); }; } diff --git a/src/events.cpp b/src/events.cpp index 46def77..cc8a417 100644 --- a/src/events.cpp +++ b/src/events.cpp @@ -18,6 +18,8 @@ namespace anm2ed::events auto& hovered = document.hoveredEvent; auto& multiSelect = document.eventMultiSelect; + hovered = -1; + if (ImGui::Begin("Events", &settings.windowIsEvents)) { auto childSize = imgui::size_without_footer_get(); diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 057fa95..c62dcaa 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -6,6 +6,11 @@ namespace anm2ed::filesystem { + bool directories_create(const std::string& path) + { + return std::filesystem::create_directories(path); + } + std::string path_preferences_get() { char* preferencesPath = SDL_GetPrefPath("", "anm2ed"); diff --git a/src/filesystem.h b/src/filesystem.h index 0959d78..2c2db96 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -8,6 +8,7 @@ namespace anm2ed::filesystem std::string path_preferences_get(); bool path_is_exist(const std::string&); bool path_is_extension(const std::string&, const std::string&); + bool directories_create(const std::string&); class WorkingDirectory { diff --git a/src/frame_properties.cpp b/src/frame_properties.cpp index 2ab5320..7a086dc 100644 --- a/src/frame_properties.cpp +++ b/src/frame_properties.cpp @@ -91,8 +91,8 @@ namespace anm2ed::frame_properties document.frame_tint_set(frame, useFrame.tint); ImGui::SetItemTooltip("%s", "Change the tint of the frame."); - if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.offset) : &dummy_value())) - document.frame_offset_set(frame, useFrame.offset); + if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value())) + document.frame_color_offset_set(frame, useFrame.colorOffset); ImGui::SetItemTooltip("%s", "Change the color added onto the frame."); if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value())) diff --git a/src/icon.h b/src/icon.h index 7af6181..50ab178 100644 --- a/src/icon.h +++ b/src/icon.h @@ -55,6 +55,14 @@ namespace icon constexpr auto HIDE_UNUSED_DATA = R"( +)"; + + constexpr auto SHOW_LAYERS_DATA = R"( + +)"; + + constexpr auto HIDE_LAYERS_DATA = R"( + )"; constexpr auto SHOW_RECT_DATA = R"( @@ -156,6 +164,8 @@ namespace icon X(HIDE_RECT, HIDE_RECT_DATA, SIZE_NORMAL) \ X(SHOW_UNUSED, SHOW_UNUSED_DATA, SIZE_NORMAL) \ X(HIDE_UNUSED, HIDE_UNUSED_DATA, SIZE_NORMAL) \ + X(SHOW_LAYERS, SHOW_LAYERS_DATA, SIZE_NORMAL) \ + X(HIDE_LAYERS, HIDE_LAYERS_DATA, SIZE_NORMAL) \ X(PAN, PAN_DATA, SIZE_NORMAL) \ X(MOVE, MOVE_DATA, SIZE_NORMAL) \ X(ROTATE, ROTATE_DATA, SIZE_NORMAL) \ diff --git a/src/imgui.cpp b/src/imgui.cpp index 1224aad..d5a1713 100644 --- a/src/imgui.cpp +++ b/src/imgui.cpp @@ -280,6 +280,11 @@ namespace anm2ed::imgui isJustOpened = true; } + bool PopupHelper::is_open() + { + return isOpen; + } + void PopupHelper::trigger() { if (isTriggered) ImGui::OpenPopup(label); diff --git a/src/imgui.h b/src/imgui.h index 2deec12..7e032ec 100644 --- a/src/imgui.h +++ b/src/imgui.h @@ -174,6 +174,7 @@ namespace anm2ed::imgui float percent{}; PopupHelper(const char*, float = POPUP_NORMAL, bool = false); + bool is_open(); void open(); void trigger(); void end(); diff --git a/src/layers.cpp b/src/layers.cpp index 69b1546..e7ae3a3 100644 --- a/src/layers.cpp +++ b/src/layers.cpp @@ -21,6 +21,8 @@ namespace anm2ed::layers auto& multiSelect = document.layersMultiSelect; auto& propertiesPopup = manager.layerPropertiesPopup; + hovered = -1; + if (ImGui::Begin("Layers", &settings.windowIsLayers)) { auto childSize = imgui::size_without_footer_get(); diff --git a/src/loader.cpp b/src/loader.cpp index 034c126..bcb82d5 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -73,7 +73,7 @@ namespace anm2ed::loader ImGuiIO& io = ImGui::GetIO(); io.IniFilename = nullptr; - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable | ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigWindowsMoveFromTitleBarOnly = true; ImGui::LoadIniSettingsFromDisk(settings_path().c_str()); diff --git a/src/manager.cpp b/src/manager.cpp index a839622..7be966d 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1,33 +1,73 @@ #include "manager.h" -#include "toast.h" +#include +#include "filesystem.h" +#include "log.h" +#include "toast.h" #include "util.h" +using namespace anm2ed::log; using namespace anm2ed::toast; using namespace anm2ed::types; using namespace anm2ed::util; namespace anm2ed::manager { + constexpr std::size_t RECENT_LIMIT = 10; + + std::filesystem::path Manager::recent_files_path_get() + { + return filesystem::path_preferences_get() + "recent.txt"; + } + + std::filesystem::path Manager::autosave_path_get() + { + return filesystem::path_preferences_get() + "autosave.txt"; + } + + std::filesystem::path Manager::autosave_directory_get() + { + return filesystem::path_preferences_get() + "autosave"; + } + + Manager::Manager() + { + recent_files_load(); + autosave_files_load(); + } + Document* Manager::get(int index) { return vector::find(documents, index > -1 ? index : selected); } - void Manager::open(const std::string& path, bool isNew) + void Manager::open(const std::string& path, bool isNew, bool isRecent) { std::string errorString{}; - Document document = Document(path, isNew, &errorString); - if (document.is_valid()) + documents.emplace_back(path, isNew, &errorString); + + auto& document = documents.back(); + if (!document.is_valid()) { - documents.emplace_back(std::move(document)); - selected = documents.size() - 1; - pendingSelected = selected; - toasts.info(std::format("Initialized document: {}", path)); + documents.pop_back(); + toasts.error(std::format("Failed to open document: {} ({})", path, errorString)); + return; } - else - toasts.error(std::format("Failed to initialize document: {} ({})", path, errorString)); + + if (isRecent) + { + recentFiles.erase(std::remove(recentFiles.begin(), recentFiles.end(), path), recentFiles.end()); + recentFiles.insert(recentFiles.begin(), path); + + if (recentFiles.size() > RECENT_LIMIT) recentFiles.resize(RECENT_LIMIT); + + recent_files_write(); + } + + selected = (int)documents.size() - 1; + pendingSelected = selected; + toasts.info(std::format("Opened document: {}", path)); } void Manager::new_(const std::string& path) @@ -50,9 +90,53 @@ namespace anm2ed::manager save(selected, path); } + void Manager::autosave(Document& document) + { + auto filename = "." + document.filename_get().string() + ".autosave"; + auto path = document.directory_get() / filename; + std::string errorString{}; + document.autosave(path, &errorString); + + autosaveFiles.erase(std::remove(autosaveFiles.begin(), autosaveFiles.end(), path), autosaveFiles.end()); + autosaveFiles.insert(autosaveFiles.begin(), path); + + autosave_files_write(); + } + void Manager::close(int index) { + if (index < 0 || index >= (int)documents.size()) return; + documents.erase(documents.begin() + index); + + if (documents.empty()) + { + selected = -1; + pendingSelected = -1; + return; + } + + if (selected >= index) selected = std::max(0, selected - 1); + + selected = std::clamp(selected, 0, (int)documents.size() - 1); + pendingSelected = selected; + + if (selected >= 0 && selected < (int)documents.size()) documents[selected].change(change::ALL); + } + + void Manager::set(int index) + { + if (documents.empty()) + { + selected = -1; + pendingSelected = -1; + return; + } + + index = std::clamp(index, 0, (int)documents.size() - 1); + selected = index; + + if (auto document = get()) document->change(change::ALL); } void Manager::layer_properties_open(int id) @@ -117,4 +201,119 @@ namespace anm2ed::manager nullPropertiesPopup.close(); } + void Manager::recent_files_load() + { + auto path = recent_files_path_get(); + + std::ifstream file(path); + if (!file) + { + logger.warning(std::format("Could not load recent files from: {}. Skipping...", path.string())); + return; + } + + logger.info(std::format("Loading recent files from: {}", path.string())); + + std::string line{}; + + while (std::getline(file, line)) + { + if (line.empty()) continue; + if (std::find(recentFiles.begin(), recentFiles.end(), line) != recentFiles.end()) continue; + recentFiles.emplace_back(line); + } + } + + void Manager::recent_files_write() + { + auto path = recent_files_path_get(); + + std::ofstream file; + file.open(path, std::ofstream::out | std::ofstream::trunc); + + if (!file.is_open()) + { + logger.warning(std::format("Could not write recent files to: {}. Skipping...", path.string())); + return; + } + + for (auto& path : recentFiles) + file << path.string() << '\n'; + } + + void Manager::recent_files_clear() + { + recentFiles.clear(); + recent_files_write(); + } + + void Manager::autosave_files_open() + { + for (auto& path : autosaveFiles) + { + auto fileName = path.filename().string(); + if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin()); + + auto restorePath = path.parent_path() / fileName; + restorePath.replace_extension(""); + open(path.string(), false, false); + + if (auto document = get()) + { + document->isForceDirty = true; + document->path = restorePath; + document->change(change::ALL); + } + } + + autosave_files_clear(); + } + + void Manager::autosave_files_load() + { + auto path = autosave_path_get(); + + std::ifstream file(path); + if (!file) + { + logger.warning(std::format("Could not load autosave files from: {}. Skipping...", path.string())); + return; + } + + logger.info(std::format("Loading autosave files from: {}", path.string())); + + std::string line{}; + + while (std::getline(file, line)) + { + if (line.empty()) continue; + if (std::find(autosaveFiles.begin(), autosaveFiles.end(), line) != autosaveFiles.end()) continue; + autosaveFiles.emplace_back(line); + } + } + + void Manager::autosave_files_write() + { + std::ofstream autosaveWriteFile; + autosaveWriteFile.open(autosave_path_get(), std::ofstream::out | std::ofstream::trunc); + + for (auto& path : autosaveFiles) + autosaveWriteFile << path.string() << "\n"; + + autosaveWriteFile.close(); + } + + void Manager::autosave_files_clear() + { + for (auto& path : autosaveFiles) + std::filesystem::remove(path); + + autosaveFiles.clear(); + autosave_files_write(); + } + + Manager::~Manager() + { + autosave_files_clear(); + } } diff --git a/src/manager.h b/src/manager.h index 1d46381..4a4417c 100644 --- a/src/manager.h +++ b/src/manager.h @@ -9,12 +9,20 @@ using namespace anm2ed::document; namespace anm2ed::manager { + constexpr auto FILE_LABEL_FORMAT = "{} [{}]"; + class Manager { + std::filesystem::path recent_files_path_get(); + std::filesystem::path autosave_path_get(); + public: std::vector documents{}; - int selected{}; - int pendingSelected{}; + std::vector recentFiles{}; + std::vector autosaveFiles{}; + + int selected{-1}; + int pendingSelected{-1}; anm2::Layer editLayer{}; imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL, true)}; @@ -22,11 +30,16 @@ namespace anm2ed::manager anm2::Null editNull{}; imgui::PopupHelper nullPropertiesPopup{imgui::PopupHelper("Null Properties", imgui::POPUP_SMALL, true)}; + Manager(); + ~Manager(); + Document* get(int = -1); - void open(const std::string&, bool = false); + void open(const std::string&, bool = false, bool = true); void new_(const std::string&); void save(int, const std::string& = {}); void save(const std::string& = {}); + void autosave(Document&); + void set(int); void close(int); void layer_properties_open(int = -1); void layer_properties_trigger(); @@ -36,5 +49,16 @@ namespace anm2ed::manager void null_properties_trigger(); void null_properties_end(); void null_properties_close(); + + void recent_files_load(); + void recent_files_write(); + void recent_files_clear(); + + void autosave_files_load(); + void autosave_files_open(); + void autosave_files_write(); + void autosave_files_clear(); + + std::filesystem::path autosave_directory_get(); }; } diff --git a/src/nulls.cpp b/src/nulls.cpp index f40dd9e..42879e1 100644 --- a/src/nulls.cpp +++ b/src/nulls.cpp @@ -20,6 +20,8 @@ namespace anm2ed::nulls auto& multiSelect = document.nullMultiSelect; auto& propertiesPopup = manager.nullPropertiesPopup; + hovered = -1; + if (ImGui::Begin("Nulls", &settings.windowIsNulls)) { auto childSize = imgui::size_without_footer_get(); diff --git a/src/settings.h b/src/settings.h index 535c27a..9f9aa3d 100644 --- a/src/settings.h +++ b/src/settings.h @@ -44,6 +44,9 @@ namespace anm2ed::settings X(IS_VSYNC, isVsync, "Vsync", BOOL, true) \ X(DISPLAY_SCALE, displayScale, "Display Scale", FLOAT, 1.0f) \ \ + X(FILE_IS_AUTOSAVE, fileIsAutosave, "Autosave", BOOL, true) \ + X(FILE_AUTOSAVE_TIME, fileAutosaveTime, "Autosave Time", INT, 1) \ + \ X(VIEW_ZOOM_STEP, viewZoomStep, "Zoom Step", FLOAT, 50.0f) \ \ X(PLAYBACK_IS_LOOP, playbackIsLoop, "Loop", BOOL, true) \ @@ -60,7 +63,6 @@ namespace anm2ed::settings X(CHANGE_IS_COLOR_OFFSET, changeIsColorOffset, "##Is Color Offset", BOOL, false) \ X(CHANGE_IS_VISIBLE_SET, changeIsVisibleSet, "##Is Visible", BOOL, false) \ X(CHANGE_IS_INTERPOLATED_SET, changeIsInterpolatedSet, "##Is Interpolated", BOOL, false) \ - X(CHANGE_IS_FROM_SELECTED_FRAME, changeIsFromSelectedFrame, "From Selected Frame", BOOL, false) \ X(CHANGE_CROP, changeCrop, "Crop", VEC2, {}) \ X(CHANGE_SIZE, changeSize, "Size", VEC2, {}) \ X(CHANGE_POSITION, changePosition, "Position", VEC2, {}) \ @@ -73,6 +75,7 @@ namespace anm2ed::settings X(CHANGE_IS_VISIBLE, changeIsVisible, "Visible", BOOL, false) \ X(CHANGE_IS_INTERPOLATED, changeIsInterpolated, "Interpolated", BOOL, false) \ X(CHANGE_NUMBER_FRAMES, changeNumberFrames, "Frame Count", INT, 1) \ + X(CHANGE_IS_FROM_SELECTED_FRAME, changeIsFromSelectedFrame, "From Selected Frame", BOOL, false) \ \ X(SCALE_VALUE, scaleValue, "Scale", FLOAT, 1.0f) \ \ @@ -80,7 +83,6 @@ namespace anm2ed::settings X(PREVIEW_IS_GRID, previewIsGrid, "Grid", BOOL, true) \ X(PREVIEW_IS_ROOT_TRANSFORM, previewIsRootTransform, "Root Transform", BOOL, true) \ X(PREVIEW_IS_PIVOTS, previewIsPivots, "Pivots", BOOL, false) \ - X(PREVIEW_IS_ICONS, previewIsIcons, "Icons", BOOL, true) \ X(PREVIEW_IS_BORDER, previewIsBorder, "Border", BOOL, false) \ X(PREVIEW_IS_ALT_ICONS, previewIsAltIcons, "Alt Icons", BOOL, false) \ X(PREVIEW_OVERLAY_TRANSPARENCY, previewOverlayTransparency, "Alpha", FLOAT, 255) \ @@ -101,6 +103,7 @@ namespace anm2ed::settings X(GENERATE_COLUMNS, generateColumns, "Columns", INT, 4) \ X(GENERATE_COUNT, generateCount, "Count", INT, 16) \ X(GENERATE_DELAY, generateDelay, "Delay", INT, 1) \ + X(GENERATE_ZOOM, generateZoom, "Zoom", FLOAT, 100.0f) \ \ X(EDITOR_IS_GRID, editorIsGrid, "Grid", BOOL, true) \ X(EDITOR_IS_GRID_SNAP, editorIsGridSnap, "Snap", BOOL, true) \ @@ -123,6 +126,7 @@ namespace anm2ed::settings X(TIMELINE_ADD_ITEM_LOCALITY, timelineAddItemLocale, "Add Item Locale", INT, types::locale::GLOBAL) \ X(TIMELINE_ADD_ITEM_SOURCE, timelineAddItemSource, "Add Item Source", INT, types::source::NEW) \ X(TIMELINE_IS_SHOW_UNUSED, timelineIsShowUnused, "##Show Unused", BOOL, true) \ + X(TIMELINE_IS_ONLY_SHOW_LAYERS, timelineIsOnlyShowLayers, "##Only Show Layers", BOOL, true) \ \ X(ONIONSKIN_IS_ENABLED, onionskinIsEnabled, "Enabled", BOOL, false) \ X(ONIONSKIN_DRAW_ORDER, onionskinDrawOrder, "Draw Order", INT, 0) \ diff --git a/src/shader.h b/src/shader.h index be761fc..ab7e0f4 100644 --- a/src/shader.h +++ b/src/shader.h @@ -112,6 +112,79 @@ namespace anm2ed::shader } )"; + constexpr auto DASHED_VERTEX = R"( + #version 330 core + layout (location = 0) in vec2 i_position; + + out vec2 v_local; + + uniform mat4 u_transform; + + void main() + { + v_local = i_position; + gl_Position = u_transform * vec4(i_position, 0.0, 1.0); + } + )"; + + constexpr auto DASHED_FRAGMENT = R"( + #version 330 core + in vec2 v_local; + + uniform vec4 u_color; + uniform vec2 u_axis_x; + uniform vec2 u_axis_y; + uniform float u_dash_length; + uniform float u_dash_gap; + uniform float u_dash_offset; + + out vec4 o_fragColor; + + void main() + { + vec2 local = clamp(v_local, 0.0, 1.0); + + float lengthX = max(length(u_axis_x), 1e-4); + float lengthY = max(length(u_axis_y), 1e-4); + + float dash = max(u_dash_length, 1e-4); + float gap = max(u_dash_gap, 0.0); + float period = max(dash + gap, 1e-4); + + vec2 pixel = max(fwidth(v_local), vec2(1e-5)); + + float bottomMask = 1.0 - smoothstep(pixel.y, pixel.y * 2.0, local.y); + float topMask = 1.0 - smoothstep(pixel.y, pixel.y * 2.0, 1.0 - local.y); + float leftMask = 1.0 - smoothstep(pixel.x, pixel.x * 2.0, local.x); + float rightMask = 1.0 - smoothstep(pixel.x, pixel.x * 2.0, 1.0 - local.x); + + float perimeterOffset = u_dash_offset; + + float bottomPos = mod(perimeterOffset + local.x * lengthX, period); + if (bottomPos < 0.0) bottomPos += period; + float bottomDash = bottomMask * (bottomPos <= dash ? 1.0 : 0.0); + + float rightPos = mod(perimeterOffset + lengthX + local.y * lengthY, period); + if (rightPos < 0.0) rightPos += period; + float rightDash = rightMask * (rightPos <= dash ? 1.0 : 0.0); + + float topPos = mod(perimeterOffset + lengthX + lengthY + (1.0 - local.x) * lengthX, period); + if (topPos < 0.0) topPos += period; + float topDash = topMask * (topPos <= dash ? 1.0 : 0.0); + + float leftPos = mod(perimeterOffset + 2.0 * lengthX + lengthY + (1.0 - local.y) * lengthY, period); + if (leftPos < 0.0) leftPos += period; + float leftDash = leftMask * (leftPos <= dash ? 1.0 : 0.0); + + float alpha = max(max(bottomDash, topDash), max(leftDash, rightDash)); + + if (alpha <= 0.0) + discard; + + o_fragColor = vec4(u_color.rgb, u_color.a * alpha); + } + )"; + constexpr auto UNIFORM_AXIS = "u_axis"; constexpr auto UNIFORM_COLOR = "u_color"; constexpr auto UNIFORM_TRANSFORM = "u_transform"; @@ -123,18 +196,27 @@ namespace anm2ed::shader constexpr auto UNIFORM_MODEL = "u_model"; constexpr auto UNIFORM_RECT_SIZE = "u_rect_size"; constexpr auto UNIFORM_TEXTURE = "u_texture"; + constexpr auto UNIFORM_AXIS_X = "u_axis_x"; + constexpr auto UNIFORM_AXIS_Y = "u_axis_y"; + constexpr auto UNIFORM_DASH_LENGTH = "u_dash_length"; + constexpr auto UNIFORM_DASH_GAP = "u_dash_gap"; + constexpr auto UNIFORM_DASH_OFFSET = "u_dash_offset"; enum Type { LINE, + DASHED, TEXTURE, AXIS, GRID, COUNT }; - const Info SHADERS[COUNT] = { - {VERTEX, FRAGMENT}, {VERTEX, TEXTURE_FRAGMENT}, {AXIS_VERTEX, FRAGMENT}, {GRID_VERTEX, GRID_FRAGMENT}}; + const Info SHADERS[COUNT] = {{VERTEX, FRAGMENT}, + {DASHED_VERTEX, DASHED_FRAGMENT}, + {VERTEX, TEXTURE_FRAGMENT}, + {AXIS_VERTEX, FRAGMENT}, + {GRID_VERTEX, GRID_FRAGMENT}}; class Shader { diff --git a/src/spritesheet_editor.cpp b/src/spritesheet_editor.cpp index 24c90dc..ff10925 100644 --- a/src/spritesheet_editor.cpp +++ b/src/spritesheet_editor.cpp @@ -37,7 +37,7 @@ namespace anm2ed::spritesheet_editor auto& tool = settings.tool; auto& shaderGrid = resources.shaders[shader::GRID]; auto& shaderTexture = resources.shaders[shader::TEXTURE]; - auto& lineShader = resources.shaders[shader::LINE]; + auto& dashedShader = resources.shaders[shader::DASHED]; if (ImGui::Begin("Spritesheet Editor", &settings.windowIsSpritesheetEditor)) { @@ -96,20 +96,22 @@ namespace anm2ed::spritesheet_editor auto frame = document.frame_get(); - if (spritesheet) + if (spritesheet && spritesheet->texture.is_valid()) { auto& texture = spritesheet->texture; auto transform = transform_get(zoom, pan); - auto spritesheetTransform = transform * math::quad_model_get(texture.size); + auto spritesheetModel = math::quad_model_get(texture.size); + auto spritesheetTransform = transform * spritesheetModel; texture_render(shaderTexture, texture.id, spritesheetTransform); - if (isBorder) rect_render(lineShader, spritesheetTransform); + if (isBorder) rect_render(dashedShader, spritesheetTransform, spritesheetModel); if (frame && reference.itemID > -1 && anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet) { - auto cropTransform = transform * math::quad_model_get(frame->size, frame->crop); - rect_render(lineShader, cropTransform, color::RED); + auto cropModel = math::quad_model_get(frame->size, frame->crop); + auto cropTransform = transform * cropModel; + rect_render(dashedShader, cropTransform, cropModel, color::RED); auto pivotTransform = transform * math::quad_model_get(canvas::PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); diff --git a/src/spritesheets.cpp b/src/spritesheets.cpp index e2dbaca..ea8c78f 100644 --- a/src/spritesheets.cpp +++ b/src/spritesheets.cpp @@ -28,6 +28,8 @@ namespace anm2ed::spritesheets auto& hovered = document.hoveredSpritesheet; auto& reference = document.referenceSpritesheet; + hovered = -1; + if (ImGui::Begin("Spritesheets", &settings.windowIsSpritesheets)) { auto style = ImGui::GetStyle(); diff --git a/src/state.cpp b/src/state.cpp index ab209f4..fae94f2 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -61,7 +61,7 @@ namespace anm2ed::state break; } case SDL_EVENT_QUIT: - isQuit = true; + isQuitting = true; break; default: break; @@ -72,14 +72,15 @@ namespace anm2ed::state ImGui_ImplOpenGL3_NewFrame(); ImGui::NewFrame(); - taskbar.update(manager, settings, dialog, isQuit); + taskbar.update(manager, settings, resources, dialog, isQuitting); + documents.update(taskbar, manager, settings, resources, isQuitting); dockspace.update(taskbar, documents, manager, settings, resources, dialog, clipboard); toasts.update(); - documents.update(taskbar, manager, resources); - ImGui::GetStyle().FontScaleMain = settings.displayScale; SDL_GetWindowSize(window, &settings.windowSize.x, &settings.windowSize.y); + + if (isQuitting && manager.documents.empty()) isQuit = true; } void State::render(SDL_Window*& window, Settings& settings) diff --git a/src/state.h b/src/state.h index 4ee9adc..6f04633 100644 --- a/src/state.h +++ b/src/state.h @@ -14,9 +14,10 @@ namespace anm2ed::state public: bool isQuit{}; - dialog::Dialog dialog; - resources::Resources resources; + bool isQuitting{}; manager::Manager manager; + resources::Resources resources; + dialog::Dialog dialog; clipboard::Clipboard clipboard; taskbar::Taskbar taskbar; diff --git a/src/taskbar.cpp b/src/taskbar.cpp index cec33ac..04da3b4 100644 --- a/src/taskbar.cpp +++ b/src/taskbar.cpp @@ -4,18 +4,25 @@ #include #include "imgui.h" +#include "math.h" -using namespace anm2ed::settings; +using namespace anm2ed::canvas; using namespace anm2ed::dialog; using namespace anm2ed::manager; +using namespace anm2ed::resources; +using namespace anm2ed::settings; using namespace anm2ed::types; +using namespace glm; namespace anm2ed::taskbar { - void Taskbar::update(Manager& manager, Settings& settings, Dialog& dialog, bool& isQuit) + Taskbar::Taskbar() : generate(vec2()) + { + } + + void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) { auto document = manager.get(); - auto animation = document ? document->animation_get() : nullptr; if (ImGui::BeginMainMenuBar()) { @@ -27,6 +34,32 @@ namespace anm2ed::taskbar if (ImGui::MenuItem("Open", settings.shortcutOpen.c_str())) dialog.anm2_open(); + if (manager.recentFiles.empty()) + { + ImGui::BeginDisabled(); + ImGui::MenuItem("Open Recent"); + ImGui::EndDisabled(); + } + else + { + if (ImGui::BeginMenu("Open Recent")) + { + for (auto [i, file] : std::views::enumerate(manager.recentFiles)) + { + auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string()); + + ImGui::PushID(i); + if (ImGui::MenuItem(label.c_str())) manager.open(file); + ImGui::PopID(); + } + + if (!manager.recentFiles.empty()) + if (ImGui::MenuItem("Clear List")) manager.recent_files_clear(); + + ImGui::EndMenu(); + } + } + ImGui::BeginDisabled(!document); { if (ImGui::MenuItem("Save", settings.shortcutSave.c_str())) manager.save(); @@ -36,7 +69,7 @@ namespace anm2ed::taskbar ImGui::EndDisabled(); ImGui::Separator(); - if (ImGui::MenuItem("Exit", settings.shortcutExit.c_str())) isQuit = true; + if (ImGui::MenuItem("Exit", settings.shortcutExit.c_str())) isQuitting = true; ImGui::EndMenu(); } if (dialog.is_selected_file(dialog::ANM2_NEW)) @@ -59,11 +92,10 @@ namespace anm2ed::taskbar if (ImGui::BeginMenu("Wizard")) { - ImGui::BeginDisabled(!animation); - { - ImGui::MenuItem("Generate Animation From Grid"); - ImGui::MenuItem("Change All Frame Properties"); - } + auto item = document ? document->item_get() : nullptr; + ImGui::BeginDisabled(!item || document->reference.itemType != anm2::LAYER); + if (ImGui::MenuItem("Generate Animation From Grid")) generatePopup.open(); + if (ImGui::MenuItem("Change All Frame Properties")) changePopup.open(); ImGui::EndDisabled(); ImGui::EndMenu(); } @@ -107,6 +139,264 @@ namespace anm2ed::taskbar ImGui::EndMainMenuBar(); } + generatePopup.trigger(); + + if (ImGui::BeginPopupModal(generatePopup.label, &generatePopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto& startPosition = settings.generateStartPosition; + auto& size = settings.generateSize; + auto& pivot = settings.generatePivot; + auto& rows = settings.generateRows; + auto& columns = settings.generateColumns; + auto& count = settings.generateCount; + auto& delay = settings.generateDelay; + auto& zoom = settings.generateZoom; + auto& zoomStep = settings.viewZoomStep; + + auto childSize = ImVec2(imgui::row_widget_width_get(2), imgui::size_without_footer_get().y); + + if (ImGui::BeginChild("##Options Child", childSize, ImGuiChildFlags_Borders)) + { + ImGui::InputInt2("Start Position", value_ptr(startPosition)); + ImGui::InputInt2("Frame Size", value_ptr(size)); + ImGui::InputInt2("Pivot", value_ptr(pivot)); + ImGui::InputInt("Rows", &rows, step::NORMAL, step::FAST); + ImGui::InputInt("Columns", &columns, step::NORMAL, step::FAST); + + ImGui::InputInt("Count", &count, step::NORMAL, step::FAST); + count = glm::min(count, rows * columns); + + ImGui::InputInt("Delay", &delay, step::NORMAL, step::FAST); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("##Preview Child", childSize, ImGuiChildFlags_Borders)) + { + auto& backgroundColor = settings.previewBackgroundColor; + auto& time = generateTime; + auto& shaderTexture = resources.shaders[shader::TEXTURE]; + + auto previewSize = ImVec2(ImGui::GetContentRegionAvail().x, imgui::size_without_footer_get(2).y); + + generate.size_set(to_vec2(previewSize)); + generate.bind(); + generate.viewport_set(); + generate.clear(backgroundColor); + + if (document && document->reference.itemType == anm2::LAYER) + { + auto& texture = document->anm2.content + .spritesheets[document->anm2.content.layers[document->reference.itemID].spritesheetID] + .texture; + + auto index = std::clamp((int)(time * count), 0, count); + auto row = index / columns; + auto column = index % columns; + auto crop = startPosition + ivec2(size.x * column, size.y * row); + auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size); + auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size); + + mat4 transform = generate.transform_get(zoom) * math::quad_model_get(size, {}, pivot); + + generate.texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {}, + math::uv_vertices_get(uvMin, uvMax).data()); + } + generate.unbind(); + + ImGui::Image(generate.texture, previewSize); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::SliderFloat("##Time", &time, 0.0f, 1.0f, ""); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputFloat("##Zoom", &zoom, zoomStep, zoomStep, "%.0f%%"); + zoom = glm::clamp(zoom, canvas::ZOOM_MIN, canvas::ZOOM_MAX); + } + + ImGui::EndChild(); + + auto widgetSize = imgui::widget_size_with_row_get(2); + + if (ImGui::Button("Generate", widgetSize)) + { + document->generate_animation_from_grid(startPosition, size, pivot, columns, count, delay); + generatePopup.close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) generatePopup.close(); + + ImGui::EndPopup(); + } + + changePopup.trigger(); + + if (ImGui::BeginPopupModal(changePopup.label, &changePopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto& isCrop = settings.changeIsCrop; + auto& isSize = settings.changeIsSize; + auto& isPosition = settings.changeIsPosition; + auto& isPivot = settings.changeIsPivot; + auto& isScale = settings.changeIsScale; + auto& isRotation = settings.changeIsRotation; + auto& isDelay = settings.changeIsDelay; + auto& isTint = settings.changeIsTint; + auto& isColorOffset = settings.changeIsColorOffset; + auto& isVisibleSet = settings.changeIsVisibleSet; + auto& isInterpolatedSet = settings.changeIsInterpolatedSet; + auto& crop = settings.changeCrop; + auto& size = settings.changeSize; + auto& position = settings.changePosition; + auto& pivot = settings.changePivot; + auto& scale = settings.changeScale; + auto& rotation = settings.changeRotation; + auto& delay = settings.changeDelay; + auto& tint = settings.changeTint; + auto& colorOffset = settings.changeColorOffset; + auto& isVisible = settings.changeIsVisible; + auto& isInterpolated = settings.changeIsInterpolated; + + auto& isFromSelectedFrame = settings.changeIsFromSelectedFrame; + auto& numberFrames = settings.changeNumberFrames; + + auto propertiesSize = imgui::child_size_get(10); + + if (ImGui::BeginChild("##Properties", propertiesSize, ImGuiChildFlags_Borders)) + { + auto start = [&](const char* checkboxLabel, bool& isEnabled) + { + ImGui::Checkbox(checkboxLabel, &isEnabled); + ImGui::SameLine(); + ImGui::BeginDisabled(!isEnabled); + }; + auto end = [&]() { ImGui::EndDisabled(); }; + + auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value) + { + start(checkboxLabel, isEnabled); + ImGui::Checkbox(valueLabel, &value); + end(); + }; + + auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value) + { + start(checkboxLabel, isEnabled); + ImGui::ColorEdit3(valueLabel, value_ptr(value)); + end(); + }; + + auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value) + { + start(checkboxLabel, isEnabled); + ImGui::ColorEdit4(valueLabel, value_ptr(value)); + end(); + }; + + auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value) + { + start(checkboxLabel, isEnabled); + ImGui::InputFloat2(valueLabel, value_ptr(value), math::vec2_format_get(value)); + end(); + }; + + auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) + { + start(checkboxLabel, isEnabled); + ImGui::InputFloat(valueLabel, &value, step::NORMAL, step::FAST, math::float_format_get(value)); + end(); + }; + + auto int_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) + { + start(checkboxLabel, isEnabled); + ImGui::InputInt(valueLabel, &value, step::NORMAL, step::FAST); + end(); + }; + + float2_value("##Is Crop", "Crop", isCrop, crop); + float2_value("##Is Size", "Size", isSize, size); + float2_value("##Is Position", "Position", isPosition, position); + float2_value("##Is Pivot", "Pivot", isPivot, pivot); + float2_value("##Is Scale", "Scale", isScale, scale); + float_value("##Is Rotation", "Rotation", isRotation, rotation); + int_value("##Is Delay", "Delay", isDelay, delay); + color4_value("##Is Tint", "Tint", isTint, tint); + color3_value("##Is Color Offset", "Color Offset", isColorOffset, colorOffset); + bool_value("##Is Visible", "Visible", isVisibleSet, isVisible); + ImGui::SameLine(); + bool_value("##Is Interpolated", "Interpolated", isInterpolatedSet, isInterpolated); + } + ImGui::EndChild(); + + auto settingsSize = imgui::child_size_get(2); + + if (ImGui::BeginChild("##Settings", settingsSize, ImGuiChildFlags_Borders)) + { + ImGui::Checkbox("From Selected Frame", &isFromSelectedFrame); + ImGui::SetItemTooltip("The frames after the currently referenced frame will be changed with these values.\nIf" + "off, will use all frames."); + + ImGui::BeginDisabled(!isFromSelectedFrame); + ImGui::InputInt("Number of Frames", &numberFrames, step::NORMAL, step::FAST); + numberFrames = glm::clamp(numberFrames, anm2::FRAME_NUM_MIN, + (int)document->item_get()->frames.size() - document->reference.frameIndex); + ImGui::SetItemTooltip("Set the number of frames that will be changed."); + ImGui::EndDisabled(); + } + ImGui::EndChild(); + + auto widgetSize = imgui::widget_size_with_row_get(4); + + auto frame_change = [&](frame_change::Type type) + { + anm2::FrameChange frameChange; + frameChange.crop = isCrop ? std::make_optional(crop) : std::nullopt; + frameChange.size = isSize ? std::make_optional(size) : std::nullopt; + frameChange.position = isPosition ? std::make_optional(position) : std::nullopt; + frameChange.pivot = isPivot ? std::make_optional(pivot) : std::nullopt; + frameChange.scale = isScale ? std::make_optional(scale) : std::nullopt; + frameChange.rotation = isRotation ? std::make_optional(rotation) : std::nullopt; + frameChange.delay = isDelay ? std::make_optional(delay) : std::nullopt; + frameChange.tint = isTint ? std::make_optional(tint) : std::nullopt; + frameChange.colorOffset = isColorOffset ? std::make_optional(colorOffset) : std::nullopt; + frameChange.isVisible = isVisibleSet ? std::make_optional(isVisible) : std::nullopt; + frameChange.isInterpolated = isInterpolatedSet ? std::make_optional(isInterpolated) : std::nullopt; + + document->frames_change(frameChange, type, isFromSelectedFrame, numberFrames); + }; + + if (ImGui::Button("Add", widgetSize)) + { + frame_change(frame_change::ADD); + changePopup.close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Subtract", widgetSize)) + { + frame_change(frame_change::SUBTRACT); + changePopup.close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Adjust", widgetSize)) + { + frame_change(frame_change::ADJUST); + changePopup.close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) changePopup.close(); + + ImGui::EndPopup(); + } + configurePopup.trigger(); if (ImGui::BeginPopupModal(configurePopup.label, &configurePopup.isOpen, ImGuiWindowFlags_NoResize)) @@ -115,29 +405,33 @@ namespace anm2ed::taskbar if (ImGui::BeginTabBar("##Configure Tabs")) { - if (ImGui::BeginTabItem("View")) + if (ImGui::BeginTabItem("General")) { if (ImGui::BeginChild("##Tab Child", childSize, true)) { - ImGui::InputFloat("Zoom Step", &editSettings.viewZoomStep, 10.0f, 10.0f, "%.2f"); - ImGui::SetItemTooltip("%s", "When zooming in/out with mouse or shortcut, this value will be used."); - editSettings.viewZoomStep = glm::clamp(editSettings.viewZoomStep, 1.0f, 250.0f); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } + ImGui::SeparatorText("File"); + + ImGui::Checkbox("Autosaving", &editSettings.fileIsAutosave); + ImGui::SetItemTooltip("Enables autosaving of documents."); + + ImGui::BeginDisabled(!editSettings.fileIsAutosave); + ImGui::InputInt("Autosave Time (minutes", &editSettings.fileAutosaveTime, step::NORMAL, step::FAST); + editSettings.fileAutosaveTime = glm::clamp(editSettings.fileAutosaveTime, 0, 10); + ImGui::SetItemTooltip("If changed, will autosave documents using this interval."); + ImGui::EndDisabled(); + + ImGui::SeparatorText("View"); - if (ImGui::BeginTabItem("Video")) - { - if (ImGui::BeginChild("##Tab Child", childSize, true)) - { ImGui::InputFloat("Display Scale", &editSettings.displayScale, 0.25f, 0.25f, "%.2f"); - ImGui::SetItemTooltip("%s", "Change the scale of the display."); + ImGui::SetItemTooltip("Change the scale of the display."); editSettings.displayScale = glm::clamp(editSettings.displayScale, 0.5f, 2.0f); + ImGui::InputFloat("Zoom Step", &editSettings.viewZoomStep, 10.0f, 10.0f, "%.2f"); + ImGui::SetItemTooltip("When zooming in/out with mouse or shortcut, this value will be used."); + editSettings.viewZoomStep = glm::clamp(editSettings.viewZoomStep, 1.0f, 250.0f); + ImGui::Checkbox("Vsync", &editSettings.isVsync); - ImGui::SetItemTooltip("%s", - "Toggle vertical sync; synchronizes program update rate with monitor refresh rate."); + ImGui::SetItemTooltip("Toggle vertical sync; synchronizes program update rate with monitor refresh rate."); } ImGui::EndChild(); @@ -241,11 +535,10 @@ namespace anm2ed::taskbar ImGui::EndPopup(); } - if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutNew), ImGuiInputFlags_RouteGlobal)) dialog.anm2_new(); - if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutOpen), ImGuiInputFlags_RouteGlobal)) dialog.anm2_open(); - if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutSave), ImGuiInputFlags_RouteGlobal)) manager.save(); - if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutSaveAs), ImGuiInputFlags_RouteGlobal)) - dialog.anm2_save(); - if (ImGui::Shortcut(imgui::string_to_chord(settings.shortcutExit), ImGuiInputFlags_RouteGlobal)) isQuit = true; + if (imgui::shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.anm2_new(); + if (imgui::shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.anm2_open(); + if (imgui::shortcut(settings.shortcutSave, shortcut::GLOBAL)) document->save(); + if (imgui::shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.anm2_save(); + if (imgui::shortcut(settings.shortcutExit, shortcut::GLOBAL)) isQuitting = true; } } diff --git a/src/taskbar.h b/src/taskbar.h index b013eab..9549ae2 100644 --- a/src/taskbar.h +++ b/src/taskbar.h @@ -1,22 +1,31 @@ #pragma once +#include "canvas.h" #include "dialog.h" #include "imgui.h" #include "manager.h" +#include "resources.h" #include "settings.h" namespace anm2ed::taskbar { class Taskbar { + canvas::Canvas generate; + float generateTime{}; + imgui::PopupHelper generatePopup{imgui::PopupHelper("Generate Animation from Grid")}; + imgui::PopupHelper changePopup{imgui::PopupHelper("Change All Frame Properties", imgui::POPUP_SMALL, true)}; + imgui::PopupHelper renderPopup{imgui::PopupHelper("Render Animation")}; imgui::PopupHelper configurePopup{imgui::PopupHelper("Configure")}; imgui::PopupHelper aboutPopup{imgui::PopupHelper("About")}; settings::Settings editSettings{}; int selectedShortcut{-1}; + bool isQuittingMode{}; public: float height{}; - void update(manager::Manager&, settings::Settings&, dialog::Dialog&, bool&); + Taskbar(); + void update(manager::Manager&, settings::Settings&, resources::Resources&, dialog::Dialog&, bool&); }; }; diff --git a/src/timeline.cpp b/src/timeline.cpp index e0e71f6..dfdfeb0 100644 --- a/src/timeline.cpp +++ b/src/timeline.cpp @@ -11,6 +11,7 @@ using namespace anm2ed::manager; using namespace anm2ed::resources; using namespace anm2ed::settings; using namespace anm2ed::playback; +using namespace anm2ed::clipboard; using namespace glm; namespace anm2ed::timeline @@ -50,14 +51,63 @@ namespace anm2ed::timeline - Press {} to extend the selected frame, by one frame. - Hold Alt while clicking a non-trigger frame to toggle interpolation.)"; + void Timeline::context_menu(Document& document, Settings& settings, Clipboard& clipboard) + { + auto& hoveredFrame = document.hoveredFrame; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + + auto copy = [&]() + { + if (auto frame = document.anm2.frame_get(hoveredFrame)) clipboard.set(frame->to_string(hoveredFrame.itemType)); + }; + + auto cut = [&]() + { + if (auto frame = document.anm2.frame_get(hoveredFrame)) + { + if (auto item = document.anm2.item_get(hoveredFrame)) + { + clipboard.set(frame->to_string(hoveredFrame.itemType)); + document.frames_delete(item); + hoveredFrame = anm2::REFERENCE_DEFAULT; + } + } + }; + + auto paste = [&]() { document.frames_deserialize(clipboard.get()); }; + + 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(hoveredFrame == anm2::REFERENCE_DEFAULT); + 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")) paste(); + ImGui::EndDisabled(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + } + void Timeline::item_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings, - Resources& resources, anm2::Type type, int id, int& index) + Resources& resources, Clipboard& clipboard, 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& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto isVisible = item ? item->isVisible : false; + if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; auto isActive = reference.itemType == type && reference.itemID == id; std::string label = "##None"; icon::Type icon{}; @@ -129,7 +179,6 @@ namespace anm2ed::timeline ImGui::TextUnformatted(label.c_str()); anm2::Item* item = animation->item_get(type, id); - bool& isVisible = item->isVisible; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); @@ -138,7 +187,7 @@ namespace anm2ed::timeline ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE; + int visibleIcon = item->isVisible ? icon::VISIBLE : icon::INVISIBLE; if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, imgui::icon_size_get())) document.item_visible_toggle(item); @@ -165,22 +214,32 @@ namespace anm2ed::timeline else { auto cursorPos = ImGui::GetCursorPos(); - auto& isShowUnused = settings.timelineIsShowUnused; - - ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, - (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + auto& isShowUnused = settings.timelineIsShowUnused; auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, imgui::icon_size_get())) isShowUnused = !isShowUnused; ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide." : "Unused layers/nulls are hidden. Press to show."); + auto onlyShowLayersIcon = isOnlyShowLayers ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; + ImGui::SetCursorPos( + ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + if (ImGui::ImageButton("##Layers Visibility Toggle", resources.icons[onlyShowLayersIcon].id, + imgui::icon_size_get())) + isOnlyShowLayers = !isOnlyShowLayers; + ImGui::SetItemTooltip(isOnlyShowLayers + ? "Only layers are visible. Press to toggle visibility for all items." + : "Non-layer items are visible. Press to toggle visiblity only for layers."); + ImGui::PopStyleVar(); ImGui::PopStyleColor(3); @@ -201,7 +260,7 @@ namespace anm2ed::timeline } void Timeline::items_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings, - Resources& resources) + Resources& resources, Clipboard& clipboard) { auto& reference = document.reference; @@ -232,7 +291,7 @@ namespace anm2ed::timeline { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - item_child(manager, document, animation, settings, resources, type, id, index); + item_child(manager, document, animation, settings, resources, clipboard, type, id, index); }; item_child_row(anm2::NONE); @@ -302,13 +361,16 @@ namespace anm2ed::timeline } void Timeline::frame_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources, - anm2::Type type, int id, int& index, float width) + Clipboard& clipboard, anm2::Type type, int id, int& index, float width) { auto& anm2 = document.anm2; auto& playback = document.playback; auto& reference = document.reference; + auto& hoveredFrame = document.hoveredFrame; auto item = animation ? animation->item_get(type, id) : nullptr; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto isVisible = item ? item->isVisible : false; + if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); @@ -489,6 +551,8 @@ namespace anm2ed::timeline reference = frameReference; reference.frameTime = frameTime; } + if (ImGui::IsItemHovered()) hoveredFrame = frameReference; + if (type != anm2::TRIGGER) ImGui::SameLine(); ImGui::PopStyleColor(3); @@ -502,6 +566,8 @@ namespace anm2ed::timeline ImGui::PopID(); } + + context_menu(document, settings, clipboard); } } ImGui::EndChild(); @@ -511,10 +577,12 @@ namespace anm2ed::timeline ImGui::PopID(); } - void Timeline::frames_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources) + void Timeline::frames_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources, + Clipboard& clipboard) { auto& anm2 = document.anm2; auto& playback = document.playback; + auto& hoveredFrame = document.hoveredFrame; auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15; @@ -571,11 +639,13 @@ namespace anm2ed::timeline { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - frame_child(document, animation, settings, resources, type, id, index, childWidth); + frame_child(document, animation, settings, resources, clipboard, type, id, index, childWidth); }; frames_child_row(anm2::NONE); + //hoveredFrame = anm2::REFERENCE_DEFAULT; + if (animation) { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); @@ -622,6 +692,8 @@ namespace anm2ed::timeline pickerLineDrawList->PopClipRect(); ImGui::PopStyleVar(); + + context_menu(document, settings, clipboard); } ImGui::EndChild(); ImGui::PopStyleVar(); @@ -917,7 +989,7 @@ namespace anm2ed::timeline } } - void Timeline::update(Manager& manager, Settings& settings, Resources& resources) + void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { auto& document = *manager.get(); auto& playback = document.playback; @@ -931,8 +1003,8 @@ namespace anm2ed::timeline if (ImGui::Begin("Timeline", &settings.windowIsTimeline)) { isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); - frames_child(document, animation, settings, resources); - items_child(manager, document, animation, settings, resources); + frames_child(document, animation, settings, resources, clipboard); + items_child(manager, document, animation, settings, resources, clipboard); } ImGui::PopStyleVar(); ImGui::End(); @@ -956,10 +1028,7 @@ namespace anm2ed::timeline } } - if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutShortenFrame))) - if (auto frame = anm2.frame_get(reference); frame) frame->shorten(); - - if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutExtendFrame))) - if (auto frame = anm2.frame_get(reference); frame) frame->extend(); + if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutShortenFrame))) document.frame_shorten(); + if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutExtendFrame))) document.frame_extend(); } } \ No newline at end of file diff --git a/src/timeline.h b/src/timeline.h index 5284ca7..9bb6f16 100644 --- a/src/timeline.h +++ b/src/timeline.h @@ -1,6 +1,7 @@ #pragma once #include "anm2.h" +#include "clipboard.h" #include "document.h" #include "manager.h" #include "resources.h" @@ -25,16 +26,19 @@ namespace anm2ed::timeline ImDrawList* pickerLineDrawList{}; ImGuiStyle style{}; + void context_menu(document::Document&, settings::Settings&, clipboard::Clipboard&); 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&); + clipboard::Clipboard&, anm2::Type, int, int&); + void items_child(manager::Manager&, Document&, anm2::Animation*, settings::Settings&, resources::Resources&, + clipboard::Clipboard&); + void frame_child(document::Document&, anm2::Animation*, settings::Settings&, resources::Resources&, + clipboard::Clipboard&, anm2::Type, int, int&, float); + void frames_child(document::Document&, anm2::Animation*, settings::Settings&, resources::Resources&, + clipboard::Clipboard&); void popups(document::Document&, anm2::Animation*, settings::Settings&); public: - void update(manager::Manager&, settings::Settings&, resources::Resources&); + void update(manager::Manager&, settings::Settings&, resources::Resources&, clipboard::Clipboard&); }; } diff --git a/src/types.h b/src/types.h index 439d138..acc12b3 100644 --- a/src/types.h +++ b/src/types.h @@ -69,6 +69,16 @@ namespace anm2ed::types::merge }; } +namespace anm2ed::types::frame_change +{ + enum Type + { + ADD, + SUBTRACT, + ADJUST + }; +} + namespace anm2ed::types::color { using namespace glm; diff --git a/src/util.h b/src/util.h index ec99f47..b66ac09 100644 --- a/src/util.h +++ b/src/util.h @@ -10,6 +10,9 @@ namespace anm2ed::util::time { + constexpr auto SECOND_S = 1.0; + constexpr auto SECOND_M = 60.0; + std::string get(const char*); } @@ -95,4 +98,13 @@ namespace anm2ed::util::vector return moveIndices; } + + template bool in_bounds(std::vector& v, int& index) + { + return index >= 0 || index <= (int)v.size() - 1; + } + template void clamp_in_bounds(std::vector& v, int& index) + { + index = std::clamp(index, 0, (int)v.size() - 1); + } } diff --git a/src/welcome.cpp b/src/welcome.cpp new file mode 100644 index 0000000..2114906 --- /dev/null +++ b/src/welcome.cpp @@ -0,0 +1,106 @@ +#include "welcome.h" + +#include + +using namespace anm2ed::dialog; +using namespace anm2ed::taskbar; +using namespace anm2ed::documents; +using namespace anm2ed::resources; +using namespace anm2ed::manager; + +namespace anm2ed::welcome +{ + void Welcome::update(Manager& manager, Resources& resources, Dialog& dialog, Taskbar& taskbar, Documents& documents) + { + auto viewport = ImGui::GetMainViewport(); + auto windowHeight = viewport->Size.y - taskbar.height - documents.height; + + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height + documents.height)); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight)); + + if (ImGui::Begin("##Welcome", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse)) + { + + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); + ImGui::Text("Anm2Ed"); + ImGui::PopFont(); + + ImGui::Text( + "Select a recent file or an option below. You can also drag and drop files into the window to open them."); + + auto widgetSize = imgui::widget_size_with_row_get(2); + + if (ImGui::Button("New", widgetSize)) dialog.anm2_new(); // handled in taskbar.cpp + + ImGui::SameLine(); + + if (ImGui::Button("Open", widgetSize)) dialog.anm2_open(); // handled in taskbar.cpp + + if (ImGui::BeginChild("##Recent Child", ImVec2(), ImGuiChildFlags_Borders)) + { + for (auto [i, file] : std::views::enumerate(manager.recentFiles)) + { + ImGui::PushID(i); + + auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string()); + + if (ImGui::Selectable(label.c_str())) + { + manager.open(file); + ImGui::PopID(); + break; + } + + ImGui::PopID(); + } + } + ImGui::EndChild(); + } + ImGui::End(); + + if (!manager.autosaveFiles.empty() && !restorePopup.is_open()) restorePopup.open(); + + restorePopup.trigger(); + + if (ImGui::BeginPopupModal(restorePopup.label, &restorePopup.isOpen, ImGuiWindowFlags_NoResize)) + { + ImGui::Text("Autosaved files detected. Would you like to restore them?"); + + auto childSize = imgui::child_size_get(5); + + if (ImGui::BeginChild("##Autosave Documents", childSize, ImGuiChildFlags_Borders, + ImGuiWindowFlags_HorizontalScrollbar)) + { + for (auto& file : manager.autosaveFiles) + { + auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string()); + ImGui::TextUnformatted(label.c_str()); + } + } + ImGui::EndChild(); + + auto widgetSize = imgui::widget_size_with_row_get(2); + + if (ImGui::Button("Yes", widgetSize)) + { + manager.autosave_files_open(); + restorePopup.close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("No", widgetSize)) + { + manager.autosave_files_clear(); + restorePopup.close(); + } + + ImGui::EndPopup(); + } + } + +} diff --git a/src/welcome.h b/src/welcome.h new file mode 100644 index 0000000..238470f --- /dev/null +++ b/src/welcome.h @@ -0,0 +1,16 @@ +#pragma once + +#include "documents.h" +#include "manager.h" +#include "taskbar.h" + +namespace anm2ed::welcome +{ + class Welcome + { + imgui::PopupHelper restorePopup{imgui::PopupHelper("Restore", imgui::POPUP_SMALL, true)}; + + public: + void update(manager::Manager&, resources::Resources&, dialog::Dialog&, taskbar::Taskbar&, documents::Documents&); + }; +}; \ No newline at end of file