From c57c32aca8d040689bc1fc2200eb699fae4ee5a9 Mon Sep 17 00:00:00 2001 From: shweet Date: Thu, 13 Nov 2025 22:06:09 -0500 Subject: [PATCH] ...Anm2Ed 2.0 --- src/anm2/anm2.cpp | 256 ++++++++++++++++++++++++ src/anm2/anm2.h | 3 + src/anm2/anm2_sounds.cpp | 7 +- src/anm2/frame.cpp | 7 - src/anm2/frame.h | 1 - src/anm2/item.cpp | 2 + src/canvas.cpp | 93 +-------- src/canvas.h | 21 +- src/dialog.h | 1 + src/document.cpp | 12 ++ src/document.h | 1 + src/framebuffer.cpp | 88 ++++++++ src/framebuffer.h | 37 ++++ src/imgui/dockspace.cpp | 2 +- src/imgui/documents.cpp | 63 +++++- src/imgui/imgui_.cpp | 74 ++++++- src/imgui/imgui_.h | 7 +- src/imgui/taskbar.cpp | 156 +++++++++++---- src/imgui/window/animation_preview.cpp | 43 ++-- src/imgui/window/animation_preview.h | 8 +- src/imgui/window/animations.cpp | 80 ++++---- src/imgui/window/spritesheet_editor.cpp | 56 ++++-- src/imgui/window/timeline.cpp | 79 +++++--- src/imgui/window/welcome.cpp | 3 +- src/loader.cpp | 19 +- src/manager.cpp | 93 ++++++++- src/manager.h | 19 +- src/playback.cpp | 2 +- src/render.h | 1 + src/resource/texture.cpp | 18 +- src/resource/texture.h | 6 +- src/resources.cpp | 23 ++- src/resources.h | 1 + src/settings.cpp | 1 + src/settings.h | 13 +- src/state.cpp | 40 ++-- 36 files changed, 1003 insertions(+), 333 deletions(-) create mode 100644 src/framebuffer.cpp create mode 100644 src/framebuffer.h diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp index 58b3c61..ff66961 100644 --- a/src/anm2/anm2.cpp +++ b/src/anm2/anm2.cpp @@ -1,6 +1,11 @@ #include "anm2.h" +#include +#include +#include + #include "filesystem_.h" +#include "map_.h" #include "time_.h" #include "vector_.h" #include "xml_.h" @@ -74,4 +79,255 @@ namespace anm2ed::anm2 if (vector::in_bounds(item->frames, frameIndex)) return &item->frames[frameIndex]; return nullptr; } + + void Anm2::merge(const Anm2& source, const std::filesystem::path& destinationDirectory, + const std::filesystem::path& sourceDirectory) + { + using util::map::next_id_get; + + auto remap_path = [&](const std::filesystem::path& original) -> std::filesystem::path + { + if (destinationDirectory.empty()) return original; + std::error_code ec{}; + std::filesystem::path absolute{}; + bool hasAbsolute = false; + + if (!original.empty()) + { + if (original.is_absolute()) + { + absolute = original; + hasAbsolute = true; + } + else if (!sourceDirectory.empty()) + { + absolute = std::filesystem::weakly_canonical(sourceDirectory / original, ec); + if (ec) + { + ec.clear(); + absolute = sourceDirectory / original; + } + hasAbsolute = true; + } + } + + if (!hasAbsolute) return original; + + auto relative = std::filesystem::relative(absolute, destinationDirectory, ec); + if (!ec) return relative; + ec.clear(); + try + { + return std::filesystem::relative(absolute, destinationDirectory); + } + catch (const std::filesystem::filesystem_error&) + { + return original.empty() ? absolute : original; + } + return original; + }; + + auto remap_id = [](const auto& table, int value) + { + if (value < 0) return value; + if (auto it = table.find(value); it != table.end()) return it->second; + return value; + }; + + std::unordered_map spritesheetRemap{}; + std::unordered_map layerRemap{}; + std::unordered_map nullRemap{}; + std::unordered_map eventRemap{}; + std::unordered_map soundRemap{}; + + // Spritesheets + for (auto& [sourceID, sprite] : source.content.spritesheets) + { + auto sheet = sprite; + sheet.path = remap_path(sheet.path); + if (!destinationDirectory.empty() && !sheet.path.empty()) sheet.reload(destinationDirectory); + + int destinationID = next_id_get(content.spritesheets); + content.spritesheets[destinationID] = std::move(sheet); + spritesheetRemap[sourceID] = destinationID; + } + + // Sounds + for (auto& [sourceID, soundEntry] : source.content.sounds) + { + auto sound = soundEntry; + sound.path = remap_path(sound.path); + if (!destinationDirectory.empty() && !sound.path.empty()) sound.reload(destinationDirectory); + + int destinationID = -1; + for (auto& [id, existing] : content.sounds) + if (existing.path == sound.path) + { + destinationID = id; + existing = sound; + break; + } + + if (destinationID == -1) + { + destinationID = next_id_get(content.sounds); + content.sounds[destinationID] = sound; + } + soundRemap[sourceID] = destinationID; + } + + auto find_by_name = [](auto& container, const std::string& name) -> int + { + for (auto& [id, value] : container) + if (value.name == name) return id; + return -1; + }; + + // Layers + for (auto& [sourceID, sourceLayer] : source.content.layers) + { + auto layer = sourceLayer; + layer.spritesheetID = remap_id(spritesheetRemap, layer.spritesheetID); + + int destinationID = find_by_name(content.layers, layer.name); + if (destinationID != -1) + content.layers[destinationID] = layer; + else + { + destinationID = next_id_get(content.layers); + content.layers[destinationID] = layer; + } + layerRemap[sourceID] = destinationID; + } + + // Nulls + for (auto& [sourceID, sourceNull] : source.content.nulls) + { + auto null = sourceNull; + int destinationID = find_by_name(content.nulls, null.name); + if (destinationID != -1) + content.nulls[destinationID] = null; + else + { + destinationID = next_id_get(content.nulls); + content.nulls[destinationID] = null; + } + nullRemap[sourceID] = destinationID; + } + + // Events + for (auto& [sourceID, sourceEvent] : source.content.events) + { + auto event = sourceEvent; + event.soundID = remap_id(soundRemap, event.soundID); + + int destinationID = find_by_name(content.events, event.name); + if (destinationID != -1) + content.events[destinationID] = event; + else + { + destinationID = next_id_get(content.events); + content.events[destinationID] = event; + } + eventRemap[sourceID] = destinationID; + } + + auto remap_item = [&](Item& item) + { + for (auto& frame : item.frames) + { + frame.soundID = remap_id(soundRemap, frame.soundID); + frame.eventID = remap_id(eventRemap, frame.eventID); + } + }; + + auto build_animation = [&](const Animation& incoming) -> Animation + { + Animation remapped{}; + remapped.name = incoming.name; + remapped.frameNum = incoming.frameNum; + remapped.isLoop = incoming.isLoop; + remapped.rootAnimation = incoming.rootAnimation; + remapped.triggers = incoming.triggers; + remap_item(remapped.rootAnimation); + remap_item(remapped.triggers); + + for (auto layerID : incoming.layerOrder) + { + auto mapped = remap_id(layerRemap, layerID); + if (mapped >= 0 && + std::find(remapped.layerOrder.begin(), remapped.layerOrder.end(), mapped) == remapped.layerOrder.end()) + remapped.layerOrder.push_back(mapped); + } + + for (auto& [layerID, item] : incoming.layerAnimations) + { + auto mapped = remap_id(layerRemap, layerID); + if (mapped < 0) continue; + auto copy = item; + remap_item(copy); + remapped.layerAnimations[mapped] = std::move(copy); + if (std::find(remapped.layerOrder.begin(), remapped.layerOrder.end(), mapped) == remapped.layerOrder.end()) + remapped.layerOrder.push_back(mapped); + } + + for (auto& [nullID, item] : incoming.nullAnimations) + { + auto mapped = remap_id(nullRemap, nullID); + if (mapped < 0) continue; + auto copy = item; + remap_item(copy); + remapped.nullAnimations[mapped] = std::move(copy); + } + + remap_item(remapped.triggers); + return remapped; + }; + + auto find_animation = [&](const std::string& name) -> Animation* + { + for (auto& animation : animations.items) + if (animation.name == name) return &animation; + return nullptr; + }; + + auto merge_item_map = [&](auto& destination, const auto& incoming) + { + for (auto& [id, item] : incoming) + { + if (!item.frames.empty()) + destination[id] = item; + else if (!destination.contains(id)) + destination[id] = item; + } + }; + + for (auto& animation : source.animations.items) + { + auto processed = build_animation(animation); + if (auto destination = find_animation(processed.name)) + { + destination->frameNum = std::max(destination->frameNum, processed.frameNum); + destination->isLoop = processed.isLoop; + if (!processed.rootAnimation.frames.empty()) destination->rootAnimation = processed.rootAnimation; + if (!processed.triggers.frames.empty()) destination->triggers = processed.triggers; + + merge_item_map(destination->layerAnimations, processed.layerAnimations); + merge_item_map(destination->nullAnimations, processed.nullAnimations); + + for (auto id : processed.layerOrder) + if (std::find(destination->layerOrder.begin(), destination->layerOrder.end(), id) == + destination->layerOrder.end()) + destination->layerOrder.push_back(id); + + destination->fit_length(); + } + else + animations.items.push_back(std::move(processed)); + } + + if (animations.defaultAnimation.empty() && !source.animations.defaultAnimation.empty()) { + animations.defaultAnimation = source.animations.defaultAnimation; + } + } } diff --git a/src/anm2/anm2.h b/src/anm2/anm2.h index a4191aa..a0257e7 100644 --- a/src/anm2/anm2.h +++ b/src/anm2/anm2.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -74,5 +75,7 @@ namespace anm2ed::anm2 Reference null_animation_add(Reference = {}, std::string = {}, types::locale::Type = types::locale::GLOBAL); Frame* frame_get(int, Type, int, int = -1); + void merge(const Anm2& source, const std::filesystem::path& destinationDirectory = {}, + const std::filesystem::path& sourceDirectory = {}); }; } diff --git a/src/anm2/anm2_sounds.cpp b/src/anm2/anm2_sounds.cpp index 299d64c..e8b48ba 100644 --- a/src/anm2/anm2_sounds.cpp +++ b/src/anm2/anm2_sounds.cpp @@ -1,7 +1,5 @@ #include "anm2.h" -#include - #include "filesystem_.h" #include "map_.h" @@ -35,8 +33,11 @@ namespace anm2ed::anm2 if (content.sounds.contains(trigger.soundID)) used.insert(trigger.soundID); std::set unused; - for (auto& id : content.sounds | std::views::keys) + for (const auto& [id, sound] : content.sounds) + { + (void)sound; if (!used.contains(id)) unused.insert(id); + } return unused; } diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp index 70bcb9d..dde0cd6 100644 --- a/src/anm2/frame.cpp +++ b/src/anm2/frame.cpp @@ -120,11 +120,4 @@ namespace anm2ed::anm2 void Frame::extend() { duration = glm::clamp(++duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } - bool Frame::is_visible(Type type) - { - if (type == TRIGGER) - return isVisible && eventID > -1; - else - return isVisible; - } } \ No newline at end of file diff --git a/src/anm2/frame.h b/src/anm2/frame.h index 9bba4cf..461c125 100644 --- a/src/anm2/frame.h +++ b/src/anm2/frame.h @@ -46,7 +46,6 @@ namespace anm2ed::anm2 void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type); void shorten(); void extend(); - bool is_visible(Type = NONE); }; struct FrameChange diff --git a/src/anm2/item.cpp b/src/anm2/item.cpp index 16a3cf0..cf8b202 100644 --- a/src/anm2/item.cpp +++ b/src/anm2/item.cpp @@ -77,6 +77,8 @@ namespace anm2ed::anm2 if (frames.empty()) return frame; + time = time < 0.0f ? 0.0f : time; + Frame* frameNext = nullptr; int durationCurrent = 0; int durationNext = 0; diff --git a/src/canvas.cpp b/src/canvas.cpp index e885625..3ad14c9 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -7,7 +7,6 @@ #include #include "math_.h" -#include "texture.h" using namespace glm; using namespace anm2ed::resource; @@ -25,8 +24,7 @@ namespace anm2ed Canvas::Canvas(vec2 size) { - this->size = size; - previousSize = size; + Framebuffer::size_set(size); // Axis glGenVertexArrays(1, &axisVAO); @@ -88,24 +86,12 @@ namespace anm2ed glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); glBindVertexArray(0); - - // Framebuffer(s) - glGenFramebuffers(1, &fbo); - glGenRenderbuffers(1, &rbo); - - // Framebuffer(s) Texture - glGenTextures(1, &texture); - - framebuffer_set(); } Canvas::~Canvas() { - if (!is_valid()) return; + if (!Framebuffer::is_valid()) return; - glDeleteFramebuffers(1, &fbo); - glDeleteRenderbuffers(1, &rbo); - glDeleteTextures(1, &texture); glDeleteVertexArrays(1, &axisVAO); glDeleteBuffers(1, &axisVBO); @@ -116,41 +102,6 @@ namespace anm2ed glDeleteBuffers(1, &rectVBO); } - bool Canvas::is_valid() const { return fbo != 0; } - - void Canvas::framebuffer_set() const - { - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); - - glBindRenderbuffer(GL_RENDERBUFFER, rbo); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x, size.y); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - void Canvas::framebuffer_resize_check() - { - if (previousSize != size) - { - framebuffer_set(); - previousSize = size; - } - } - - void Canvas::size_set(vec2 size) - { - this->size = size; - framebuffer_resize_check(); - } - mat4 Canvas::transform_get(float zoom, vec2 pan) const { auto zoomFactor = math::percent_to_unit(zoom); @@ -203,7 +154,7 @@ namespace anm2ed glUseProgram(0); } - void Canvas::texture_render(Shader& shader, GLuint& texture, mat4& transform, vec4 tint, vec3 colorOffset, + void Canvas::texture_render(Shader& shader, GLuint& texture, mat4 transform, vec4 tint, vec3 colorOffset, float* vertices) const { glUseProgram(shader.id); @@ -267,33 +218,6 @@ namespace anm2ed glUseProgram(0); } - void Canvas::viewport_set() const { glViewport(0, 0, size.x, size.y); } - - void Canvas::clear(vec4 color) const - { - glClearColor(color.r, color.g, color.b, color.a); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } - - void Canvas::bind() const { glBindFramebuffer(GL_FRAMEBUFFER, fbo); } - - void Canvas::unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } - - std::vector Canvas::pixels_get() const - { - auto count = size.x * size.y * texture::CHANNELS; - std::vector pixels(count); - - glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); - glReadBuffer(GL_COLOR_ATTACHMENT0); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glPixelStorei(GL_PACK_ROW_LENGTH, 0); - glReadPixels(0, 0, size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - - return pixels; - } - void Canvas::zoom_set(float& zoom, vec2& pan, vec2 focus, float step) const { auto zoomFactor = math::percent_to_unit(zoom); @@ -306,17 +230,6 @@ namespace anm2ed } } - vec4 Canvas::pixel_read(vec2 position, vec2 framebufferSize) const - { - uint8_t rgba[4]{}; - - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glReadPixels(position.x, framebufferSize.y - 1 - position.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba); - - return vec4(math::uint8_to_float(rgba[0]), math::uint8_to_float(rgba[1]), math::uint8_to_float(rgba[2]), - math::uint8_to_float(rgba[3])); - } - vec2 Canvas::position_translate(float& zoom, vec2& pan, vec2 position) const { auto zoomFactor = math::percent_to_unit(zoom); diff --git a/src/canvas.h b/src/canvas.h index b019077..780279e 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -3,6 +3,7 @@ #include #include +#include "framebuffer.h" #include "shader.h" namespace anm2ed::canvas @@ -31,11 +32,10 @@ namespace anm2ed::canvas namespace anm2ed { - class Canvas + + class Canvas : public Framebuffer { public: - GLuint fbo{}; - GLuint rbo{}; GLuint axisVAO{}; GLuint axisVBO{}; GLuint rectVAO{}; @@ -45,34 +45,21 @@ namespace anm2ed GLuint textureVAO{}; GLuint textureVBO{}; GLuint textureEBO{}; - GLuint texture{}; - glm::vec2 previousSize{}; - glm::vec2 size{}; Canvas(); Canvas(glm::vec2); ~Canvas(); - bool is_valid() const; - void framebuffer_set() const; - void framebuffer_resize_check(); - void size_set(glm::vec2); - glm::vec4 pixel_read(glm::vec2, glm::vec2) const; glm::mat4 transform_get(float = 100.0f, glm::vec2 = {}) const; void axes_render(resource::Shader&, float, glm::vec2, glm::vec4 = glm::vec4(1.0f)) const; void grid_render(resource::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {}, glm::vec4 = glm::vec4(1.0f)) const; - void texture_render(resource::Shader&, GLuint&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}, + void texture_render(resource::Shader&, GLuint&, glm::mat4 = {1.0f}, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}, float* = (float*)canvas::TEXTURE_VERTICES) const; void rect_render(resource::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f), float dashLength = canvas::DASH_LENGTH, float dashGap = canvas::DASH_GAP, float dashOffset = canvas::DASH_OFFSET) const; - void viewport_set() const; - void clear(glm::vec4 = glm::vec4()) const; - void bind() const; - void unbind() const; void zoom_set(float&, glm::vec2&, glm::vec2, float) const; glm::vec2 position_translate(float&, glm::vec2&, glm::vec2) const; void set_to_rect(float& zoom, glm::vec2& pan, glm::vec4 rect) const; - std::vector pixels_get() const; }; } diff --git a/src/dialog.h b/src/dialog.h index 928b06f..8750870 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -47,6 +47,7 @@ namespace anm2ed::dialog X(SPRITESHEET_REPLACE, PNG) \ X(FFMPEG_PATH_SET, EXECUTABLE) \ X(PNG_DIRECTORY_SET, NO_FILTER) \ + X(PNG_PATH_SET, PNG) \ X(GIF_PATH_SET, GIF) \ X(WEBM_PATH_SET, WEBM) \ X(MP4_PATH_SET, MP4) diff --git a/src/document.cpp b/src/document.cpp index 4813bdf..0cc0c38 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -13,6 +13,14 @@ using namespace glm; namespace anm2ed { + Document::Document(Anm2& anm2, const std::string& path) + { + this->anm2 = std::move(anm2); + this->path = path; + clean(); + change(Document::ALL); + } + Document::Document(const std::string& path, bool isNew, std::string* errorString) { if (isNew) @@ -176,6 +184,10 @@ namespace anm2ed case ANIMATIONS: animations_set(); break; + case FRAMES: + events_set(); + sounds_set(); + break; case ALL: layers_set(); nulls_set(); diff --git a/src/document.h b/src/document.h index c37ff07..71f64b2 100644 --- a/src/document.h +++ b/src/document.h @@ -61,6 +61,7 @@ namespace anm2ed bool isAnimationPreviewSet{false}; bool isSpritesheetEditorSet{false}; + Document(anm2::Anm2& anm2, const std::string&); Document(const std::string&, bool = false, std::string* = nullptr); Document(const Document&) = delete; Document& operator=(const Document&) = delete; diff --git a/src/framebuffer.cpp b/src/framebuffer.cpp new file mode 100644 index 0000000..0ef0c25 --- /dev/null +++ b/src/framebuffer.cpp @@ -0,0 +1,88 @@ +#include "framebuffer.h" + +#include "texture.h" + +using namespace anm2ed::resource; +using namespace glm; + +namespace anm2ed +{ + Framebuffer::Framebuffer() + { + glGenFramebuffers(1, &fbo); + glGenRenderbuffers(1, &rbo); + glGenTextures(1, &texture); + set(); + } + + Framebuffer::~Framebuffer() + { + if (!is_valid()) return; + + glDeleteFramebuffers(1, &fbo); + glDeleteRenderbuffers(1, &rbo); + glDeleteTextures(1, &texture); + } + + void Framebuffer::set() + { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + + glBindRenderbuffer(GL_RENDERBUFFER, rbo); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x, size.y); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + void Framebuffer::resize_check() + { + if (size != previousSize) + { + set(); + previousSize = size; + } + } + + void Framebuffer::size_set(vec2 size) + { + previousSize = this->size; + this->size = size; + resize_check(); + } + + std::vector Framebuffer::pixels_get() const + { + auto count = size.x * size.y * texture::CHANNELS; + std::vector pixels(count); + + glReadBuffer(GL_COLOR_ATTACHMENT0); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + glReadPixels(0, 0, size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + return pixels; + } + + void Framebuffer::clear(vec4 color) const + { + glEnable(GL_BLEND); + glClearColor(color.r, color.g, color.b, color.a); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if (color.a == 0.0f) glDisable(GL_BLEND); + } + + bool Framebuffer::is_valid() const { return fbo != 0; } + void Framebuffer::viewport_set() const { glViewport(0, 0, size.x, size.y); } + void Framebuffer::bind() const { glBindFramebuffer(GL_FRAMEBUFFER, fbo); } + void Framebuffer::unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } +} diff --git a/src/framebuffer.h b/src/framebuffer.h new file mode 100644 index 0000000..2610456 --- /dev/null +++ b/src/framebuffer.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace anm2ed +{ + class Framebuffer + { + public: + enum Type + { + UNDERLAY, + LAYER, + OVERLAY, + }; + + GLuint fbo{}; + GLuint rbo{}; + GLuint texture{}; + glm::vec2 size{}; + glm::vec2 previousSize{}; + + Framebuffer(); + ~Framebuffer(); + + void set(); + void resize_check(); + void size_set(glm::vec2); + void viewport_set() const; + void clear(glm::vec4 = glm::vec4(1.0f, 1.0f, 1.0f, 0.0f)) const; + std::vector pixels_get() const; + bool is_valid() const; + void bind() const; + void unbind() const; + }; +}; \ No newline at end of file diff --git a/src/imgui/dockspace.cpp b/src/imgui/dockspace.cpp index 2ca7218..8e12bf3 100644 --- a/src/imgui/dockspace.cpp +++ b/src/imgui/dockspace.cpp @@ -5,7 +5,7 @@ namespace anm2ed::imgui void Dockspace::tick(Manager& manager, Settings& settings) { if (auto document = manager.get(); document) - if (settings.windowIsAnimationPreview) animationPreview.tick(manager, *document, settings); + if (settings.windowIsAnimationPreview) animationPreview.tick(manager, settings); } void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings, diff --git a/src/imgui/documents.cpp b/src/imgui/documents.cpp index c4d419f..348fddc 100644 --- a/src/imgui/documents.cpp +++ b/src/imgui/documents.cpp @@ -22,9 +22,11 @@ namespace anm2ed::imgui for (auto& document : manager.documents) { auto isDirty = document.is_dirty() && document.is_autosave_dirty(); - document.lastAutosaveTime += ImGui::GetIO().DeltaTime; - - if (isDirty && document.lastAutosaveTime > settings.fileAutosaveTime * time::SECOND_M) manager.autosave(document); + if (isDirty) + { + document.lastAutosaveTime += ImGui::GetIO().DeltaTime; + if (document.lastAutosaveTime > settings.fileAutosaveTime * time::SECOND_M) manager.autosave(document); + } } if (ImGui::Begin("##Documents", nullptr, @@ -157,5 +159,60 @@ namespace anm2ed::imgui } ImGui::End(); + + if (manager.isAnm2DragDrop) + { + auto drag_drop_reset = [&]() + { + manager.isAnm2DragDrop = false; + manager.anm2DragDropPaths.clear(); + manager.anm2DragDropPopup.close(); + }; + + if (manager.anm2DragDropPaths.empty()) + drag_drop_reset(); + else + { + if (!manager.anm2DragDropPopup.is_open()) manager.anm2DragDropPopup.open(); + + bool wasOpen = manager.anm2DragDropPopup.is_open(); + manager.anm2DragDropPopup.trigger(); + + if (ImGui::BeginPopupContextWindow(manager.anm2DragDropPopup.label, ImGuiPopupFlags_None)) + { + auto document = manager.get(); + if (ImGui::MenuItem(manager.anm2DragDropPaths.size() > 1 ? "Open Many Documents" : "Open New Document")) + { + for (auto& path : manager.anm2DragDropPaths) + manager.open(path); + drag_drop_reset(); + } + + if (ImGui::MenuItem("Merge into Current Document", nullptr, false, + document && !manager.anm2DragDropPaths.empty())) + { + if (document) + { + DOCUMENT_EDIT_PTR(document, "Merge Anm2", Document::ALL, { + for (auto& path : manager.anm2DragDropPaths) + { + anm2::Anm2 source(path); + document->anm2.merge(source, document->directory_get(), path.parent_path()); + } + }); + drag_drop_reset(); + } + } + + if (ImGui::MenuItem("Cancel")) drag_drop_reset(); + + manager.anm2DragDropPopup.end(); + ImGui::EndPopup(); + } + else if (wasOpen && !manager.anm2DragDropPopup.is_open()) + drag_drop_reset(); + } + } } + } diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index b1d7dea..1ebd66c 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -105,6 +106,50 @@ namespace anm2ed::imgui ImGui::SetItemTooltip("%s\n(Shortcut: %s)", tooltip, shortcut.c_str()); } + namespace + { + struct CheckerStart + { + float position{}; + long long index{}; + }; + + CheckerStart checker_start(float minCoord, float offset, float step) + { + float world = minCoord + offset; + long long idx = static_cast(std::floor(world / step)); + float first = minCoord - (world - static_cast(idx) * step); + return {first, idx}; + } + } + + void render_checker_background(ImDrawList* drawList, ImVec2 min, ImVec2 max, vec2 offset, float step) + { + if (!drawList || step <= 0.0f) return; + + const ImU32 colorLight = IM_COL32(204, 204, 204, 255); + const ImU32 colorDark = IM_COL32(128, 128, 128, 255); + + auto [startY, rowIndex] = checker_start(min.y, offset.y, step); + for (float y = startY; y < max.y; y += step, ++rowIndex) + { + float y1 = glm::max(y, min.y); + float y2 = glm::min(y + step, max.y); + if (y2 <= y1) continue; + + auto [startX, columnIndex] = checker_start(min.x, offset.x, step); + for (float x = startX; x < max.x; x += step, ++columnIndex) + { + float x1 = glm::max(x, min.x); + float x2 = glm::min(x + step, max.x); + if (x2 <= x1) continue; + + bool isDark = ((rowIndex + columnIndex) & 1LL) != 0; + drawList->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), isDark ? colorDark : colorLight); + } + } + } + void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected) { auto* set = (std::set*)self->UserData; @@ -248,9 +293,12 @@ namespace anm2ed::imgui return false; } - bool shortcut(ImGuiKeyChord chord, shortcut::Type type) + bool shortcut(ImGuiKeyChord chord, shortcut::Type type, bool isRepeat) { if (ImGui::GetTopMostPopupModal() != nullptr) return false; + + if (isRepeat && (type == shortcut::GLOBAL || type == shortcut::FOCUSED)) return chord_repeating(chord); + int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal : ImGuiInputFlags_RouteFocused; if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET) @@ -301,15 +349,21 @@ namespace anm2ed::imgui auto viewport = ImGui::GetMainViewport(); - if (position == POPUP_CENTER) - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f))); - else - ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None); - - if (POPUP_IS_HEIGHT_SET[type]) - ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type])); - else - ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0)); + switch (position) + { + case POPUP_CENTER: + ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f))); + if (POPUP_IS_HEIGHT_SET[type]) + ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * POPUP_MULTIPLIERS[type])); + else + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0)); + break; + case POPUP_BY_ITEM: + ImGui::SetNextWindowPos(ImGui::GetItemRectMin(), ImGuiCond_None); + case POPUP_BY_CURSOR: + default: + break; + } } void PopupHelper::end() { isJustOpened = false; } diff --git a/src/imgui/imgui_.h b/src/imgui/imgui_.h index f9bc156..1277b72 100644 --- a/src/imgui/imgui_.h +++ b/src/imgui/imgui_.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -31,7 +32,8 @@ namespace anm2ed::imgui enum PopupPosition { POPUP_CENTER, - POPUP_BY_ITEM + POPUP_BY_ITEM, + POPUP_BY_CURSOR }; constexpr float POPUP_MULTIPLIERS[] = { @@ -171,10 +173,11 @@ namespace anm2ed::imgui ImGuiSelectableFlags = 0, bool* = nullptr); void set_item_tooltip_shortcut(const char*, const std::string& = {}); void external_storage_set(ImGuiSelectionExternalStorage*, int, bool); + void render_checker_background(ImDrawList*, ImVec2, ImVec2, glm::vec2, float); ImVec2 icon_size_get(); bool chord_held(ImGuiKeyChord); bool chord_repeating(ImGuiKeyChord, float = ImGui::GetIO().KeyRepeatDelay, float = ImGui::GetIO().KeyRepeatRate); - bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET); + bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET, bool = false); class MultiSelectStorage : public std::set { diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index 3a1e75f..4f71f71 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -3,10 +3,11 @@ #include #include #include +#include #include #include #include -#include +#include #include #include @@ -18,6 +19,7 @@ #include "types.h" #include "icon.h" +#include "toast.h" using namespace anm2ed::resource; using namespace anm2ed::types; @@ -148,11 +150,12 @@ namespace anm2ed::imgui auto recentFiles = manager.recent_files_ordered(); if (ImGui::BeginMenu("Open Recent", !recentFiles.empty())) { - for (auto [i, file] : std::views::enumerate(recentFiles)) + for (std::size_t index = 0; index < recentFiles.size(); ++index) { + const auto& file = recentFiles[index]; auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string()); - ImGui::PushID(i); + ImGui::PushID((int)index); if (ImGui::MenuItem(label.c_str())) manager.open(file.string()); ImGui::PopID(); } @@ -218,8 +221,11 @@ namespace anm2ed::imgui if (ImGui::BeginMenu("Window")) { - for (auto [i, member] : std::views::enumerate(WINDOW_MEMBERS)) - ImGui::MenuItem(WINDOW_STRINGS[i], nullptr, &(settings.*member)); + for (std::size_t index = 0; index < WINDOW_COUNT; ++index) + { + auto member = WINDOW_MEMBERS[index]; + ImGui::MenuItem(WINDOW_STRINGS[index], nullptr, &(settings.*member)); + } ImGui::EndMenu(); } @@ -288,7 +294,7 @@ namespace anm2ed::imgui generate.size_set(to_vec2(previewSize)); generate.bind(); generate.viewport_set(); - generate.clear(backgroundColor); + generate.clear(vec4(backgroundColor, 1.0f)); if (document && document->reference.itemType == anm2::LAYER) { @@ -455,8 +461,9 @@ namespace anm2ed::imgui if (ImGui::IsKeyDown(ImGuiMod_Alt)) chord |= ImGuiMod_Alt; if (ImGui::IsKeyDown(ImGuiMod_Super)) chord |= ImGuiMod_Super; - for (auto& key : KEY_MAP | std::views::values) + for (const auto& entry : KEY_MAP) { + auto key = entry.second; if (ImGui::IsKeyPressed(key)) { chord |= key; @@ -521,34 +528,95 @@ namespace anm2ed::imgui auto& frames = document->frames.selection; int length = std::max(1, end - start + 1); + auto ffmpeg_is_executable = [](const std::string& pathString) + { + if (pathString.empty()) return false; + + std::error_code ec{}; + auto status = std::filesystem::status(pathString, ec); + if (ec || !std::filesystem::is_regular_file(status)) return false; + +#ifndef _WIN32 + constexpr auto EXEC_PERMS = std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | + std::filesystem::perms::others_exec; + if ((status.permissions() & EXEC_PERMS) == std::filesystem::perms::none) return false; +#endif + return true; + }; + + auto png_directory_ensure = [](const std::string& directory) + { + if (directory.empty()) + { + toasts.error("PNG output directory must be set."); + return false; + } + + std::error_code ec{}; + auto pathValue = std::filesystem::path(directory); + auto exists = std::filesystem::exists(pathValue, ec); + + if (ec) + { + toasts.error(std::format("Could not access directory: {} ({})", directory, ec.message())); + return false; + } + + if (exists) + { + if (!std::filesystem::is_directory(pathValue, ec) || ec) + { + toasts.error(std::format("PNG output path must be a directory: {}", directory)); + return false; + } + return true; + } + + if (!std::filesystem::create_directories(pathValue, ec) || ec) + { + toasts.error(std::format("Could not create directory: {} ({})", directory, ec.message())); + return false; + } + + return true; + }; + + auto range_to_frames_set = [&]() + { + if (auto item = document->item_get()) + { + int duration{}; + for (std::size_t index = 0; index < item->frames.size(); ++index) + { + const auto& frame = item->frames[index]; + + if ((int)index == *frames.begin()) + start = duration; + else if ((int)index == *frames.rbegin()) + { + end = duration; + break; + } + + duration += frame.duration; + } + } + }; + + auto range_to_animation_set = [&]() + { + start = 0; + end = animation->frameNum - 1; + }; + auto range_set = [&]() { if (!frames.empty()) - { - if (auto item = document->item_get()) - { - int duration{}; - for (auto [i, frame] : std::views::enumerate(item->frames)) - { - if (i == *frames.begin()) - start = duration; - else if (i == *frames.rbegin()) - { - end = duration; - break; - } - - duration += frame.duration; - } - } - } + range_to_frames_set(); else if (!isRange) - { - start = 0; - end = animation->frameNum - 1; - } + range_to_animation_set(); - length = std::max(1, end - start + 1); + length = std::max(1, end - (start + 1)); }; auto rows_columns_set = [&]() @@ -652,15 +720,20 @@ namespace anm2ed::imgui ImGui::SameLine(); ImGui::BeginDisabled(frames.empty()); - if (ImGui::Button("To Selected Frames")) range_set(); + if (ImGui::Button("To Selected Frames")) range_to_frames_set(); ImGui::SetItemTooltip("If frames are selected, use that range for the rendered animation."); ImGui::EndDisabled(); + ImGui::SameLine(); + + if (ImGui::Button("To Animation Range")) range_to_animation_set(); + ImGui::SetItemTooltip("Set the range to the normal range of the animation."); + ImGui::BeginDisabled(!isRange); { - input_int_range("Start", start, 0, animation->frameNum - 1); + input_int_range("Start", start, 0, animation->frameNum); ImGui::SetItemTooltip("Set the starting time of the animation."); - input_int_range("End", end, start + 1, animation->frameNum); + input_int_range("End", end, start, animation->frameNum); ImGui::SetItemTooltip("Set the ending time of the animation."); } ImGui::EndDisabled(); @@ -687,9 +760,22 @@ namespace anm2ed::imgui if (ImGui::Button("Render", widgetSize)) { - manager.isRecordingStart = true; + bool isRender = true; + if (!ffmpeg_is_executable(ffmpegPath)) + { + toasts.error("FFmpeg path must point to a valid executable file."); + isRender = false; + } + + if (isRender && type == render::PNGS) isRender = png_directory_ensure(path); + + if (isRender) + { + manager.isRecordingStart = true; + manager.progressPopup.open(); + } + renderPopup.close(); - manager.progressPopup.open(); } ImGui::SetItemTooltip("Render the animation using the current settings."); diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index 295eeb7..e93633d 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -1,12 +1,12 @@ #include "animation_preview.h" #include +#include #include -#include #include -#include "imgui_internal.h" +#include "imgui_.h" #include "log.h" #include "math_.h" #include "toast.h" @@ -38,6 +38,7 @@ namespace anm2ed::imgui auto& frameTime = document.frameTime; auto& end = manager.recordingEnd; auto& zoom = document.previewZoom; + auto& overlayIndex = document.overlayIndex; auto& pan = document.previewPan; if (manager.isRecording) @@ -46,19 +47,17 @@ namespace anm2ed::imgui auto& path = settings.renderPath; auto& type = settings.renderType; - auto pixels = pixels_get(); - renderFrames.push_back(Texture(pixels.data(), size)); - if (playback.time > end || playback.isFinished) { if (type == render::PNGS) { auto& format = settings.renderFormat; bool isSuccess{true}; - for (auto [i, frame] : std::views::enumerate(renderFrames)) + for (std::size_t index = 0; index < renderFrames.size(); ++index) { + auto& frame = renderFrames[index]; std::filesystem::path outputPath = - std::filesystem::path(path) / std::vformat(format, std::make_format_args(i)); + std::filesystem::path(path) / std::vformat(format, std::make_format_args(index)); if (!frame.write_png(outputPath)) { @@ -93,10 +92,11 @@ namespace anm2ed::imgui std::vector spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS); - for (auto [i, frame] : std::views::enumerate(renderFrames)) + for (std::size_t index = 0; index < renderFrames.size(); ++index) { - auto row = (int)(i / columns); - auto column = (int)(i % columns); + const auto& frame = renderFrames[index]; + auto row = (int)(index / columns); + auto column = (int)(index % columns); if (row >= rows || column >= columns) break; if ((int)frame.pixels.size() < frameWidth * frameHeight * CHANNELS) continue; @@ -131,6 +131,7 @@ namespace anm2ed::imgui pan = savedPan; zoom = savedZoom; settings = savedSettings; + overlayIndex = savedOverlayIndex; isSizeTrySet = true; if (settings.timelineIsSound) audioStream.capture_end(mixer); @@ -140,6 +141,13 @@ namespace anm2ed::imgui manager.isRecording = false; manager.progressPopup.close(); } + else + { + + bind(); + auto pixels = pixels_get(); + renderFrames.push_back(Texture(pixels.data(), size)); + } } if (playback.isPlaying) @@ -247,7 +255,7 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { - ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); + ImGui::ColorEdit3("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); ImGui::SetItemTooltip("Change the background color."); ImGui::SameLine(); ImGui::Checkbox("Axes", &isAxes); @@ -311,6 +319,7 @@ namespace anm2ed::imgui settings.timelineIsOnlyShowLayers = true; settings.onionskinIsEnabled = false; + savedOverlayIndex = overlayIndex; savedZoom = zoom; savedPan = pan; @@ -329,10 +338,12 @@ namespace anm2ed::imgui playback.time = manager.recordingStart; } - if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail())); - viewport_set(); + size_set(to_vec2(ImGui::GetContentRegionAvail())); + bind(); - clear(); + viewport_set(); + clear(manager.isRecording && settings.renderIsRawAnimation ? vec4() : vec4(backgroundColor, 1.0f)); + if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor); if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); @@ -468,9 +479,7 @@ namespace anm2ed::imgui unbind(); - ImGui::RenderColorRectWithAlphaCheckerboard(ImGui::GetWindowDrawList(), min, max, 0, CHECKER_SIZE, - to_imvec2(-size + pan)); - ImGui::GetCurrentWindow()->DrawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(backgroundColor))); + render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - pan, CHECKER_SIZE); ImGui::Image(texture, to_imvec2(size)); isPreviewHovered = ImGui::IsItemHovered(); diff --git a/src/imgui/window/animation_preview.h b/src/imgui/window/animation_preview.h index 26f4cbf..c2b1fdf 100644 --- a/src/imgui/window/animation_preview.h +++ b/src/imgui/window/animation_preview.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "audio_stream.h" #include "canvas.h" #include "manager.h" @@ -19,15 +17,13 @@ namespace anm2ed::imgui Settings savedSettings{}; float savedZoom{}; glm::vec2 savedPan{}; + int savedOverlayIndex{}; glm::ivec2 mousePos{}; std::vector renderFrames{}; - std::future renderFuture{}; - bool isRenderFutureValid{}; - std::string renderOutputPath{}; public: AnimationPreview(); - void tick(Manager&, Document&, Settings&); + void tick(Manager&, Settings&); void update(Manager&, Settings&, Resources&); }; } diff --git a/src/imgui/window/animations.cpp b/src/imgui/window/animations.cpp index 4fc6e3d..c376dd9 100644 --- a/src/imgui/window/animations.cpp +++ b/src/imgui/window/animations.cpp @@ -1,6 +1,6 @@ #include "animations.h" -#include +#include #include "toast.h" #include "vector_.h" @@ -24,6 +24,21 @@ namespace anm2ed::imgui hovered = -1; + auto animations_remove = [&]() + { + if (!selection.empty()) + { + for (auto it = selection.rbegin(); it != selection.rend(); ++it) + { + auto i = *it; + if (overlayIndex == i) overlayIndex = -1; + if (reference.animationIndex == i) reference.animationIndex = -1; + anm2.animations.items.erase(anm2.animations.items.begin() + i); + } + selection.clear(); + } + }; + if (ImGui::Begin("Animations", &settings.windowIsAnimations)) { auto childSize = size_without_footer_get(); @@ -32,12 +47,13 @@ namespace anm2ed::imgui { selection.start(anm2.animations.items.size()); - for (auto [i, animation] : std::views::enumerate(anm2.animations.items)) + for (std::size_t index = 0; index < anm2.animations.items.size(); ++index) { - ImGui::PushID(i); + auto& animation = anm2.animations.items[index]; + ImGui::PushID((int)index); auto isDefault = anm2.animations.defaultAnimation == animation.name; - auto isReferenced = reference.animationIndex == i; + auto isReferenced = reference.animationIndex == (int)index; auto font = isDefault && isReferenced ? font::BOLD_ITALICS : isDefault ? font::BOLD @@ -45,14 +61,14 @@ namespace anm2ed::imgui : font::REGULAR; ImGui::PushFont(resources.fonts[font].get(), font::SIZE); - ImGui::SetNextItemSelectionUserData((int)i); - if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i), - animation.name, selection.contains((int)i))) + ImGui::SetNextItemSelectionUserData((int)index); + if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, index), + animation.name, selection.contains((int)index))) { - reference = {(int)i}; + reference = {(int)index}; document.frames.clear(); } - if (ImGui::IsItemHovered()) hovered = (int)i; + if (ImGui::IsItemHovered()) hovered = (int)index; ImGui::PopFont(); if (ImGui::BeginItemTooltip()) @@ -121,23 +137,7 @@ namespace anm2ed::imgui auto cut = [&]() { copy(); - - auto remove = [&]() - { - if (!selection.empty()) - { - for (auto& i : selection | std::views::reverse) - anm2.animations.items.erase(anm2.animations.items.begin() + i); - selection.clear(); - } - else if (hovered > -1) - { - anm2.animations.items.erase(anm2.animations.items.begin() + hovered); - hovered = -1; - } - }; - - DOCUMENT_EDIT(document, "Cut Animation(s)", Document::ANIMATIONS, remove()); + DOCUMENT_EDIT(document, "Cut Animation(s)", Document::ANIMATIONS, animations_remove()); }; auto paste = [&]() @@ -275,19 +275,7 @@ namespace anm2ed::imgui shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button("Remove", widgetSize)) - { - auto remove = [&]() - { - for (auto& i : selection | std::views::reverse) - { - if (i == overlayIndex) overlayIndex = -1; - anm2.animations.items.erase(anm2.animations.items.begin() + i); - } - selection.clear(); - }; - - DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, remove()); - } + DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, animations_remove()); set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutRemove); ImGui::SameLine(); @@ -328,14 +316,16 @@ namespace anm2ed::imgui { mergeSelection.start(anm2.animations.items.size()); - for (auto [i, animation] : std::views::enumerate(anm2.animations.items)) + for (std::size_t index = 0; index < anm2.animations.items.size(); ++index) { - if (i == mergeReference) continue; + if ((int)index == mergeReference) continue; - ImGui::PushID(i); + auto& animation = anm2.animations.items[index]; - ImGui::SetNextItemSelectionUserData(i); - ImGui::Selectable(animation.name.c_str(), mergeSelection.contains(i)); + ImGui::PushID((int)index); + + ImGui::SetNextItemSelectionUserData((int)index); + ImGui::Selectable(animation.name.c_str(), mergeSelection.contains((int)index)); ImGui::PopID(); } @@ -399,4 +389,4 @@ namespace anm2ed::imgui } ImGui::End(); } -} \ No newline at end of file +} diff --git a/src/imgui/window/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp index 7094796..43c1797 100644 --- a/src/imgui/window/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -1,8 +1,10 @@ #include "spritesheet_editor.h" #include +#include #include +#include "imgui_.h" #include "imgui_internal.h" #include "math_.h" #include "tool.h" @@ -41,6 +43,7 @@ namespace anm2ed::imgui auto& isGridSnap = settings.editorIsGridSnap; auto& zoomStep = settings.viewZoomStep; auto& isBorder = settings.editorIsBorder; + auto& isTransparent = settings.editorIsTransparent; auto spritesheet = document.spritesheet_get(); auto& tool = settings.tool; auto& shaderGrid = resources.shaders[shader::GRID]; @@ -101,22 +104,40 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { - ImGui::ColorEdit4("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); - ImGui::SetItemTooltip("Change the background color."); + auto subChildSize = ImVec2(row_widget_width_get(2), ImGui::GetContentRegionAvail().y); - ImGui::Checkbox("Border", &isBorder); - ImGui::SetItemTooltip("Toggle a border appearing around the spritesheet."); + if (ImGui::BeginChild("##Background Child 1", subChildSize)) + { + ImGui::ColorEdit3("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); + ImGui::SetItemTooltip("Change the background color."); + + ImGui::Checkbox("Border", &isBorder); + ImGui::SetItemTooltip("Toggle a border appearing around the spritesheet."); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("##Background Child 2", subChildSize)) + { + ImGui::Checkbox("Transparent", &isTransparent); + ImGui::SetItemTooltip("Toggle the spritesheet editor being transparent."); + } + + ImGui::EndChild(); } ImGui::EndChild(); + auto drawList = ImGui::GetCurrentWindow()->DrawList; auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto min = ImGui::GetCursorScreenPos(); auto max = to_imvec2(to_vec2(min) + size); size_set(to_vec2(ImGui::GetContentRegionAvail())); + bind(); viewport_set(); - clear(); + clear(isTransparent ? vec4() : vec4(backgroundColor, 1.0f)); auto frame = document.frame_get(); @@ -127,7 +148,11 @@ namespace anm2ed::imgui auto spritesheetModel = math::quad_model_get(texture.size); auto spritesheetTransform = transform * spritesheetModel; + texture_render(shaderTexture, texture.id, spritesheetTransform); + + if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); + if (isBorder) rect_render(dashedShader, spritesheetTransform, spritesheetModel, color::WHITE, BORDER_DASH_LENGTH, BORDER_DASH_GAP, BORDER_DASH_OFFSET); @@ -145,14 +170,12 @@ namespace anm2ed::imgui } } - if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); - unbind(); - ImGui::RenderColorRectWithAlphaCheckerboard(ImGui::GetWindowDrawList(), min, max, 0, CHECKER_SIZE, - to_imvec2(-size * 0.5f + pan)); - ImGui::GetCurrentWindow()->DrawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(backgroundColor))); - ImGui::Image(texture, to_imvec2(size)); + render_checker_background(drawList, min, max, -size * 0.5f - pan, CHECKER_SIZE); + if (!isTransparent) drawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(vec4(backgroundColor, 1.0f)))); + drawList->AddImage(texture, min, max); + ImGui::InvisibleButton("##Spritesheet Editor", to_imvec2(size)); if (ImGui::IsItemHovered()) { @@ -325,13 +348,18 @@ namespace anm2ed::imgui } case tool::COLOR_PICKER: { - if (isDuring) + if (spritesheet && isDuring) { - auto position = to_vec2(ImGui::GetMousePos()); - toolColor = pixel_read(position, {settings.windowSize.x, settings.windowSize.y}); + toolColor = spritesheet->texture.pixel_read(mousePos); if (ImGui::BeginTooltip()) { ImGui::ColorButton("##Color Picker Button", to_imvec4(toolColor)); + ImGui::SameLine(); + auto rgba8 = glm::clamp(ivec4(toolColor * 255.0f + 0.5f), ivec4(0), ivec4(255)); + auto hex = std::format("#{:02X}{:02X}{:02X}{:02X}", rgba8.r, rgba8.g, rgba8.b, rgba8.a); + ImGui::TextUnformatted(hex.c_str()); + ImGui::SameLine(); + ImGui::Text("(%d, %d, %d, %d)", rgba8.r, rgba8.g, rgba8.b, rgba8.a); ImGui::EndTooltip(); } } diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index d48bc5c..f2a2bdc 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -1,7 +1,7 @@ #include "timeline.h" #include -#include +#include #include @@ -53,8 +53,11 @@ namespace anm2ed::imgui { if (auto item = animation->item_get(reference.itemType, reference.itemID); item) { - for (auto& i : frames.selection | std::views::reverse) + for (auto it = frames.selection.rbegin(); it != frames.selection.rend(); ++it) + { + auto i = *it; item->frames.erase(item->frames.begin() + i); + } reference.frameIndex = -1; frames.clear(); @@ -93,7 +96,12 @@ namespace anm2ed::imgui document.snapshot("Paste Frame(s)"); std::set indices{}; std::string errorString{}; - auto insertIndex = reference.frameIndex == -1 ? item->frames.size() : reference.frameIndex + 1; + int insertIndex = (int)item->frames.size(); + if (!frames.selection.empty()) + insertIndex = std::min((int)item->frames.size(), *frames.selection.rbegin() + 1); + else if (reference.frameIndex >= 0 && reference.frameIndex < (int)item->frames.size()) + insertIndex = reference.frameIndex + 1; + auto start = reference.itemType == anm2::TRIGGER ? hoveredTime : insertIndex; if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) { @@ -249,7 +257,7 @@ namespace anm2ed::imgui 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, icon_size_get())) DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, item->isVisible = !item->isVisible); ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show."); @@ -544,15 +552,17 @@ namespace anm2ed::imgui frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); - for (auto [i, frame] : std::views::enumerate(item->frames)) + for (std::size_t frameIndex = 0; frameIndex < item->frames.size(); ++frameIndex) { - ImGui::PushID(i); + auto& frame = item->frames[frameIndex]; + ImGui::PushID((int)frameIndex); - auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)i}; + auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)frameIndex}; auto isFrameVisible = isVisible && frame.isVisible; auto isReferenced = reference == frameReference; auto isSelected = - (frames.selection.contains(i) && reference.itemType == type && reference.itemID == id) || isReferenced; + (frames.selection.contains((int)frameIndex) && reference.itemType == type && reference.itemID == id) || + isReferenced; if (type == anm2::TRIGGER) frameTime = frame.atFrame; @@ -569,7 +579,7 @@ namespace anm2ed::imgui ImGui::PushStyleColor(ImGuiCol_HeaderHovered, isFrameVisible ? colorHovered : colorHoveredHidden); ImGui::SetNextItemAllowOverlap(); - ImGui::SetNextItemSelectionUserData((int)i); + ImGui::SetNextItemSelectionUserData((int)frameIndex); if (ImGui::Selectable("##Frame Button", true, ImGuiSelectableFlags_None, buttonSize)) { if (type == anm2::LAYER) @@ -785,9 +795,10 @@ namespace anm2ed::imgui draggedTrigger->atFrame = glm::clamp( hoveredTime, 0, settings.playbackIsClamp ? animation->frameNum - 1 : anm2::FRAME_NUM_MAX - 1); - for (auto&& [i, trigger] : std::views::enumerate(animation->triggers.frames)) + for (std::size_t triggerIndex = 0; triggerIndex < animation->triggers.frames.size(); ++triggerIndex) { - if (i == draggedTriggerIndex) continue; + if ((int)triggerIndex == draggedTriggerIndex) continue; + auto& trigger = animation->triggers.frames[triggerIndex]; if (trigger.atFrame == draggedTrigger->atFrame) draggedTrigger->atFrame--; } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) @@ -888,8 +899,9 @@ namespace anm2ed::imgui frames_child_row(anm2::LAYER, id); } - for (auto& id : animation->nullAnimations | std::views::keys) + for (const auto& entry : animation->nullAnimations) { + auto id = entry.first; if (auto item = animation->item_get(anm2::NULL_, id); item) if (!settings.timelineIsShowUnused && item->frames.empty()) continue; frames_child_row(anm2::NULL_, id); @@ -1259,9 +1271,12 @@ namespace anm2ed::imgui if (ImGui::Button("Bake", widgetSize)) { if (auto item = document.item_get()) - for (auto i : frames.selection | std::views::reverse) + for (auto it = frames.selection.rbegin(); it != frames.selection.rend(); ++it) + { + auto i = *it; DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES, item->frames_bake(i, interval, isRoundScale, isRoundRotation)); + } bakePopup.close(); } ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected."); @@ -1274,40 +1289,42 @@ namespace anm2ed::imgui ImGui::EndPopup(); } - if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle(); - if (animation) { - if (chord_repeating(manager.chords[SHORTCUT_PREVIOUS_FRAME])) + if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle(); + + if (shortcut(manager.chords[SHORTCUT_PREVIOUS_FRAME], shortcut::GLOBAL, true)) { playback.decrement(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); document.frameTime = playback.time; } - if (chord_repeating(manager.chords[SHORTCUT_NEXT_FRAME])) + if (shortcut(manager.chords[SHORTCUT_NEXT_FRAME], shortcut::GLOBAL, true)) { playback.increment(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); document.frameTime = playback.time; } - } - if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_SHORTEN_FRAME])) document.snapshot("Shorten Frame"); - if (chord_repeating(manager.chords[SHORTCUT_SHORTEN_FRAME])) - { - if (auto frame = document.frame_get()) + if (shortcut(manager.chords[SHORTCUT_SHORTEN_FRAME], shortcut::GLOBAL)) document.snapshot("Shorten Frame"); + if (shortcut(manager.chords[SHORTCUT_SHORTEN_FRAME], shortcut::GLOBAL, true)) { - frame->shorten(); - document.change(Document::FRAMES); + + if (auto frame = document.frame_get()) + { + frame->shorten(); + document.change(Document::FRAMES); + } } - } - if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_EXTEND_FRAME])) document.snapshot("Extend Frame"); - if (chord_repeating(manager.chords[SHORTCUT_EXTEND_FRAME])) - { - if (auto frame = document.frame_get()) + if (shortcut(manager.chords[SHORTCUT_EXTEND_FRAME], shortcut::GLOBAL)) document.snapshot("Extend Frame"); + if (shortcut(manager.chords[SHORTCUT_EXTEND_FRAME], shortcut::GLOBAL, true)) { - frame->extend(); - document.change(Document::FRAMES); + + if (auto frame = document.frame_get()) + { + frame->extend(); + document.change(Document::FRAMES); + } } } } diff --git a/src/imgui/window/welcome.cpp b/src/imgui/window/welcome.cpp index b9d997f..6eab222 100644 --- a/src/imgui/window/welcome.cpp +++ b/src/imgui/window/welcome.cpp @@ -36,7 +36,8 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Recent Files Child", {}, ImGuiChildFlags_Borders)) { - for (auto [i, file] : std::views::enumerate(manager.recentFiles)) + auto recentFiles = manager.recent_files_ordered(); + for (auto [i, file] : std::views::enumerate(recentFiles)) { ImGui::PushID(i); diff --git a/src/loader.cpp b/src/loader.cpp index d5d08af..d52617c 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -118,6 +118,22 @@ namespace anm2ed logger.info("Initialized SDL"); + if (settings.isDefault) + { + if (auto display = SDL_GetPrimaryDisplay(); display != 0) + { + if (auto mode = SDL_GetCurrentDisplayMode(display)) + { + if (mode->w >= 3840 || mode->h >= 2160) + settings.uiScale = 1.5f; + else + logger.warning(std::format("Failed to query primary display mode: {}", SDL_GetError())); + } + } + else + logger.warning("Failed to detect primary display for UI scaling."); + } + if (!MIX_Init()) logger.warning(std::format("Could not initialize SDL_mixer! {}", SDL_GetError())); else @@ -149,11 +165,10 @@ namespace anm2ed logger.info(std::format("Initialized OpenGL {}", (const char*)glGetString(GL_VERSION))); glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glLineWidth(2.0f); glDisable(GL_DEPTH_TEST); glDisable(GL_LINE_SMOOTH); - glClearColor(color::BLACK.r, color::BLACK.g, color::BLACK.b, color::BLACK.a); IMGUI_CHECKVERSION(); if (!ImGui::CreateContext()) diff --git a/src/manager.cpp b/src/manager.cpp index 68af292..5cbfd61 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1,6 +1,7 @@ #include "manager.h" #include +#include #include "filesystem_.h" #include "log.h" @@ -22,11 +23,37 @@ namespace anm2ed if (parent.empty()) return; std::error_code ec{}; std::filesystem::create_directories(parent, ec); - if (ec) - logger.warning(std::format("Could not create directory for {}: {}", path.string(), ec.message())); + if (ec) logger.warning(std::format("Could not create directory for {}: {}", path.string(), ec.message())); } } + void Manager::selection_history_push(int index) + { + if (index < 0 || index >= (int)documents.size()) return; + selectionHistory.erase(std::remove(selectionHistory.begin(), selectionHistory.end(), index), + selectionHistory.end()); + selectionHistory.push_back(index); + } + + void Manager::selection_history_cleanup(int removedIndex) + { + if (removedIndex >= 0) + { + for (auto& entry : selectionHistory) + { + if (entry == removedIndex) + entry = -1; + else if (entry > removedIndex) + --entry; + } + } + + selectionHistory.erase(std::remove_if(selectionHistory.begin(), selectionHistory.end(), + [&](int idx) { return idx < 0 || idx >= (int)documents.size(); }), + selectionHistory.end()); + if (documents.empty()) selectionHistory.clear(); + } + 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"; } @@ -57,6 +84,7 @@ namespace anm2ed selected = (int)documents.size() - 1; pendingSelected = selected; + selection_history_push(selected); toasts.info(std::format("Opened document: {}", pathString)); } @@ -96,6 +124,7 @@ namespace anm2ed autosave_files_write(); documents.erase(documents.begin() + index); + selection_history_cleanup(index); if (documents.empty()) { @@ -104,7 +133,13 @@ namespace anm2ed return; } - if (selected >= index) selected = std::max(0, selected - 1); + if (!selectionHistory.empty()) + { + selected = selectionHistory.back(); + selectionHistory.pop_back(); + } + else if (selected >= index) + selected = std::max(0, selected - 1); selected = std::clamp(selected, 0, (int)documents.size() - 1); pendingSelected = selected; @@ -123,6 +158,7 @@ namespace anm2ed index = std::clamp(index, 0, (int)documents.size() - 1); selected = index; + selection_history_push(selected); if (auto document = get()) document->change(Document::ALL); } @@ -177,20 +213,42 @@ namespace anm2ed nullPropertiesPopup.close(); } + void Manager::recent_files_trim() + { + while (recentFiles.size() > RECENT_LIMIT) + { + auto oldest = std::min_element(recentFiles.begin(), recentFiles.end(), + [](const auto& lhs, const auto& rhs) { return lhs.second < rhs.second; }); + if (oldest == recentFiles.end()) break; + recentFiles.erase(oldest); + } + } + + std::vector Manager::recent_files_ordered() const + { + std::vector> orderedEntries(recentFiles.begin(), recentFiles.end()); + std::sort(orderedEntries.begin(), orderedEntries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.second > rhs.second; }); + + std::vector ordered; + ordered.reserve(orderedEntries.size()); + for (const auto& [pathString, _] : orderedEntries) + ordered.emplace_back(pathString); + return ordered; + } + void Manager::recent_file_add(const std::filesystem::path& path) { if (path.empty()) return; - const auto pathString = path.string(); std::error_code ec{}; if (!std::filesystem::exists(path, ec)) { - logger.warning(std::format("Skipping missing recent file: {}", pathString)); + logger.warning(std::format("Skipping missing recent file: {}", path.string())); return; } - 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); + recentFiles[path.string()] = ++recentFilesCounter; + recent_files_trim(); recent_files_write(); } @@ -208,21 +266,32 @@ namespace anm2ed logger.info(std::format("Loading recent files from: {}", path.string())); std::string line{}; + std::vector loaded{}; + std::unordered_set seen{}; while (std::getline(file, line)) { if (line.empty()) continue; if (!line.empty() && line.back() == '\r') line.pop_back(); std::filesystem::path entry = line; - if (std::find(recentFiles.begin(), recentFiles.end(), entry) != recentFiles.end()) continue; std::error_code ec{}; if (!std::filesystem::exists(entry, ec)) { logger.warning(std::format("Skipping missing recent file: {}", line)); continue; } - recentFiles.emplace_back(std::move(entry)); + auto entryString = entry.string(); + if (!seen.insert(entryString).second) continue; + loaded.emplace_back(std::move(entryString)); } + + recentFiles.clear(); + recentFilesCounter = 0; + for (auto it = loaded.rbegin(); it != loaded.rend(); ++it) + { + recentFiles[*it] = ++recentFilesCounter; + } + recent_files_trim(); } void Manager::recent_files_write() @@ -239,13 +308,15 @@ namespace anm2ed return; } - for (auto& entry : recentFiles) + auto ordered = recent_files_ordered(); + for (auto& entry : ordered) file << entry.string() << '\n'; } void Manager::recent_files_clear() { recentFiles.clear(); + recentFilesCounter = 0; recent_files_write(); } diff --git a/src/manager.h b/src/manager.h index 00effcb..6383441 100644 --- a/src/manager.h +++ b/src/manager.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "document.h" @@ -14,10 +16,14 @@ namespace anm2ed { std::filesystem::path recent_files_path_get(); std::filesystem::path autosave_path_get(); + void selection_history_push(int); + void selection_history_cleanup(int removedIndex); + void recent_files_trim(); public: std::vector documents{}; - std::vector recentFiles{}; + std::map> recentFiles{}; + std::size_t recentFilesCounter{}; std::vector autosaveFiles{}; int selected{-1}; @@ -31,6 +37,14 @@ namespace anm2ed ImGuiKeyChord chords[SHORTCUT_COUNT]{}; + std::vector anm2DragDropPaths{}; + bool isAnm2DragDrop{}; + imgui::PopupHelper anm2DragDropPopup{ + imgui::PopupHelper("Anm2 Drag Drop", imgui::POPUP_NORMAL, imgui::POPUP_BY_CURSOR)}; + + std::filesystem::path spritesheetDragDropPath{}; + bool isSpritesheetDragDrop{}; + anm2::Layer editLayer{}; imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; @@ -39,6 +53,8 @@ namespace anm2ed imgui::PopupHelper progressPopup{imgui::PopupHelper("Rendering...", imgui::POPUP_SMALL_NO_HEIGHT)}; + std::vector selectionHistory{}; + Manager(); ~Manager(); @@ -63,6 +79,7 @@ namespace anm2ed void recent_files_write(); void recent_files_clear(); void recent_file_add(const std::filesystem::path&); + std::vector recent_files_ordered() const; void autosave_files_load(); void autosave_files_open(); diff --git a/src/playback.cpp b/src/playback.cpp index cb6ce18..8d4be14 100644 --- a/src/playback.cpp +++ b/src/playback.cpp @@ -19,7 +19,7 @@ namespace anm2ed time += (float)fps / 30.0f; - if (time >= (float)length) + if (time > (float)length - 1.0f) { if (isLoop) time = 0.0f; diff --git a/src/render.h b/src/render.h index 22df9a7..b5b7985 100644 --- a/src/render.h +++ b/src/render.h @@ -7,6 +7,7 @@ namespace anm2ed::render { #define RENDER_LIST \ X(PNGS, "PNGs", "") \ + X(SPRITESHEET, "Spritesheet (PNG)", ".png") \ X(GIF, "GIF", ".gif") \ X(WEBM, "WebM", ".webm") \ X(MP4, "MP4", ".mp4") diff --git a/src/resource/texture.cpp b/src/resource/texture.cpp index 92a1847..4b1c1d8 100644 --- a/src/resource/texture.cpp +++ b/src/resource/texture.cpp @@ -29,9 +29,9 @@ using namespace glm; namespace anm2ed::resource { - bool Texture::is_valid() { return id != 0; } + bool Texture::is_valid() const { return id != 0; } - size_t Texture::pixel_size_get() { return size.x * size.y * CHANNELS; } + size_t Texture::pixel_size_get() const { return size.x * size.y * CHANNELS; } void Texture::upload(const uint8_t* data) { @@ -133,6 +133,20 @@ namespace anm2ed::resource bool Texture::write_png(const std::filesystem::path& path) { return write_png(path.string()); } + vec4 Texture::pixel_read(vec2 position) const + { + if (pixels.size() < CHANNELS || size.x <= 0 || size.y <= 0) return vec4(0.0f); + + int x = glm::clamp((int)(position.x), 0, size.x - 1); + int y = glm::clamp((int)(position.y), 0, size.y - 1); + + auto index = ((size_t)(y) * (size_t)(size.x) + (size_t)(x)) * CHANNELS; + if (index + CHANNELS > pixels.size()) return vec4(0.0f); + + return vec4(uint8_to_float(pixels[index + 0]), uint8_to_float(pixels[index + 1]), uint8_to_float(pixels[index + 2]), + uint8_to_float(pixels[index + 3])); + } + void Texture::pixel_set(ivec2 position, vec4 color) { if (position.x < 0 || position.y < 0 || position.x >= size.x || position.y >= size.y) return; diff --git a/src/resource/texture.h b/src/resource/texture.h index 5ae03d5..7a41d55 100644 --- a/src/resource/texture.h +++ b/src/resource/texture.h @@ -23,10 +23,11 @@ namespace anm2ed::resource int channels{}; std::vector pixels{}; - bool is_valid(); - size_t pixel_size_get(); + bool is_valid() const; + size_t pixel_size_get() const; void upload(); void upload(const uint8_t*); + glm::vec4 pixel_read(glm::vec2) const; Texture(); ~Texture(); @@ -40,6 +41,7 @@ namespace anm2ed::resource Texture(const std::filesystem::path&); bool write_png(const std::string&); bool write_png(const std::filesystem::path&); + static bool write_pixels_png(const std::filesystem::path&, glm::ivec2, const uint8_t*); void pixel_set(glm::ivec2, glm::vec4); void pixel_line(glm::ivec2, glm::ivec2, glm::vec4); }; diff --git a/src/resources.cpp b/src/resources.cpp index d4a5934..b386bca 100644 --- a/src/resources.cpp +++ b/src/resources.cpp @@ -1,6 +1,6 @@ #include "resources.h" -#include +#include #include "music.h" @@ -10,14 +10,23 @@ namespace anm2ed { Resources::Resources() { - for (auto [i, font] : std::views::enumerate(font::FONTS)) - fonts[i] = Font((void*)font.data, font.length, font::SIZE); + for (std::size_t i = 0; i < font::COUNT; ++i) + { + const auto& fontInfo = font::FONTS[i]; + fonts[i] = Font((void*)fontInfo.data, fontInfo.length, font::SIZE); + } - for (auto [i, icon] : std::views::enumerate(icon::ICONS)) - icons[i] = Texture(icon.data, icon.length, icon.size); + for (std::size_t i = 0; i < icon::COUNT; ++i) + { + const auto& iconInfo = icon::ICONS[i]; + icons[i] = Texture(iconInfo.data, iconInfo.length, iconInfo.size); + } - for (auto [i, shader] : std::views::enumerate(shader::SHADERS)) - shaders[i] = Shader(shader.vertex, shader.fragment); + for (std::size_t i = 0; i < shader::COUNT; ++i) + { + const auto& shaderInfo = shader::SHADERS[i]; + shaders[i] = Shader(shaderInfo.vertex, shaderInfo.fragment); + } }; resource::Audio& Resources::music_track() diff --git a/src/resources.h b/src/resources.h index 7e7a0d8..46a67ff 100644 --- a/src/resources.h +++ b/src/resources.h @@ -15,6 +15,7 @@ namespace anm2ed public: resource::Font fonts[resource::font::COUNT]{}; resource::Texture icons[resource::icon::COUNT]{}; + resource::Texture backgroundTexture{}; resource::Shader shaders[resource::shader::COUNT]{}; resource::Audio music{}; diff --git a/src/settings.cpp b/src/settings.cpp index 7429bbb..7762082 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -111,6 +111,7 @@ DockSpace ID=0x123F8F08 Window=0x6D581B32 Pos=8,62 Size=1902,994 Split { logger.warning("Settings file does not exist; using default"); save(path, IMGUI_DEFAULT); + isDefault = true; } std::ifstream file(path); diff --git a/src/settings.h b/src/settings.h index 7e4e09b..6d0d22d 100644 --- a/src/settings.h +++ b/src/settings.h @@ -96,7 +96,7 @@ namespace anm2ed X(PREVIEW_GRID_OFFSET, previewGridOffset, "Offset", IVEC2, {}) \ X(PREVIEW_GRID_COLOR, previewGridColor, "Color", VEC4, {1.0f, 1.0f, 1.0f, 0.125f}) \ X(PREVIEW_AXES_COLOR, previewAxesColor, "Color", VEC4, {1.0f, 1.0f, 1.0f, 0.125f}) \ - X(PREVIEW_BACKGROUND_COLOR, previewBackgroundColor, "Background Color", VEC4, {0.113f, 0.184f, 0.286f, 1.0f}) \ + X(PREVIEW_BACKGROUND_COLOR, previewBackgroundColor, "Background Color", VEC3, {0.113f, 0.184f, 0.286f}) \ \ X(PROPERTIES_IS_ROUND, propertiesIsRound, "Round", BOOL, false) \ \ @@ -112,12 +112,13 @@ namespace anm2ed X(EDITOR_IS_GRID, editorIsGrid, "Grid", BOOL, true) \ X(EDITOR_IS_GRID_SNAP, editorIsGridSnap, "Snap", BOOL, true) \ X(EDITOR_IS_BORDER, editorIsBorder, "Border", BOOL, true) \ + X(EDITOR_IS_TRANSPARENT, editorIsTransparent, "Transparent", BOOL, true) \ X(EDITOR_START_ZOOM, editorStartZoom, "Zoom", FLOAT, 200.0f) \ X(EDITOR_SIZE, editorSize, "Size", IVEC2_WH, {1200, 600}) \ X(EDITOR_GRID_SIZE, editorGridSize, "Grid Size", IVEC2, {32, 32}) \ X(EDITOR_GRID_OFFSET, editorGridOffset, "Offset", IVEC2, {32, 32}) \ X(EDITOR_GRID_COLOR, editorGridColor, "Color", VEC4, {1.0, 1.0, 1.0, 0.125}) \ - X(EDITOR_BACKGROUND_COLOR, editorBackgroundColor, "Background Color", VEC4, {0.113, 0.184, 0.286, 1.0}) \ + X(EDITOR_BACKGROUND_COLOR, editorBackgroundColor, "Background Color", VEC3, {0.113, 0.184, 0.286}) \ \ X(MERGE_TYPE, mergeType, "Type", INT, 0) \ X(MERGE_IS_DELETE_ANIMATIONS_AFTER, mergeIsDeleteAnimationsAfter, "Delete Animations After", BOOL, false) \ @@ -143,8 +144,10 @@ namespace anm2ed X(TOOL, tool, "##Tool", INT, 0) \ X(TOOL_COLOR, toolColor, "##Color", VEC4, {1.0, 1.0, 1.0, 1.0}) \ \ - X(RENDER_TYPE, renderType, "Output", INT, render::PNGS) \ - X(RENDER_PATH, renderPath, "Path", STRING, ".") \ + X(RENDER_TYPE, renderType, "Output", INT, render::GIF) \ + X(RENDER_PATH, renderPath, "Path", STRING, "./output.gif") \ + X(RENDER_ROWS, renderRows, "Rows", INT, 0) \ + X(RENDER_COLUMNS, renderColumns, "Columns", INT, 0) \ X(RENDER_FORMAT, renderFormat, "Format", STRING, "{}.png") \ X(RENDER_IS_RAW_ANIMATION, renderIsRawAnimation, "Raw Animation", BOOL, true) \ X(RENDER_SCALE, renderScale, "Scale", FLOAT, 1.0f) \ @@ -225,6 +228,8 @@ namespace anm2ed SETTINGS_MEMBERS SETTINGS_SHORTCUTS SETTINGS_WINDOWS #undef X + bool isDefault{}; + Settings() = default; Settings(const std::string&); diff --git a/src/state.cpp b/src/state.cpp index 916999e..a773d19 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -1,5 +1,7 @@ #include "state.h" +#include + #include #include @@ -27,19 +29,7 @@ namespace anm2ed manager.chords_set(settings); } - void State::tick(Settings& settings) - { - - if (auto document = manager.get()) - { - if (auto animation = document->animation_get()) - if (document->playback.isPlaying) - document->playback.tick(document->anm2.info.fps, animation->frameNum, - (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording); - } - - dockspace.tick(manager, settings); - } + void State::tick(Settings& settings) { dockspace.tick(manager, settings); } void State::update(SDL_Window*& window, Settings& settings) { @@ -55,7 +45,16 @@ namespace anm2ed auto droppedFile = event.drop.data; if (filesystem::path_is_extension(droppedFile, "anm2")) { - manager.open(std::string(droppedFile)); + std::filesystem::path droppedPath{droppedFile}; + if (manager.documents.empty()) + manager.open(droppedPath); + else + { + if (std::find(manager.anm2DragDropPaths.begin(), manager.anm2DragDropPaths.end(), droppedPath) == + manager.anm2DragDropPaths.end()) + manager.anm2DragDropPaths.push_back(droppedPath); + manager.isAnm2DragDrop = true; + } SDL_FlashWindow(window, SDL_FLASH_UNTIL_FOCUSED); } else if (filesystem::path_is_extension(droppedFile, "png")) @@ -96,6 +95,7 @@ namespace anm2ed { glViewport(0, 0, settings.windowSize.x, settings.windowSize.y); glClear(GL_COLOR_BUFFER_BIT); + ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); SDL_GL_SwapWindow(window); @@ -107,12 +107,6 @@ namespace anm2ed auto currentTick = SDL_GetTicks(); auto currentUpdate = SDL_GetTicks(); - if (currentTick - previousTick >= TICK_INTERVAL) - { - tick(settings); - previousTick = currentTick; - } - if (currentUpdate - previousUpdate >= UPDATE_INTERVAL) { update(window, settings); @@ -120,6 +114,12 @@ namespace anm2ed previousUpdate = currentUpdate; } + if (currentTick - previousTick >= TICK_INTERVAL) + { + tick(settings); + previousTick = currentTick; + } + SDL_Delay(1); } }